Java String 的 replaceFirst 和 replaceAll 详解


首先我们来看 API 文档。

  • replaceFirst

    public String replaceFirst(String regex,
                               String replacement)
    用 给定的 replacement 字符串参数 来替换 被给定的正则表达式(regex 字符串参数)匹配的此字符串的第一个子字符串。

    str.replaceFirst(regex, repl)的结果与以下表达式 的结果完全相同

    Pattern.compile(regex).matcher(str).replaceFirst(repl)

    请注意,替换字符串 replacement 中的反斜杠( \ )和美元符号( $ )可能会导致结果与被视为一般替换字符串时的结果不同; 见Matcher.replaceFirst(java.lang.String) 。 如果需要,使用Matcher.quoteReplacement(java.lang.String)来抑制这些字符的特殊含义。

    参数
    regex - 要匹配此字符串的正则表达式
    replacement - 要替换第一个匹配的字符串
    返回
    被替换后的新String
    抛出
    PatternSyntaxException - 如果正则表达式的语法无效
    从以下版本开始:
    1.4
  • replaceAll

    public String replaceAll(String regex,
                             String replacement)
    用 给定的 replacement 字符串参数 来替换 被给定的正则表达式(regex 字符串参数)匹配的此字符串的每个子字符串。

    str.replaceAll(regex, repl)的结果与以下表达式 的结果完全相同

    Pattern.compile(regex).matcher(str).replaceAll(repl)

    请注意,替换字符串 replacement 中的反斜杠( \ )和美元符号( $ )可能会导致结果与被视为一般替换字符串时的结果不同; 见Matcher.replaceAll 。 如果需要,使用Matcher.quoteReplacement(java.lang.String)来抑制这些字符的特殊含义。

    参数
    regex - 要匹配此字符串的正则表达式
    replacement - 要替换每个匹配的字符串
    返回
    被替换后的新String
    抛出
    PatternSyntaxException - 如果正则表达式的语法无效
    从以下版本开始:
    1.4

由以上 Java API 文档我们可以看到,String.replaceFirst 和 String.replaceAll 都把第一个参数(regex 字符串参数)视为正则表达式。replaceFirst 只替换字符串中第一个被匹配到的子串,而 replaceAll 会替换字符串中所有被匹配到的子串。

下面我们来看一个例子。我用这个例子是因为这里面包含了我踩过的一个坑:“\”在 Java 源码字符串中和在正则表达式中都被用来标识转义字符。

final String origin = "a\\b\\c/d/ef\\g";
System.out.println(origin); // a\b\c/d/ef\g
// \\ 中第一个 \ 是转义字符的标识
System.out.println(origin.replaceFirst("\\\\", ".")); // a.b\c/d/ef\g
// 由 Java 字符串的语法,第一个参数实际上是两个 \ ,又因为 replaceFirst 把
// 第一个参数看成是正则表达式,而 \ 在正则表达式里也是转义字符的标识,所以
// "\\\\" 实际上是只匹配一个 \ 字符的正则表达式。
// 这个结果是把 origin 字符串的第一个“\”替换成了“.”。
System.out.println(origin.replaceAll("\\\\", ".")); // a.b.c/d/ef.g
// 这个结果是把 origin 字符串的所有“\”替换成了“.”。

