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. (\\*$):捕获字符串结尾的星号
  3. \\*:捕获所有其他位置的星号

替换字符串$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获取。


原始标题:Replacing Strings in Java Using Regex: Back Reference vs. Lookaround