在 Java 中,从路径类字符串(如 `/good/123/bad/456`)中提取数字时,直接使用正则 `Matcher` 配合 `Long.parseLong(CharSequence, int, int, int)`(Java 9+)比 `String.split()` 或 `Pattern.matcher().group()` 更高效,可避免不必要的字符串拷贝和对象创建,从而提升性能。
从路径类字符串(比如 /good/123/bad/456)里高效提取数字,到底哪种方法才是最优选?先给出结论:在 Java 9 及更高版本中,直接使用预编译的正则 Matcher 配合 Long.parseLong(CharSequence, int, int, int) 这套组合拳,比我们常用的 String.split() 或 Pattern.matcher().group() 要高效得多。核心原因在于,它能从根本上避免掉不必要的字符串拷贝和对象创建,从而显著降低内存开销与 GC 压力。
如果场景是高频调用或性能敏感(比如日志解析、路由匹配、协议解码),底层那些字符串操作的细节就绝不能忽视。咱们就拿一个具体例子来拆解——比如要解析 "/good/312321312/bad/3213122131" 这个字符串,目标是干净利落地取出两个数字 312321312 和 3213122131。很多人第一反应是用 split("/"),代码写起来确实很直观:
String[] parts = s.split("/");// 然后遍历 parts 找数字 → 低效:创建 5 个新 String 对象,含 "/good"、""、"312321312" 等
问题就藏在这里。 split() 内部会为每一个分割出来的片段都调用 substring(),而在 Java 9+ 里,substring() 虽然已经改成复制字符数组了,但还是要重新分配新的字符串对象并拷贝对应的字节。想想看,我们只是想要那两个数字,结果却白白地多创建了 "good"、"bad" 这些中间对象,对于数值提取来说,这是纯粹的额外负担,也会拖慢整体性能。
那么,更优的方案是什么?关键思路是:复用预编译的 Pattern,结合 Matcher 定位数字的起止索引,然后用零拷贝的方式直接从原始字符串中读取目标片段。代码实践起来是这样的:
private static final Pattern DIGIT_PATTERN = Pattern.compile("\\d+");public static long[] extractNumbers(String s) { Matcher m = DIGIT_PATTERN.matcher(s); LongStream.Builder builder = LongStream.builder(); while (m.find()) { // Java 9+ 新增重载:无需创建子串,直接解析 char 序列区间 long num = Long.parseLong(s, m.start(), m.end(), 10); builder.add(num); } return builder.build().toArray();}
这才是关键优化所在,我们一处一处来看:
- 复用预编译的 Pattern:将
Pattern声明为static final,避免了每次调用方法时都重新编译正则的巨大开销。那其实是String.split(String)内部隐式干的事情,在这里被我们绕过去了。 - 零字符串创建:
Long.parseLong(CharSequence, int, int, int)这个重载的绝妙之处在于,它直接操作原字符串的逻辑区间进行解析,完全绕过了m.group()生成临时字符串这一步骤。 - 减少 GC 压力:因为没有产生任何中间 String 对象(尤其是像
"good"、"bad"这些非数字部分,压根不会被实例化),垃圾回收的压力自然就小了很多,这对高并发环境尤为重要。 - 更强的可控性:使用
Matcher.find()可以方便地按需提取前 N 个匹配结果,比如场景明确要求“只取前两个数字”时,这种写法就非常灵活,无需遍历所有分割片段。
不过,这个方案也有一些需要注意的地方:
- 它要求运行在 JDK 9 或更高版本上,因为这个四参数的
Long.parseLong重载是 Java 9 才加入的特性,低版本无法直接使用。 - 如果项目必须兼容 JDK 8,可以退而求其次,用
Long.parseLong(m.group())来做。虽然这比split()方案要好上不少(至少只创建了数字部分的字符串对象),但依然会多出少量字符串对象,性能稍逊一筹。 - 对于超长的字符串或者海量调用的场景,还可以进一步优化:把
Matcher实例也缓存起来(通过m.reset(s)复用),这样可以进一步减少对象分配的次数,提升吞吐量。 - 如果你的字符串中,数字的位置是固定的(比如永远都是第二个和第四个路径段),那么手动用
indexOf加substring定位也不失为一种选择。但相比之下,正则方案在可维护性和健壮性上更胜一筹——它能自动跳过非数字的干扰项,适应各种格式变化。
总结一下,在务实且追求性能的工程实践中,正则匹配配合区间解析这一套方法论,在效率、可读性和可扩展性上取得了很好的平衡,确实是一个真正优秀的选择。而 split() 要怎么做?把它留在那些逻辑简单、调用频率很低,或者确实需要拿到所有分段语义的场景里去用,才是最合适的。
