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 会替换字符串中所有被匹配到的子串。

下面我们来看一个例子(在 System.out.println 之后同一行的注释为其输出,在 System.out.println 一行下面的注释是对输出的说明)。我用这个例子是因为这里面包含了我踩过的一个坑:"\" 在 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 (java.util.regex.PatternSyntaxException ex) {
    System.err.println(ex.getMessage());
    // Unexpected internal error near index 1
    // \
    //  ^
}
try {
    System.out.println("a\\b".replaceAll("\\", "."));
} catch (java.util.regex.PatternSyntaxException ex) {
    System.err.println(ex.getMessage());
    // Unexpected internal error near index 1
    // \
    //  ^
}

 

下面我们来解释 API 文档里的这句话“请注意,替换字符串 replacement 中的反斜杠(\)和美元符号($)可能会导致结果与被视为一般替换字符串时的结果不同”是什么意思。

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”

为了方便大家理解,我把以上代码改一下颜色,用相同的颜色来标识(this 字符串内)被匹配到的子串、(regex 参数)正则表达式里的分组和替换后的字符串(返回值)里对应正则表达式分组的内容:

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

 

如果 replacement 参数中的美元符号($)后面没有序号或者不是数字,那么就会抛 IllegalArgumentException,请看如下代码示例:

final String origin = "a\\b\\c/d/e/f\\g";
try {
    System.out.println(origin.replaceFirst("([a-z])/", "x$1y$"));
    // 最后的“$”后没有序号
} catch(IllegalArgumentException ex) {
    System.err.println(ex.getMessage());
    // Illegal group reference: group index is missing
}
try {
    System.out.println(origin.replaceAll("([a-z])/", "x\\$y$0"));
    // 第一个“$”后面是“y”
} catch(IllegalArgumentException ex) {
    System.err.println(ex.getMessage());
    // Illegal group reference
}

 

replacement 参数中的反斜杠的作用

那么如果我们就是需要把子串替换成含有 "$" 的字符串怎么办呢?这时候就需要 API 文档里提到的另一个特殊字符(转义字符)"\"。请看如下代码示例:

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\x$1y$0d/e/f\g
// 要记得代码里的字符串里的“\\”对应运行时的字符串里的一个“\”
// 由于“$”之前有转义字符标识“\”,所以它会被看作是一个普通的字符而不是代表正则表达式中的组
// 第一个被匹配到的子串是“c/”,它要被替换成“x$1y$0”。
System.out.println(origin.replaceAll("([a-z])/", "x\\$1y\\$0")); // a\b\x$1y$0x$1y$0x$1y$0f\g
// 还可以匹配到第二个子串“d/”,它要被替换成“x$1y$0”。
// 还可以匹配到第三个子串“e/”,它要被替换成“x$1y$0”。

 

由于 replacement 参数中的反斜杠(\)被看作是转义字符的标识,如果我们就是需要把子串替换成含有 "\" 的字符串,那么我们就要用两个反斜杠 "\\":

final String origin = "a\\b\\c/d/e/f\\g"; // a\b\c/d/e/f\g
System.out.println(origin.replaceAll("/", "\\\\")); // a\b\c\d\e\f\g
// 要记得代码里的字符串里的“\\”对应运行时的字符串里的一个“\”

 

如果 replacement 参数中的反斜杠(\)后面没有要被转义的字符,那么就会抛 IllegalArgumentException,请看如下代码示例:

final String origin = "a\\b\\c/d/e/f\\g";
try {
    System.out.println(origin.replaceFirst("/", "\\"));
    // 最后的“\”后没有字符
} catch(IllegalArgumentException ex) {
    System.err.println(ex.getMessage());
    // character to be escaped is missing
}

 

与 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”会被看作一般字符串而不会有特殊的处理
posted @ 2021-11-16 18:35  Firas  阅读(1912)  评论(0编辑  收藏  举报