1. 概述
本文将深入探讨如何使用Java中String
类的replaceAll()
方法,通过正则表达式实现文本替换。我们将重点学习两种核心技术:反向引用(Back Reference)和环视(Lookaround),并对比它们的性能表现。下面先从第一种方法讲起。
2. 结合反向引用使用replaceAll()
要理解反向引用,首先需要掌握匹配组(Matching Group)的概念。简单说,匹配组就是将多个字符视为一个整体单位。反向引用允许我们在同一正则表达式中引用之前捕获的组,通常用\1
、\2
等数字表示对应的捕获组。
示例:正则表达式
(a)(b)\1
中的\1
引用了第一个捕获组(a)
。
在字符串替换操作中,我们利用这些引用将匹配文本替换为目标内容。使用replaceAll()
时,替换字符串中通过$1
、$2
等引用捕获组。
考虑一个实际场景:移除字符串中间的星号(*
),仅保留开头和结尾的星号。例如:
*text*
保持不变**te*x**t**
转换为*text*
2.1. 反向引用实现
String str = "**te*xt**";
String replaced = str.replaceAll("(^\\*)|(\\*$)|\\*", "$1$2");
assertEquals("*text*", replaced);
正则表达式(^\\*)|(\\*$)|\\*
包含三个部分:
(^\\*)
:捕获字符串开头的星号(\\*$)
:捕获字符串结尾的星号\\*
:捕获所有其他位置的星号
替换字符串$1$2
仅保留首尾星号(对应组1和组2),其他星号被替换为空。不同匹配部分用颜色区分如下:
接下来看另一种更直观的实现方式。
3. 结合环视使用replaceAll()
环视(Lookaround)允许在匹配时忽略周围字符,提供更简洁的解决方案:
String str = "**te*xt**";
String replacedUsingLookaround = str.replaceAll("(?<!^)\\*+(?!$)", "");
assertEquals("*text*", replacedUsingLookaround);
正则表达式(?<!^)\\*+(?!$)
解析:
(?<!^)
:负向后查找,确保星号前不是字符串开头\\*+
:匹配一个或多个星号(?!$)
:负向前查找,确保星号后不是字符串结尾
空替换字符串直接移除匹配的中间星号。匹配范围可视化如下:
除可读性外,两种方法性能差异显著,下面通过基准测试验证。
4. 性能对比:环视 vs 反向引用
使用JMH基准测试框架,测量两种方法处理大量字符串替换的平均耗时。测试配置:
- 测试字符串:
"*example*text**with*many*asterisks**"
重复1000次 - 预热迭代:2次
- 测量迭代:5次
- 输出单位:毫秒(ms)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(1)
@Warmup(iterations = 2)
@Measurement(iterations = 5)
public class RegexpBenchmark {
private static final int ITERATIONS_COUNT = 1000;
@State(Scope.Benchmark)
public static class BenchmarkState {
String testString = "*example*text**with*many*asterisks**".repeat(ITERATIONS_COUNT);
}
@Benchmark
public void backReference(BenchmarkState state) {
state.testString.replaceAll("(^\\*)|(\\*$)|\\*", "$1$2");
}
@Benchmark
public void lookaround(BenchmarkState state) {
state.testString.replaceAll("(?<!^)\\*+(?!$)", "");
}
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(RegexpBenchmark.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
测试结果明确显示环视性能更优:
Benchmark Mode Cnt Score Error Units
RegexpBenchmark.backReference avgt 5 0.504 ± 0.011 ms/op
RegexpBenchmark.lookaround avgt 5 0.315 ± 0.006 ms/op
反向引用较慢的原因:需要额外开销单独捕获组,再用替换字符串重组结果。而环视直接定位目标字符并删除,效率更高。
5. 总结
本文展示了如何通过replaceAll()
结合反向引用和环视实现正则替换。反向引用适合复用匹配文本片段,但捕获组开销会降低性能。基准测试证明,在简单替换场景中,环视是更优选择。
✅ 核心要点:
- 反向引用:
$n
语法保留捕获组内容 - 环视:
(?<!X)
和(?!Y)
排除特定上下文 - 性能:环视比反向引用快约37%
完整代码示例可在GitHub获取。