作者網址:
https://my.oschina.net/shipley/blog/98973
背景:
前幾天有人發了一個關於下面問題的貼,對這個有點好奇,故花時間做了點研究。
對單個反斜杠字符串替換成雙斜杠的Java實現如下:
String s = "\\";
方法一:String sr1 = s.replaceAll("\\\\", "\\\\\\\\");
方法二:String sr1 = s.replaceAll("\\\\", "$0$0");
我第一眼看到比較困惑,下面慢慢來分析。
分析:
對String類的replaceAll(String reg, String replacement)方法分析
一、兩點疑惑
A. 為啥第一個參數reg必須是”\\\\”?
B. 為啥第二個參數replacement 必須是”\\\\\\\\”?
二、解答
A.因為reg這個參數表示一個正則表達式,首先字符串“\\\\”被轉義后代表的實際是字符串\\,這就是正則表達式,那么在正則表達式里也有轉義,那么這個正則匹配的就是\
B.首先字符串“\\\\\\\\”被轉義后實際代表的其實是字符串\\\\;
接下來才是重點:
查看源碼replaceAll方法的實現如下
public String replaceAll(String regex, String replacement) { return Pattern.compile(regex).matcher(this).replaceAll(replacement); }
其中Pattern.compile(regex).matcher(this), 返回的是一個Matcher對象。
先簡單介紹java.util.regex.Matcher類,是通過解釋 Pattern 對 字符序列執行匹配操作的引擎,其中持有對當前Pattern對象和當前String對象的引用。
執行一次調用其find方法,即對字符串執行一次從左向右的以Pattern為正則的匹配,並記錄下匹配結果字符串的開始和結束位置索引,以及更新一個記錄當前匹配結果的分組groups。
順藤摸瓜,進入Matcher類的replaceAll方法,繼續查看源碼,
public String replaceAll(String replacement) { // 對當前Matcher類進行重置,即對其中記錄匹配結果的開始和結束位置索引,以及分組信息重置 reset(); // 執行第一次搜索 boolean result = find(); if (result) { // 第一次搜索匹配成功 // 用於記錄最終的替換結果字符串 StringBuffer sb = new StringBuffer(); // 循環搜索 do { // *重點在此方法內:用於將從上一次匹配子字符串的下一個索引位置開始,到當前匹配的子字符串的結束索引位置的所有字符 append到字符串sb中 // 有點繞,可以暫時跳過,下面會對該方法進一步分析 appendReplacement(sb, replacement); result = find(); } while (result); // 將從最后一次匹配子字符串的下一個索引位置,到字符串的結尾的所有字符append到字符串sb中 appendTail(sb); return sb.toString(); } return text.toString(); }
繼續南下,進入Matcher類的appendReplacement方法,
public Matcher appendReplacement(StringBuffer sb, String replacement) { ...省略部分代碼 // 用於跟蹤replacement字符串的索引 int cursor = 0; String s = replacement;// Java api源碼也有垃圾代碼啊,呵呵 (s局部變量並未在后續代碼中被使用) // 對當前匹配到子字符串替換后的結果字符串 StringBuffer result = new StringBuffer(); // 遍歷replacement字符串 while (cursor < replacement.length()) { char nextChar = replacement.charAt(cursor); if (nextChar == '\\') { // 重點1:當字符為\時,跳過,並獲取其后面的字符,追加到result cursor++; nextChar = replacement.charAt(cursor); result.append(nextChar); cursor++; } else if (nextChar == '$') { // 重點2:當字符為$時,跳過,並獲取其后面的數值,並以此如果$后面第一個不為數字則拋異常, // Skip past $ cursor++; // The first number is always a group int refNum = (int)replacement.charAt(cursor) - '0'; // 此處代碼用於計算$符號后的數值,數值結果賦予refNum ...省略部分代碼 // group(refNum) 用於獲取正則表達式第refNum個分組表示的字符串,不詳說了 if (group(refNum) != null) result.append(group(refNum)); // 追加到result } else { // 當前字符不為\ 或 $ 則直接追加到result result.append(nextChar); cursor++; } } // 將從上一次匹配的子字符串的結尾索引,到當前匹配的第一個字符串索引的字符串追加到sb // lastAppendPosition參數為上一次執行appendReplacement方法最后追加的字符在原始字符串中的索引位置。 // first 參數為當前待替換的子字符串的首個字符在原始字符串中的索引位置 sb.append(getSubSequence(lastAppendPosition, first)); // 將當前配置子字符串替換后的結果字符串追加到sb sb.append(result.toString()); // 更新lastAppendPosition,供下一個匹配執行appendReplacement方法使用 lastAppendPosition = last; /* 到此, sb中追加了當前匹配的子字符串與前一次匹配子字符串中間的字符,以及當前匹配子字符串被替換后的字符串 */ return this; }
分析結束。
總結
1、replaceAll中第二個參數replacement中,\有轉義的作用,$用於獲取分組匹配的當前子字符串
現在想想為什么要引入這個\轉義的功能? 我的猜測是 ----- 因為引入了$符的分組功能,所以為了解決能輸出$字符,故引入\轉義功能
2、有助於理解Java的正則表達式;
3、世界上沒有十全十美的代碼,Java源碼里也有垃圾代碼,呵呵。
提供幾個問題大家可以實踐下:
1、對兩個反斜杠字符串每個字符串都替換成雙斜杠,如何實現?
即String s = "\\\\"; 替換成 String sr = "\\\\\\";
2、將單反斜杠替換成美元符,如何實現?
即String s = "\\"; 替換成 String sr = "$";
3、String s = "Jack is Rose's boyfriends."
使用$分組替換功能 替換成
String sr = "Rose is Jack's girlfriends."