String类 replaceAll方法 及Matcher类 appendReplacement方法处理$ \特殊字符 详解

时间:2021-09-21 19:54:46

1、先原看看String类的replace方法的原码如下:

public String replaceAll(String regex, String replacement) {

return Pattern.compile(regex).matcher(this).replaceAll(replacement);
    }

 可以看出String类的replaceAll方法实际上是调用了Matcher类的replaceAll方法,查看Matcher类,replaceAll方法原码如下:

public String replaceAll(String replacement) {
        reset();
        boolean result = find();
        if (result) {
            StringBuffer sb = new StringBuffer();
            do {
                appendReplacement(sb, replacement);
                result = find();
            } while (result);
            appendTail(sb);
            return sb.toString();
        }
        return text.toString();
    }

可以看出Matcher类的replaceAll方法调用了Mathcer类下面的reset、appendReplacement、appendTail 方法。

下面一个跟踪Matcher类下的这些方法。

以String str="testtest".replaceAll("test",“love”);为例子进行跟踪。

String类的replaceAll方法的两个形式参数regex="test",replacement="love";

进入String类的replaceAll方法内部有

Pattern.compile("test").matcher(this).replaceAll("love");// this等价于str="testtest"

String类的replaceAll方法内部等价于下面代码

         Pattern pattern=Pattern.compile("test");

         Matcher mathcer=pattern.matcher("testtest");

         String reslut=mathcer.replaceAll("love");

进入Macher类的replaceAll方法

Matcher类,replaceAll方法原码如下:

public String replaceAll(String replacement) {
        reset();
        boolean result = find();
        if (result) {
            StringBuffer sb = new StringBuffer();
            do {
                appendReplacement(sb, replacement);
                result = find();
            } while (result);
            appendTail(sb);
            return sb.toString();
        }
        return text.toString();
    }

便有 Matcher类,replaceAll方法的形式参数replacement="love"

进入replaceAll方法内部,

reset方法的作用是使Matcher类的对象(可以理解为待查找字符)按Pattern类对象(可以理解目标字符串)从头开始匹配(可以理解为将数组下标i重置使i=0)

find方法的作用是找到待查找字符中以目标字符串目标字符串配置的字串,找到返回true,如果没有找到返回false

第一次调用find方法时从待查找字符中下标为0的位置开始查找,找到后调用Matcher类的start方法将返回目标字符串在待查找字符串的下标值,调用Matcher类的end方法将返回下次一调用find方法的进行匹配的初始位置,find方法会从从这个位置开始,查找字串中是否有和目标字符串匹配的字符串,如果没有匹配串却调用start或end或group方法将抛出java.lang.IllegalStateException: No match available

group方法为返回匹配的字符串

举个例子,假设

      //此时"test12"是目标字符串,目标字符串一般设置为正则表达式

      Pattern p=Pttern.compile("test12");

     //"test12mtest12"为待查找字符串

      Matcher m=p.matcher("test12mtest12");

     //使用find方法在待查找字符串"test12mtest12"查找到目标字符串"test12"

     System.out.println( m.find());//结果为true

     System.out.println(m.group());

     //第一次查目标字符串"test12"找到的开始位置

     System.out.println(m.strat());//结果为0

    //第一次查目标字符串"test12"找到的结束位置

     System.out.println(m.end());//结果为6

    //再次调用m.find()方法默认会在字符’m‘位置进行查找,

    System.out.println( m.find());//结果为true

     System.out.println(m.group());

     //第二次查目标字符串"test12"找到的开始位置

     System.out.println(m.strat());//结果为7

    //第二次查目标字符串"test12"找到的结束位置

     System.out.println(m.end());//结果为13

    //重置查找位置,使查找重新从下标为0的位置开始

     m.reset();

    System.out.println( m.find());//结果为true

     System.out.println(m.group());

     //第三次查目标字符串"test12"找到的开始位置

     System.out.println(m.strat());//结果为0

    //第三次查目标字符串"test12"找到的结束位置

     System.out.println(m.end());//结果为6


好了返回Matcher类,replaceAll方法中

