注:该文是本博主记录学习之用,没有太多详细的讲解,敬请谅解!
一、背景
想必大家在日常项目中对于字符串替换比较常用的方法都是用String类中的replace、replaceAll、replaceFirst等方法,本博主今天刚好遇到这些方法中一个小小的坑,因为平时不太注意,所以今天刚好踩雷了,所以撰写本文以做学习记录之用。
二、示例代码
以上示例代码输出结果
调用 replace、replaceAll、replaceFirst三个方法替换相同内容,由输出的结果可以看出,replace方法虽然成功但是没有替换到字符串,而replaceAll、replaceFirst这两个方法则抛出了异常:java.lang.IllegalArgumentException: Illegal group reference: group index is missing
既然报错了,我们就要找错,所以我们接下来分别看下这三个方法的源码处理:
-
replace方法
我们点进查看String类中replace的源码
从replace方法中我们会看到两个比较重要的点:
第一个是Pattern.compile(target.toString(), Pattern.LITERAL),这里的正则匹配Pattern.LITERAL的意思就是:启用字面值解析模式, 指定此标志后,指定模式的输入字符串就会作为字面值字符序列来对待,输入序列中的元字符或转义序列不具有任何特殊意义(通俗的说就是输入值是什么就是什么)第二个是Matcher.quoteReplacement(replacement.toString()),这个是重点,我们跳到Matcher类查看quoteReplacement方法,这个方法主要是处理特殊符号$和\,为它俩加上转义符号
由此我们可以看得出为什么replace方法可以成功,但是却没有替换值,要想replace成功替换内容需要改成str.replace("?",replacement),不加任何转义符号。 -
replaceAll和replaceFirst方法
为什么这里要一起讲这两个方法,因为这两个方法抛出异常的处理逻辑是一样的,我们首先看下它们的源码,由String类中的replaceAll和replaceFirst方法中跳转到Matcher类中的replaceAll和replaceFirst方法,以下是Matcher类中的方法
从上面两个方法中你可以看到它们都调用了一个appendReplacement方法,正是这个方法抛出了异常,接下来看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;
StringBuilder result = new StringBuilder();
while (cursor < replacement.length()) {
char nextChar = replacement.charAt(cursor);
if (nextChar == '\\') {
cursor++;
if (cursor == replacement.length())
throw new IllegalArgumentException(
"character to be escaped is missing");
nextChar = replacement.charAt(cursor);
result.append(nextChar);
cursor++;
} else if (nextChar == '$') {
// Skip past $
cursor++;
// Throw IAE if this "$" is the last character in replacement
if (cursor == replacement.length())
throw new IllegalArgumentException(
"Illegal group reference: group index is missing");
nextChar = replacement.charAt(cursor);
int refNum = -1;
if (nextChar == '{') {
cursor++;
StringBuilder gsb = new StringBuilder();
while (cursor < replacement.length()) {
nextChar = replacement.charAt(cursor);
if (ASCII.isLower(nextChar) ||
ASCII.isUpper(nextChar) ||
ASCII.isDigit(nextChar)) {
gsb.append(nextChar);
cursor++;
} else {
break;
}
}
if (gsb.length() == 0)
throw new IllegalArgumentException(
"named capturing group has 0 length name");
if (nextChar != '}')
throw new IllegalArgumentException(
"named capturing group is missing trailing '}'");
String gname = gsb.toString();
if (ASCII.isDigit(gname.charAt(0)))
throw new IllegalArgumentException(
"capturing group name {" + gname +
"} starts with digit character");
if (!parentPattern.namedGroups().containsKey(gname))
throw new IllegalArgumentException(
"No group with name {" + gname + "}");
refNum = parentPattern.namedGroups().get(gname);
cursor++;
} else {
// The first number is always a group
refNum = (int)nextChar - '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 (start(refNum) != -1 && end(refNum) != -1)
result.append(text, start(refNum), end(refNum));
} else {
result.append(nextChar);
cursor++;
}
}
// Append the intervening text
sb.append(text, lastAppendPosition, first);
// Append the match substitution
sb.append(result);
lastAppendPosition = last;
return this;
}
通过以上代码分析,可以看到这里面对 $ 符号和 \\ 符号进行了处理。出现异常错误的原因是:如果参数replacement中出现这两个符号,$符号处理会按照 $1的分组模式进行匹配。当编译器发现$后跟的不是整数的时候,就会抛出“Illegal group reference”的异常。 \\ 符号处理如果长度相等则也会抛出异常。
三、最终正常处理结果
既然知道了它抛出异常的原因,所以我们就更好的处理,我们前面也讲到了replace中调用到了Matcher类中quoteReplacement的方法进行转义,所以它能正常替换,因为replace是自动加上这个方法,所以不用做处理,调用replaceAll和replaceFirst方法时如果涉及到这两个特殊符号就要加上quoteReplacement进行转义。最终正常处理(如下图):