String类replaceAll方法正则替换深入分析

时间:2022-09-20 04:59:23

作者网址:

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."