实际上是matcer.rest()而matcher=Pattern.compile("test").matcher("testtest");

即重置待查找串”testtest“的查找下标,目标字符串为"test"

boolean reslut=matcher.find();//为true

往下创建一个StringBuffer类的局部变量对象sb,

往下调用appendReplacement(sb,replacement)

查看Macher类的appendReplacement方法的源码如下 

public Matcher appendReplacement(StringBuffer sb, String replacement) {


        // If no match, return error
        if (first < 0)
            throw new IllegalStateException("No match available");


        // Process substitution string to replace group references with groups
        int cursor = 0;
        String s = replacement;

        StringBuffer result = new StringBuffer();


        while (cursor < replacement.length()) {
            char nextChar = replacement.charAt(cursor);
            if (nextChar == '\\') {
                cursor++;
                nextChar = replacement.charAt(cursor);
                result.append(nextChar);
                cursor++;
            } else if (nextChar == '$') {
                // Skip past $
                cursor++;


                // The first number is always a group
                int refNum = (int)replacement.charAt(cursor) - '0';
                if ((refNum < 0)||(refNum > 9))
                    throw new IllegalArgumentException(
                        "Illegal group reference");
                cursor++;


                // Capture the largest legal group string
                boolean done = false;
                while (!done) {
                    if (cursor >= replacement.length()) {
                        break;
                    }
                    int nextDigit = replacement.charAt(cursor) - '0';
                    if ((nextDigit < 0)||(nextDigit > 9)) { // not a number
                        break;
                    }
                    int newRefNum = (refNum * 10) + nextDigit;
                    if (groupCount() < newRefNum) {
                        done = true;
                    } else {
                        refNum = newRefNum;
                        cursor++;
                    }
                }


                // Append group
                if (group(refNum) != null)
                    result.append(group(refNum));
            } else {
                result.append(nextChar);
                cursor++;
            }
        }

        // Append the intervening text
        sb.append(getSubSequence(lastAppendPosition, first));
        // Append the match substitution
        sb.append(result.toString());


        lastAppendPosition = last;
return this;
  }

