Java中使用正则表达式替换字符串:后向引用与环视的比较
Java中使用正则表达式替换字符串:后向引用与环视的比较
在本教程中,我们将学习如何使用String类中提供的replaceAll()方法使用正则表达式替换文本。此外,我们将学习两种方法,后向引用和环视,来执行相同的操作,然后比较它们的性能。
让我们首先描述第一种方法。
2. 使用replaceAll()中的后向引用
要理解后向引用,我们首先需要了解匹配组。简单来说,一个组不过是将多个字符视为一个单元。因此,后向引用是正则表达式中的一个特性,它允许我们在同一个正则表达式中引用先前匹配的组。通常,我们使用数字来表示模式中的捕获组,如\1、\2等。
例如,正则表达式(a)(b)\1使用\1来引用第一个捕获的组,在我们的例子中是(a)。
在字符串替换操作中,我们使用这些引用将匹配的文本替换为我们想要的文本。当使用replaceAll()方法时,我们在替换字符串中引用捕获组为$1、$2等。
现在,为了更好地理解,让我们考虑以下用例。我们想要删除字符串中的所有星号符号。因此,任务是仅在字符串的开头或结尾保留星号,同时删除所有其他星号。例如,text保持不变,而text变为text。
2.1 实现后向引用
为了完成我们的任务,我们将使用replaceAll()方法与正则表达式,并在其中使用后向引用:
String str = "*te*xt**";
String replaced = str.replaceAll("(\\*)|(\\*$)|\\*", "$1$2");
assertEquals("*text*", replaced);
在上面,我们定义了正则表达式“(\)|(\$)|\”,它由三部分组成。第一组(\)捕获字符串开头的星号。第二组(\*$)捕获字符串结尾的星号。第三组\*捕获所有其余的星号。因此,正则表达式只选择字符串的某些部分,只有这些被选择的部分将被替换。我们用不同的颜色突出显示不同的部分:
简而言之,替换字符串$1$2返回该组中选择的所有字符,以便它们保留在最终字符串中。
让我们看看解决相同任务的不同方法。
3. 使用replaceAll()中的环视
后向引用的替代方法是使用环视,它允许我们在进行正则表达式匹配时忽略周围的字符。在我们的示例中,我们可以更直观地删除字符串中的星号:
String str = "*te*xt**";
String replacedUsingLookaround = str.replaceAll("(?\<!^)\\*+(?!$)", "");
assertEquals("*text*", replacedUsingLookaround);
在这个例子中,(?<!)\*+捕获一个或多个星号(\*+),这些星号前面没有字符串的开头((?<!))。简而言之,我们正在进行一个负向环视。接下来,(?!$)部分是一个负向环视,我们定义它来忽略后面是字符串结尾的星号。最后,空的替换字符串在这里删除所有匹配的字符。因此,这种方法更容易理解,因为我们选择所有我们想要删除的字符:
除了可读性之外,这两种方法在性能上也有所不同。让我们接下来检查它们。
4. 性能比较:环视与后向引用
为了比较这两种方法的性能,我们将使用JMH库进行基准测试,并测量每种方法处理大量字符串替换所需的平均执行时间。
对于我们的性能测试,我们将使用前面任务中的相同星号示例。简而言之,我们将重复使用replaceAll()函数与两种正则表达式方法各1000次。
对于此测试,我们将配置2次预热迭代和5次测量迭代。此外,我们将测量完成任务所需的平均时间:
@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()方法与后向引用和环视。虽然后向引用对于重用匹配字符串的部分很有用,但由于捕获组的开销,它们可能会更慢。为了证明这一点,我们进行了基准测试来比较这两种方法。
如常,我们可以在GitHub上查看完整的代码。