如果 replaceFirst 或者 replaceAll 的第一个参数正好是"\\"(一个"\"),那么在运行时编译正则表达式的时候就会抛异常:

try {
    System.out.println("a\\b".replaceFirst("\\", "."));
} catch (Exception ex) {
    System.err.println(ex.getMessage());
    // Unexpected internal error near index 1
    // \
    //  ^
}
try {
    System.out.println("a\\b".replaceAll("\\", "."));
} catch (Exception ex) {
    System.err.println(ex.getMessage());
    // Unexpected internal error near index 1
    // \
    //  ^
}

下面我们来解释 API 文档里的这句话”请注意,替换字符串 replacement 中的反斜杠( \ )和美元符号( $ )可能会导致结果与被视为一般替换字符串时的结果不同“是什么意思。在这两个方法里,replacement 参数中的美元符号( $ )加一个序号代表正则表达式匹配结果中的第几个组(表达式在匹配时,表达式引擎会将小括号 "()" 包含的表达式所匹配到的字符串记录下来。在获取匹配结果的时候,小括号包含的表达式所匹配到的字符串可以单独获取。)我们用一些例子来说明:

final String origin = "a\\b\\c/d/e/f\\g"; // a\b\c/d/e/f\g
System.out.println(origin.replaceFirst("([a-z])/([a-z])", "x$1y$2")); // a\bxcyd/e/f\g
// 这个正则表达式匹配被“/”分隔的两个英文字母,
// origin 字符串中被匹配到的第一个子串是“c/d”,
// 其中第 1 组是第一个英文字母“c”,第 2 组是第二个英文字母“d”
// 代入第二个参数“x$1y$2”之后,就是要把“c/d”替换为“xcyd”
System.out.println(origin.replaceAll("([a-z])/([a-z])", "x$1y$2")); // a\bxcyd/xeyf\g
// origin 字符串中还可以被匹配到第二个子串“e/f”,
// 其中第 1 组是第一个英文字母“e”,第 2 组是第二个英文字母“f”
// 代入第二个参数“x$1y$2”之后,就是要把“e/f”替换为“xeyf”

为了方便大家理解,我把以上代码改一下颜色:

final String origin = "a\\b\\c/d/e/f\\g"; // a\b\c/d/e/f\g
System.out.println(origin.replaceFirst("([a-z])/([a-z])", "x$1y$2")); // a\bxcyd/e/f\g
System.out.println(origin.replaceAll  ("([a-z])/([a-z])", "x$1y$2")); // a\bxcyd/xeyf\g

在正则表达式里,第 0 组代表被匹配到的整个子串。我们可以用如下例子来看看“$0“在 replaceFirst 或 replaceAll 中的效果:

final String origin = "a\\b\\c/d/e/f\\g"; // a\b\c/d/e/f\g
System.out.println(origin.replaceFirst("([a-z])/", "x$1y$0")); // a\b\xcyc/d/e/f\g
// 这个正则表达式匹配一个小写英文字母及紧随其后的一个“/”。
// 第一个被匹配到的子串是“c/”,它要被替换成“xcyc/”。
System.out.println(origin.replaceAll("([a-z])/", "x$1y$0")); // a\b\xcyc/xdyd/xeye/f\g
// 还可以匹配到第二个子串“d/”,它要被替换成“xdyd/”。
// 还可以匹配到第三个子串“e/”,它要被替换成“xeye/”。

为了方便大家理解,我把以上代码改一下颜色:

final String origin = "a\\b\\c/d/e/f\\g"; // a\b\c/d/e/f\g
System.out.println(origin.replaceFirst("([a-z])/", "x$1y$2")); // a\bxcyc/d/e/f\g
System.out.println(origin.replaceAll  ("([a-z])/", "x$1y$2")); // a\bxcyc/xdyd/xeye/f\g

与 String.replace 的比较

对 Java 的 String API 不熟悉的人可能会将这两个方法与 String.replace 方法混淆,因为这 3 个方法都可以接受两个字符串参数。而从以下 Java API 文档中我们知道,String.replace 方法是将第一个参数看成一般文本不是正则表达式

  • replace

    public String replace(CharSequence target,
                          CharSequence replacement)
    将与字面目标序列匹配的字符串的每个子字符串替换为指定的字面替换序列。 替换从字符串开始到结束,例如,在字符串“aaa”中用“b”替换“aa”将导致“ba”而不是“ab”。
    参数
    target - 要替换的char值序列
    replacement - char值的替换顺序
    返回
    被替换后的新String
    从以下版本开始:
    1.5

其次,String.replace 方法和 String.replaceAll 方法一样,会替换所有被匹配到的子串。我们再用一个代码的实例来说明:

final String origin = "a\\b\\c/d/e/f\\g"; // a\b\c/d/e/f\g
System.out.println(origin.replaceFirst("\\\\", "x$0y")); // ax\yb\c/d/e/f\g
System.out.println(origin.replaceAll("\\\\", "x$0y")); // ax\ybx\ycd/e/fx\yg
System.out.println(origin.replace("\\\\", "x$0y")); // a\b\c/d/e/f\g
// origin 字符串中没有连续的两个“\”
System.out.println(origin.replace("\\", "x$0y")); // ax$0ybx$0yc/d/e/fx$0yg
// origin 字符串中的所有“/”都会被替换而不只是第一个
// 第二个参数里的“$0”会被看作一般字符串而不会有特殊的处理