1. 概述
在数据处理和文本解析中,从字符串中提取数值是一项基础但关键的操作。无论是解析 ID、提取电话号码、识别邮编,还是清洗日志中的金额信息,都离不开这项能力。
本文将介绍几种在 Kotlin 中从字符串提取数字的常用方法,并结合实际代码示例帮助你掌握不同方案的优劣和适用场景,避免踩坑。
2. 前提假设
为了聚焦核心逻辑,我们对“提取数字”这一任务做如下限定:
- ✅ 只提取 十进制正整数
- ✅ 支持超大数值(使用
BigInteger
类型) - ❌ 不处理小数(如
3.14
) - ❌ 不处理负数(如
-123
) - ❌ 不处理十六进制、二进制等非十进制数
这样可以简化实现,同时保证对大数的安全支持。
3. 使用循环遍历
最直观的方式是手动遍历每个字符,逐个判断是否为数字,并拼接连续的数字串。
fun extractNumbersUsingLoop(str: String): List<BigInteger> {
val numbers = mutableListOf<BigInteger>()
val currentNumber = StringBuilder()
for (char in str) {
if (char.isDigit()) {
currentNumber.append(char)
} else if (currentNumber.isNotEmpty()) {
numbers.add(currentNumber.toString().toBigInteger())
currentNumber.clear()
}
}
if (currentNumber.isNotEmpty()) {
numbers.add(currentNumber.toString().toBigInteger())
}
return numbers
}
关键点说明:
- ✅ 使用
StringBuilder
高效拼接字符 - ✅ 遇到非数字字符时,触发当前数字串的提交
- ✅ 循环结束后别忘了处理末尾残留的数字(容易踩坑!)
- ⚠️ 手动状态管理,代码稍显冗长,但逻辑清晰可控
适合对性能要求高、且需要自定义行为(比如跳过某些特殊分隔符)的场景。
4. 使用正则表达式
正则表达式是处理模式匹配的利器。我们可以用 \d+
匹配一个或多个连续数字。
fun extractMultipleUsingRegex(str: String): List<BigInteger> {
return Regex("\\d+").findAll(str).map { it.value.toBigInteger() }.toList()
}
特点分析:
- ✅ 一行搞定,简洁优雅
- ✅
findAll()
返回所有匹配结果的序列,延迟计算效率高 - ✅
it.value
是匹配到的原始字符串,转成BigInteger
安全无溢出 - ❌ 对正则不熟的同学可能觉得
\d+
神秘,但这是标准写法,建议记牢
🔍 小贴士:
\d
表示 digit,+
表示一个或多个,合起来就是“连续数字”。
5. 使用 split + 正则
另一种思路是:把非数字当作分隔符,直接切开字符串。
fun extractNumbersUsingSplitAndRegex(str: String): List<BigInteger> {
return str.split(Regex("\\D+"))
.filter { it.isNotBlank() }
.map { it.toBigInteger() }
}
解析:
- ✅
\\D+
:匹配一个或多个非数字字符(注意大小写:\D
是 non-digit) - ✅
split()
切割后会得到空字符串或纯数字字符串 - ✅
filter { it.isNotBlank() }
去除空项,防止""
转BigInteger
报错 - ✅ 链式调用,函数式风格清爽
⚠️ 注意:如果原字符串以数字开头或结尾,split
仍能正确处理,但中间若全是非数字,会产生多个空串,必须过滤。
6. 测试验证
再好的逻辑也得靠测试兜底。我们使用 JUnit 5 的参数化测试来覆盖多种场景。
添加依赖
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
参数化测试示例
@ParameterizedTest
@CsvSource(
"string with 123 and 333 in the text, 123:333",
"another string with 456 and 789, 456:789",
"string 123-234, 123:234",
"no numbers,",
"3 4 50 numbers6, 3",
"91234567891011121314151617181920number, 91234567891011121314151617181920",
"123456789large numbers0, 123456789"
)
fun `extract all occurrences of numbers from string using regex`(str: String, expected: String?) {
val numbers = extractMultipleUsingRegex(str)
val expectedList = expected?.split(":")?.map { it.toBigInteger() } ?: emptyList()
Assertions.assertEquals(expectedList, numbers)
}
测试设计要点:
- ✅ 使用
@CsvSource
定义多组输入输出 - ✅ 自定义分隔符
:
来区分预期多个数字 - ✅
expected
为 null 时表示无数字,返回空列表 - ✅ 覆盖了大数、混合字符、边界情况等典型场景
边界测试补充
对于空字符串这种特殊情况,建议单独写一个简单测试,避免参数化格式混乱:
@Test
fun `check empty string scenario for split`() {
val numbers = extractNumbersUsingSplitAndRegex("")
Assertions.assertIterableEquals(numbers, listOf<BigInteger>())
}
✅ 清晰明了,不易出错。
7. 总结
方法 | 优点 | 缺点 | 推荐场景 |
---|---|---|---|
✅ 循环遍历 | 控制精细,性能好 | 代码多,易漏边界 | 高频调用、需定制逻辑 |
✅ 正则 findAll | 简洁高效,语义清晰 | 需懂正则 | 大多数通用场景 |
✅ split + 正则 | 函数式风格,易读 | 多一次过滤步骤 | 字符串切割思维自然的场景 |
💡 推荐优先使用
Regex("\\d+").findAll()
方案——简洁、安全、可读性强,是 Kotlin 社区中的主流做法。
文中所有示例代码已托管至 GitHub:
👉 https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-strings-5
建议集合,下次遇到类似需求直接抄作业,少走弯路。