有些复杂,为便于理解先拿一般的例子来试一下appendReplacement方法的作用,

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {
public static void main(String[] args)throws Exception{
//生成Pattern对象并且编译一个简单的正则表达式"test",目标字符串
Pattern p = Pattern.compile("test"); 
//用Pattern类的matcher()方法生成一个Matcher对象 ,待查找字符串
Matcher m = p.matcher("java test  String replaceAll test  Matcher appendReplace  "); 
StringBuffer sb = new StringBuffer(); 
int i=0; 
//使用find()方法在待查找字符串中,查找目标字符串 
boolean result = m.find(); 
//使用循环将句子里所有的test找出并替换再将内容加到sb里 
while(result) { 
i++; 
m.appendReplacement(sb, "love"); 
System.out.println("第"+i+"次匹配后sb的内容是:"+sb); 
//继续查找下一个匹配对象 
result = m.find(); 

//最后调用appendTail()方法将最后一次匹配后的剩余字符串加到sb里; 
m.appendTail(sb); 
System.out.println("调用m.appendTail(sb)后sb的最终内容是:"+ sb.toString()); 
}
}

结果为如下

第1次匹配后sb的内容是:java love
第2次匹配后sb的内容是:java love  String replaceAll love
第3次匹配后sb的内容是:java love  String replaceAll love  Matcher appendReplace 
调用m.appendTail(sb)后sb的最终内容是:java love  String replaceAll love  Matcher appendReplace 

从上面可以看出当 Matcher类appendReplacement(StringBuffer sb, String replacement)方法的replacement="test"即第二个参数为一般字符串时,appendReplacement方法的作用是将当前匹配子串替换为指定字符串,并且将替换后的子串以及其之前到上次匹配子串之后的字符串段添加到一个StringBuffer对象里,而appendTail(StringBuffer sb) 方法则将最后一次匹配工作后剩余的字符串添加到一个StringBuffer对象里。 

再来看看Macher类的appendReplacement方法的源码

public Matcher appendReplacement(StringBuffer sb, String replacement) {


        // If no match, return error
        if (first < 0)
            throw new IllegalStateException("No match available");


        // Process substitution string to replace group references with groups

       // 用于跟踪replacement字符串的索引
        int cursor = 0;
        String s = replacement;//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
                if ((refNum < 0)||(refNum > 9))
                    throw new IllegalArgumentException(
                        "Illegal group reference");
                cursor++;


                // Capture the largest legal group string
                boolean done = false;
                while (!done) {
                    if (cursor >= replacement.length()) {
                        break;
                    }
                    int nextDigit = replacement.charAt(cursor) - '0';
                    if ((nextDigit < 0)||(nextDigit > 9)) { // not a number
                        break;
                    }
                    int newRefNum = (refNum * 10) + nextDigit;
                    if (groupCount() < newRefNum) {
                        done = true;
                    } else {
                        refNum = newRefNum;
                        cursor++;
                    }
                }


                // Append group 用于获取正则表达式第refNum个分组表示的字符串,不详说了
                if (group(refNum) != null)
                    result.append(group(refNum)); // 追加到result
            } else {

               //当replacement中没有’$‘和’\\‘两类特殊字符时直接加到reslut后面,即我们上面RegexTest例子的情况
                result.append(nextChar);
                cursor++;
            }
        }

         // 将从上一次匹配的子字符串的结尾索引,到当前匹配的第一个字符串索引的字符串追加到sb
         // lastAppendPosition参数为上一次执行appendReplacement方法最后追加的字符在原始字符串中的索引位置。
          // first 参数为当前待替换的子字符串的首个字符在原始字符串中的索引位置

        // Append the intervening text
        sb.append(getSubSequence(lastAppendPosition, first));
        // Append the match substitution

        // 将当前配置子字符串替换后的结果字符串追加到sb
        sb.append(result.toString());
       // 更新lastAppendPosition,供下一个匹配执行appendReplacement方法使用
        lastAppendPosition = last;

      /*到此 sb中追加了当前匹配的子字符串与前一次匹配子字符串中间的字符,以及当前匹配子字符串被替换后的字符串*/
return this;
  }

2、来看一个String类replaceAll方法特殊出字符的例子

对单个反斜杠字符串替换成双斜杠的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.首先字符串“\\\\\\\\”被转义后实际代表的其实是字符串\\\\; 

/* 
 * 字符串"$ \"中的$与\字符互换位置 
 */  
public class SpecialCharReplace {  
    public static void main(String[] args) {  
        String str = "$ \\";  
        /* 
         * string.replaceAll()中的特殊字符 $ 与 \  
         *  
         * 由于 $ 字符在作为替换内容时,是一个特殊字符,指反向引用前面的分组内容,所以把 
         * 某字符替换成 $ 字符时,因该在前面加上转义字符 \。 
         * \ 字符就不用说了,本身就是转义字符,但为什么在作为替换内容时要使用四个 \ 字符 
         * ,这里又不是用在正则表达式里?这就是因为 \ 字符在作为替换内容里也是一个特殊字 
         * 符,它用来将前面讲的 $ 字符进行转换的,所以也为特殊字符。以下是replaceAll的 
         * 源码片断,从源码就可以看出 \$ 是两个特殊字符 
         *  
         * if (nextChar == '\\') { 
         *      cursor++; 
         *      nextChar = replacement.charAt(cursor); 
         *      result.append(nextChar); 
         *      cursor++; 
         * } else if (nextChar == '$') { 
         *      // Skip past $ 
         *      cursor++; 
         *      ... 
         * }else { 
         *      result.append(nextChar); 
         *      cursor++; 
         * } 
         */  
        System.out.println(str.replaceAll("\\$(\\W)\\\\", "\\\\$1\\$"));// \ $  
    }  
  
}   
Matcher对象的appendReplacement典型应用与特殊字符&\的进一步分析


问题的提出 
字符串模板: 
    String template="尊敬的客户${customerName}你好!本次消费金额${amount},您帐户${accountNumber}上的余额为${balance},欢迎下次光临!"; 
