在java正则表达式中捕获组的行为混淆
在这个答案中,我建议使用
s.replaceFirst("\\.0*$|(\\.\\d*?)0+$", "$1");
但有两个人抱怨结果包含字符串“null”,例如23.null
。 这可以解释为$1
(即group(1)
)为null
,可以通过String.valueOf
转换为字符串“null”。 但是,我总是得到空字符串。 我的测试用例涵盖了它
assertEquals("23", removeTrailingZeros("23.00"));
经过。 确切的行为是否未定义?
当在替换字符串中指定了不捕获任何内容的捕获组( null
)时,参考实现中的Matcher类文档未指定appendReplacement
方法的行为。 虽然group
方法的行为很明确,但appendReplacement
方法中没有提到任何内容。
以下是上述案例的3个实施差异的展品:
- 对于上面的情况,引用实现不会附加任何内容(或者我们可以说附加一个空字符串)。
- 对于上面的情况,GNU Classpath和Android的实现附加
null
。
为简洁起见,省略了一些代码,并用...
表示。
1)Sun / Oracle JDK,OpenJDK(参考实现)
对于参考实现(Sun / Oracle JDK和OpenJDK), appendReplacement
的代码似乎没有从Java 6更改,并且当捕获组没有捕获任何内容时它不会附加任何内容:
} 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 ... // Append group if (start(refNum) != -1 && end(refNum) != -1) result.append(text, start(refNum), end(refNum)); } else {
参考
- JDK6 / 98e143b44620
- jdk8 / 687fd7c7986d
2)GNU Classpath
GNU Classpath是Java类库的完整重新实现,在上面的例子中对appendReplacement
有不同的实现。 在Classpath中,Classpath中java.util.regex
包中的类只是gnu.java.util.regex
中类的包装器。
Matcher.appendReplacement
调用RE.getReplacement
来处理匹配部分的替换:
public Matcher appendReplacement (StringBuffer sb, String replacement) throws IllegalStateException { assertMatchOp(); sb.append(input.subSequence(appendPosition, match.getStartIndex()).toString()); sb.append(RE.getReplacement(replacement, match, RE.REG_REPLACE_USE_BACKSLASHESCAPE)); appendPosition = match.getEndIndex(); return this; }
RE.getReplacement
调用REMatch.substituteInto
来获取捕获组的内容并直接附加其结果:
case '$': int i1 = i + 1; while (i1 < replace.length () && Character.isDigit (replace.charAt (i1))) i1++; sb.append (m.substituteInto (replace.substring (i, i1))); i = i1 - 1; break;
REMatch.substituteInto
追加REMatch.toString(int)
的结果,而不检查捕获组是否捕获了任何东西:
if ((input.charAt (pos) == '$') && (Character.isDigit (input.charAt (pos + 1)))) { // Omitted code parses the group number into val ... if (val < start.length) { output.append (toString (val)); } }
并且当捕获组未捕获时, REMatch.toString(int)
返回null
(不相关的代码已被省略)。
public String toString (int sub) { if ((sub >= start.length) || sub < 0) throw new IndexOutOfBoundsException ("No group " + sub); if (start[sub] == -1) return null; ... }
因此,在GNU Classpath的情况下,当替换字符串中指定了无法捕获任何内容的捕获组时, null
将附加到字符串。
3)Android开源项目 - Java核心库
在Android中, Matcher.appendReplacement
调用私有方法appendEvaluated
,后者又直接将group(int)
的结果附加到替换字符串。
public Matcher appendReplacement(StringBuffer buffer, String replacement) { buffer.append(input.substring(appendPos, start())); appendEvaluated(buffer, replacement); appendPos = end(); return this; } private void appendEvaluated(StringBuffer buffer, String s) { boolean escape = false; boolean dollar = false; for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c == '\\' && !escape) { escape = true; } else if (c == '$' && !escape) { dollar = true; } else if (c >= '0' && c <= '9' && dollar) { buffer.append(group(c - '0')); dollar = false; } else { buffer.append(c); dollar = false; escape = false; } } // This seemingly stupid piece of code reproduces a JDK bug. if (escape) { throw new ArrayIndexOutOfBoundsException(s.length()); } }
由于Matcher.group(int)
为捕获无法捕获的组返回null
, Matcher.appendReplacement
当替换字符串中引用捕获组时, Matcher.appendReplacement
附加null
。
最有可能是2个人抱怨你在Android上运行他们的代码。
仔细看了一下Javadoc后,我得出结论:
-
$1
相当于调用group(1)
, 指定在未捕获组时返回null
。 - 未指定替换表达式中的
nulls
处理。
Javadoc相关部分的措辞总体上令人惊讶地模糊(强调我的):
如上所述,美元符号可被视为对捕获的子序列的引用……
你有两个选择|
or-ed在一起,但只有第二个在( )
之间,因此如果第一个替代匹配,则组1为空。
通常将括号括在所有替代方案周围
在你的情况下,你想要更换
- “xxx.00000”由“xxx”或其他
- “xxx.yyy00”由“xxx.yyy”
最好分两步,因为它更具可读性:
- “xxx.y * 00”由“xxx.y *”然后
- “XXX”。 通过“xxx”
这有点多余,改变了初始的“1”。 到“1”。 所以:
.replaceFirst("(\\.\\d*?)0+$", "$1").replaceFirst("\\.$", "");