`

诡异的appendReplacement和replaceAll

    博客分类:
  • Java
 
阅读更多

 


一、起源

    这段代码的作用是将字符串中${param}替换为map中的数据

private static String replaceVariantOldVersion(String str, Map<String,String> variantMap){
		Matcher m = Pattern.compile("\\$\\{.*?\\}").matcher(str);
		StringBuffer rtn = new StringBuffer();
		while(m.find()){
			String foundStr = m.group();
			String key = foundStr.substring("${".length(),foundStr.length()-1).trim();
			String value = variantMap.get(key);
			if(null == value){
				value = "{UNKNOWN_VALUE}";
			}
			m.appendReplacement(rtn, value);
		}
		m.appendTail(rtn);
		return rtn.toString();
	}

   例如

 

String str = "my name is ${name}";
Map<String,String> map = new HashMap<String,String>();
map.put("name", "lazy");
System.out.println(replaceVariantOldVersion(str,map));//输出my name is lazy

 

 

但如果是

 

		
String str = "my name is ${name}";
Map<String,String> map = new HashMap<String,String>();
map.put("name", "$.lazy");
System.out.println(replaceVariantOldVersion(str,map));
 

则会抛出异常

 

Exception in thread "main" java.lang.IllegalArgumentException: Illegal group reference

at java.util.regex.Matcher.appendReplacement(Unknown Source)

at lgc.tools.struts2.ConventionUtil.replaceVariantOldVersion(ConventionUtil.java:241)

at lgc.tools.struts2.ConventionUtil.main(ConventionUtil.java:269)

二、 分析

分析下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;
    }
 原来第二个参数replacement可以使用$n来引用分组!所以‘$’和‘\’都被当做特殊字符处理!replacement中的字符串$1代表匹配的分组1!

什么是分组
看程序
Matcher m = Pattern.compile("((\\d\\d)(\\w))").matcher("11a22b");
StringBuffer sb = new StringBuffer();
while(m.find()){
	m.appendReplacement(sb, "$0,$1,$2,$3;");
}
m.appendTail(sb);
System.out.println(sb);
 输出的结果是11a,11a,11,a;22b,22b,22,b;
正则表达式((\d\d)(\w))有三个分组,每个括号包含的内容称作一个分组,并按照左括号出现的顺序给每个分组给予编号1,2,3,...,编号为0的分组代表整个被匹配的字符串。
例子程序中,
“11a”被 ((\d\d)(\w))匹配,$0是整个字符串,等于11a,此时sb是:11a
$1是(\d\d)(\w)撇配的内容,等于11a,此时sb是11a,11a
$2是(\d\d)匹配的内容,等于11,此时sb是11a,11a,11
$3是(\w)匹配的内容,等于a,此时sb是11a,11a,11,a;
相信大家到此应该大致明白分组的意义,如有问题请留言。

三、结论

回到正题,既然 appendReplacement(StringBuffer sb, String replacement)的replacement参数中'$'和'\',那么我就对这两个字符进行转义。

(其中'\'字符是被当做特殊字符处理是因为,java将\$当做普通的$进行处理,所以也'\'被当做了特殊字符。你可以尝试运行
String str = "my name is ${name}";
Map<String,String> map = new HashMap<String,String>();
map.put("name", "\\.lazy");
System.out.println(replaceVariantOldVersion(str,map));
同样会因为特殊字符而报错。
)

最终,我修正后的代码是
private static String replaceVariant(String str, Map<String,String> variantMap){
		Matcher m = Pattern.compile("\\$\\{.*?\\}").matcher(str);
		StringBuffer rtn = new StringBuffer();
		while(m.find()){
			String foundStr = m.group();
			String key = foundStr.substring("${".length(),foundStr.length()-1).trim();
			String value = variantMap.get(key);
			if(null == value){
				value = "{UNKNOWN_VALUE}";
			}
			String valueReplacement = value.replaceAll("\\\\","\\\\\\\\").replaceAll("\\$", "\\\\\\$");
			m.appendReplacement(rtn, valueReplacement);
		}
		m.appendTail(rtn);
		return rtn.toString();
	}
我们重点关注
String valueReplacement = value.replaceAll("\\\\","\\\\\\\\").replaceAll("\\$", "\\\\\\$");

 其实就是将一个 \ 变成 \\ ,一个 $ 变成 \$

可能会有人问,replaceAll的第一个参数是正则表达式,所以需要4个\,为什么第二个参数也需要那么多个\?这也是我开始写代码是遇到的疑惑,实际上,replaceAll最终也是调用我们刚分析过的函数

 public Matcher appendReplacement(StringBuffer sb, String replacement)
replaceAll的第二个参数将会传到appendReplacement的replacement,所以

    replaceAll的第二个参数中的\和$也属于特殊字符串!

这也解释了,我们将文件路径分隔符/替换为\的时候,为什么需要那么多\\\\了。
String s = "E:/mydir/mydir2/mdir3";
//System.out.println(s.replaceAll("/","\\"));报错
System.out.println(s.replaceAll("/","\\\\"));//正确
使用replaceAll的时候,我们需要记住的是,不论是第一个参数还是第二参数,4个\才代表自然字符串中的一个\。在这里我也期待java能推出自然字符串的语法,像python的r"\n"一样。

 

2
4
分享到:
评论
2 楼 lazy_ 2012-09-05  
urfriend 写道
java.util.regex已经自带了解决这个问题的函数

String java.util.regex.Matcher.quoteReplacement(String s)

Returns a literal replacement String for the specified String. This method produces a String that will work as a literal replacement s in the appendReplacement method of the Matcher class. The String produced will match the sequence of characters in s treated as a literal sequence. Slashes ('\') and dollar signs ('$') will be given no special meaning.


谢谢您宝贵的回复!

看了下源代码,跟我的想法一致,就是在$或\前加上一个\
 
public static String quoteReplacement(String s) {
        if ((s.indexOf('\\') == -1) && (s.indexOf('$') == -1))
            return s;
        StringBuffer sb = new StringBuffer();
        for (int i=0; i<s.length(); i++) {
            char c = s.charAt(i);
            if (c == '\\') {
                sb.append('\\'); sb.append('\\');
            } else if (c == '$') {
                sb.append('\\'); sb.append('$');
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
}

1 楼 urfriend 2012-09-04  
java.util.regex已经自带了解决这个问题的函数

String java.util.regex.Matcher.quoteReplacement(String s)

Returns a literal replacement String for the specified String. This method produces a String that will work as a literal replacement s in the appendReplacement method of the Matcher class. The String produced will match the sequence of characters in s treated as a literal sequence. Slashes ('\') and dollar signs ('$') will be given no special meaning.

相关推荐

    String.replaceAll方法详析(正则妙用)

    总结一下,`String.replaceAll` 方法在处理文本时提供了强大功能,结合正则表达式和分组,可以实现精确的文本查找和替换。了解其内部机制,特别是 `Matcher` 类的使用,对于编写高效、准确的代码至关重要。在实际...

    怎么利用Java正则表达式换模板

    如果模板中包含动态内容,可以使用预编译的正则表达式和`Matcher`的`appendReplacement()`和`appendTail()`方法,结合`StringBuilder`进行复杂替换。这种方式适用于替换过程涉及多个步骤或者需要在替换时执行额外...

    正则表达式

    - **替换匹配**:`replaceFirst()`和`replaceAll()`方法分别用于替换找到的第一个和所有匹配的子序列。而`appendReplacement()`和`appendTail()`方法则允许更复杂的替换逻辑,其中`appendReplacement()`会替换找到的...

    java必备-菜鸟的最爱-正则表达式.docx

    如`replaceAll(String replacement)`替换所有匹配的部分,`replaceFirst(String replacement)`仅替换第一次匹配的部分,以及`appendReplacement(StringBuffer sb, String replacement)`和`appendTail(StringBuffer ...

    Java正则表达式的总结和一些小例

    此外,`groupCount()`返回匹配的分组数量,`replaceAll(String replacement)`和`replaceFirst(String replacement)`分别用于替换所有和第一次匹配的部分,`appendReplacement(StringBuffer sb, String replacement)`...

    Java基于正则表达式实现的替换匹配文本功能【经典实例】

    除了`replaceAll()`,我们还可以使用`Matcher`的`find()`方法和`appendReplacement()`方法来逐个处理匹配项。`find()`方法会查找下一个匹配的子串,而`appendReplacement()`方法会将当前匹配前的文本复制到`...

    计算机网络系统

    此外,Matcher还提供了groupCount()、replaceAll()、replaceFirst()、appendReplacement()、appendTail()、group(n)等方法用于捕获组匹配、替换和追加匹配字符串。 文档提到的实例教程部分,通过具体的例子演示了...

    java正则表达式语法

    `Matcher`提供了`replaceAll()`、`replaceFirst()`和`appendReplacement()`方法来进行字符串替换。`replaceAll()`替换所有匹配项,`replaceFirst()`只替换第一个匹配项,`appendReplacement()`用于构建新的字符串。...

    javascript 正则表达式

    `replaceAll()`和`replaceFirst()`是String对象的方法,它们直接对整个字符串进行全局替换或只替换第一个匹配项。 通过熟练掌握这些正则表达式的基础知识和技巧,开发者能够在JavaScript中高效地处理字符串,进行...

    JAVA正则表达式实例教程.doc

    `replaceAll(String replacement)`和`replaceFirst(String replacement)`分别用于替换所有和首次匹配的部分;`appendReplacement(StringBuffer sb, String replacement)`和`appendTail(StringBuffer sb)`用于构建...

    Java正则表达式教程[参照].pdf

    `Matcher`类提供了`find()`、`matches()`、`lookingAt()`用于查找匹配,`replaceFirst(String replacement)`和`replaceAll(String replacement)`用于替换匹配的文本,`appendReplacement(StringBuffer sb, String ...

    JAVA正则表达式实例教程 共17页.docx

    - `replaceAll()`和`replaceFirst()`方法用于替换匹配到的部分。 通过这个教程,读者可以深入理解Java中的正则表达式,并学会如何在实际编程中运用它们进行文本处理。无论是进行数据验证、搜索替换还是其他复杂的...

    JAVA正则表达式实例教程.pdf

    - `appendReplacement(StringBuffer sb, String replacement)`:将替换内容添加到`StringBuffer`。 - `appendTail(StringBuffer sb)`:将匹配后的剩余字符串添加到`StringBuffer`。 - `group(int n)`:获取匹配的...

    java正则表达式详细图解经典案例

    - `appendReplacement(StringBuffer sb, String replacement)`:将替换后的结果添加到StringBuffer。 - `appendTail(StringBuffer sb)`:将输入序列中未匹配部分添加到StringBuffer。 正则表达式中的**通配符**和**...

    Java正则表达式的总结和一些小例子

    * Matcher appendReplacement(StringBuffer sb, String replacement):根据模式用 replacement 替换相应内容,并将匹配的结果添加到 sb 当前位置之后。 * StringBuffer appendTail(StringBuffer sb):将输入序列中...

    java正则表达式替换字符串

    在Java中,`java.util.regex`包提供了处理正则表达式的类,主要包括`Pattern`和`Matcher`。 - **Pattern**:编译后的正则表达式对象,是不可变的。 - **Matcher**:用于执行匹配操作的对象,可以多次复用。 ##### ...

    Java正则多字符串匹配替换

    3. **执行匹配和替换操作**:`Matcher`对象提供了多种方法来处理匹配,如`find()`用于查找匹配项,`appendReplacement()`和`appendTail()`用于替换匹配项,并将结果添加到`StringBuffer`中。例如: ```java ...

Global site tag (gtag.js) - Google Analytics