其中以 ${ 开始 } 结尾的为待替换的变量域。 
数据存放于Map中,key为域名,value为域值。如: 
Map-- 
    customerName = 刘明 
    accountNumber = 888888888
    balance = $1000000.00
    amount = $1000.00 
请编写函数: 
    public static String composeMessage(String template, Map data) throw Exception 
实现将任意模板字符串中的变量域,按域名替换为data中的域值。 
例如,上例替换结果为: 
    "尊敬的客户刘明你好!本次消费金额$1000.00,您帐户888888888上的余额为$1000000.00,欢迎下次光临!" 
注:如果Map中找不到域值,以空字符串""替换。 


问题的解决
Java代码  收藏代码
public class RegexExam {  
    public static void main(String args[]) {  
        HashMap data = new HashMap();  
        String template = "尊敬的客户${customerName}你好!本次消费金额${amount},"  
                + "您帐户${accountNumber}上的余额为${balance},欢迎下次光临!";  
        data.put("customerName", "刘明");  
        data.put("accountNumber", "888888888");  
        data.put("balance", "$1000000.00");  
        data.put("amount", "$1000.00");  
        try {  
            System.out.println(composeMessage(template, data));  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
  
    public static String composeMessage(String template, Map data)  
            throws Exception {  
        String regex = "\\$\\{(.+?)\\}";  
        Pattern pattern = Pattern.compile(regex);  
        Matcher matcher = pattern.matcher(template);  
        /* 
         * sb用来存储替换过的内容,它会把多次处理过的字符串按源字符串序 
         * 存储起来。 
         */  
        StringBuffer sb = new StringBuffer();  
        while (matcher.find()) {  
            String name = matcher.group(1);//键名  
            String value = (String) data.get(name);//键值  
            if (value == null) {  
                value = "";  
            } else {  
                /* 
                 * 由于$出现在replacement中时,表示对捕获组的反向引用,所以要对上面替换内容 
                 * 中的 $ 进行替换,让它们变成 "\$1000.00" 或 "\$1000000000.00" ,这样 
                 * 在下面使用 matcher.appendReplacement(sb, value) 进行替换时就不会把 
                 * $1 看成是对组的反向引用了,否则会使用子匹配项值amount 或 balance替换 $1 
                 * ,最后会得到错误结果: 
                 * 
                 * 尊敬的客户刘明你好!本次消费金额amount000.00,您帐户888888888上的余额 
                 * 为balance000000.00,欢迎下次光临! 
                 * 
                 * 要把 $ 替换成 \$ ,则要使用 \\\\\\& 来替换,因为一个 \ 要使用 \\\ 来进 
                 * 行替换,而一个 $ 要使用 \\$ 来进行替换,因 \ 与  $ 在作为替换内容时都属于 
                 * 特殊字符:$ 字符表示反向引用组,而 \ 字符又是用来转义 $ 字符的。 
                 */  
                value = value.replaceAll("\\$", "\\\\\\$");  
                //System.out.println("value=" + value);  
            }  
            /* 
             * 经过上面的替换操作,现在的 value 中含有 $ 特殊字符的内容被换成了"\$1000.00" 
             * 或 "\$1000000000.00" 了,最后得到下正确的结果: 
             * 
             * 尊敬的客户刘明你好!本次消费金额$1000.00,您帐户888888888上的 
             * 余额为$1000000.00,欢迎下次光临! 
             * 
             * 另外,我们在这里使用Matcher对象的appendReplacement()方法来进行替换操作,而 
             * 不是使用String对象的replaceAll()或replaceFirst()方法来进行替换操作,因为 
             * 它们都能只能进行一次性简单的替换操作,而且只能替换成一样的内容,而这里则是要求每 
             * 一个匹配式的替换值都不同,所以就只能在循环里使用appendReplacement方式来进行逐 
             * 个替换了。 
             */  
            matcher.appendReplacement(sb, value);  
            System.out.println("sb = " + sb.toString());  
        }  
        //最后还得要把尾串接到已替换的内容后面去,这里尾串为“,欢迎下次光临!”  
        matcher.appendTail(sb);  
        return sb.toString();  
    }  
}