使用正規表示式取代 Java 中的字串:向後引用與環視
1. 概述
在本教程中,我們將研究如何使用String類別中提供的replaceAll()來使用正規表示式來取代文字。此外,我們將學習兩種方法(後向引用和環視)來執行相同的操作,然後比較它們的表現。
我們首先描述第一種方法。
2. 將後向引用與replaceAll()一起使用
為了理解反向引用,我們首先需要了解匹配組。簡而言之,一個群體不過是被視為一個單位的多個角色。因此,反向引用是正規表示式中的功能,它允許我們引用同一正規表示式中先前符合的群組。通常,我們會用引用模式中捕獲群組的數字來表示它們,例如\1 、 \2等。
例如,正規表示式(a)(b)\1使用\1來引用第一個被捕獲的組,在我們的例子中是(a) 。
在字串替換操作中,我們使用這些引用將匹配的文字替換為我們想要的文字。當使用replaceAll()方法時,我們將替換字串中的捕獲組稱為$1 、 $2等。
現在,為了更好地理解,讓我們考慮以下用例。我們想要刪除字串中的所有星號符號。因此,任務是僅保留出現在字串開頭或結尾的星號,同時刪除所有其他星號。例如, *text*保持不變,而**te*x**t**變為*text* 。
2.1.實施反向引用
為了完成我們的任務,我們將使用具有正規表示式的replaceAll()方法,並在以下位置使用反向引用:
String str = "**te*xt**";
String replaced = str.replaceAll("(^\\*)|(\\*$)|\\*", "$1$2");
assertEquals("*text*", replaced);
上面,我們定義了正規表示式“(^\\*)|(\\*$)|\\*”它由三個部分組成。第一組(^\\*)捕獲字串開頭的星號。第二組(\\*$)捕獲字串末尾的星號。第三組\\*捕捉所有其餘的星號。因此,正規表示式僅選擇字串的某些部分,並且只有那些選定的部分將被替換。我們用不同的顏色突出顯示不同的部分:
簡而言之,替換字串$1$2會傳回該組中所有選定的字符,因此它們保留在最終字串中。
讓我們看看解決相同任務的不同方法。
3. 將Lookaround與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();
}
}
此範例的結果輸出清楚地表明,lookaround 方法的效能更高:
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 上查看完整的程式碼。