Java正则表达式API指南

1. 概述

在本教程中,我们将讨论 Java 正则表达式 API,以及如何在 Java 编程语言中使用正则表达式。

在正则表达式的世界中,有许多不同的风格可供选择,例如 grep、Perl、Python、PHP、awk 等等。

这意味着在一种编程语言中可以工作的正则表达式,可能在另一种语言中无法工作。Java 中的正则表达式语法与 Perl 中的语法最为相似。

2. 设置

在 Java 中使用正则表达式,我们不需要任何特殊设置。JDK 包含了一个专门用于正则表达式操作的包,java.util.regex,只需在代码中导入即可。

此外,java.lang.String 类也内置了正则表达式的支持,通常在我们的代码中使用。

3. Java 正则表达式包

java.util.regex 包由三个类组成:PatternMatcherPatternSyntaxException

  • Pattern 对象是已编译的正则表达式。Pattern 类没有公共构造函数。要创建模式,首先必须调用其公共静态方法 compile,该方法返回一个 Pattern 对象。这些方法接受正则表达式作为第一个参数。
  • Matcher 对象解释模式,并针对输入字符串执行匹配操作。它也没有公共构造函数。我们通过在 Pattern 对象上调用 matcher 方法来获取 Matcher 对象,并传递我们想要匹配的文本。
  • PatternSyntaxException 对象是未检查的异常,表示正则表达式模式中的语法错误。

我们将详细探讨这些类,但首先必须了解如何在 Java 中构造正则表达式。

如果我们已经熟悉了其他环境中的正则表达式,可能会发现一些差异,但它们非常小。

4. 简单示例

让我们从最简单的正则表达式使用案例开始。如前所述,当我们将正则表达式应用于字符串时,它可能会匹配零次或多次。

最基本的模式匹配形式是对字符串字面的匹配。例如,如果正则表达式是 foo 且输入字符串是 foo,匹配将成功,因为两个字符串是相同的:

@Test
public void givenText_whenSimpleRegexMatches_thenCorrect() {
    Pattern pattern = Pattern.compile("foo");
    Matcher matcher = pattern.matcher("foo");
    assertTrue(matcher.find());
}

我们首先通过调用其静态方法 compile 并传递要使用的模式,来创建一个 Pattern 对象。

然后我们通过调用 Pattern 对象的 matcher 方法并传递要检查匹配的文本来创建一个 Matcher 对象。

最后,我们调用 Matcher 对象中的 find 方法。

find 方法在输入文本中逐步前进并为每次匹配返回 true,因此我们可以使用它来计算匹配的次数:

@Test
public void givenText_whenSimpleRegexMatchesTwice_thenCorrect() {
    Pattern pattern = Pattern.compile("foo");
    Matcher matcher = pattern.matcher("foofoo");
    int matches = 0;
    while (matcher.find()) {
        matches++;
    }
    assertEquals(matches, 2);
}

由于我们将运行更多测试,可以将找到匹配次数的逻辑抽象为一个名为 runTest 的方法:

public static int runTest(String regex, String text) {
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(text);
    int matches = 0;
    while (matcher.find()) {
        matches++;
    }
    return matches;
}

当我们得到 0 次匹配时,测试应该失败;否则,它应该通过。

5. 元字符

元字符影响模式的匹配方式;它们在一定程度上为搜索模式添加了逻辑。Java API 支持几个元字符,最简单的就是点“.”,它匹配任何字符:

@Test
public void givenText_whenMatchesWithDotMetach_thenCorrect() {
    int matches = runTest(".", "foo");
    assertTrue(matches > 0);
}

让我们考虑前面的示例,其中正则表达式 foo 匹配了文本 foo 以及 foofoo,两次。如果我们在正则表达式中使用点元字符,在第二种情况下我们不会得到两次匹配:

@Test
public void givenRepeatedText_whenMatchesOnceWithDotMetach_thenCorrect() {
    int matches = runTest("foo.", "foofoo");
    assertEquals(matches, 1);
}

注意正则表达式中 foo 后面的点。匹配器匹配每个 foo 之后的文本,因为最后的点部分表示任何字符。所以在找到第一个 foo 后,其余部分被视为任意字符。这就是为什么只有一次匹配的原因。

API 还支持其他元字符 <([{\\^-=$!|]})?*+.>,我们将在本文进一步探讨。

6. 字符类

浏览官方的 Pattern 类规范时,我们会发现支持的正则表达式结构的摘要。在字符类下,我们有大约 6 种结构。

6.1 与或类

我们将其构造为 [abc]。它匹配集合中的任意一个元素:

@Test
public void givenORSet_whenMatchesAny_thenCorrect() {
    int matches = runTest("[abc]", "b");
    assertEquals(matches, 1);
}

如果它们都出现在文本中,将分别匹配每个元素,而不考虑顺序:

@Test
public void givenORSet_whenMatchesAnyAndAll_thenCorrect() {
    int matches = runTest("[abc]", "cab");
    assertEquals(matches, 3);
}

它们也可以作为字符串的一部分进行交替。在下面的示例中,当我们通过将集合中的每个元素与第一个字母交替创建不同的单词时,所有这些都匹配:

@Test
public void givenORSet_whenMatchesAllCombinations_thenCorrect() {
    int matches = runTest("[bcr]at", "bat cat rat");
    assertEquals(matches, 3);
}

6.2 NOR类

上述集合通过在第一个元素位置添加插入符进行否定:

@Test
public void givenNORSet_whenMatchesNon_thenCorrect() {
    int matches = runTest("[^abc]", "g");
 
    assertTrue(matches > 0);
}

这里是另一个例子:

@Test
public void givenNORSet_whenMatchesAllExceptElements_thenCorrect() {
    int matches = runTest("[^bcr]at", "sat mat eat");
 
    assertTrue(matches > 0);
}

6.3  范围

我们可以通过使用连字符(-)定义一个匹配文本应落入的范围。我们也可以对范围进行否定。

匹配大写字母:

@Test
public void givenUpperCaseRange_whenMatchesUpperCase_
  thenCorrect() {
    int matches = runTest(
      "[A-Z]", "Two Uppercase alphabets 34 overall");
 
    assertEquals(matches, 2);
}

匹配小写字母:


@Test
public void givenLowerCaseRange_whenMatchesLowerCase_
  thenCorrect() {
    int matches = runTest(
      "[a-z]", "Two Uppercase alphabets 34 overall");
 
    assertEquals(matches, 26);
}

匹配大写和小写字母:

@Test
public void givenBothLowerAndUpperCaseRange_
  whenMatchesAllLetters_thenCorrect() {
    int matches = runTest(
      "[a-zA-Z]", "Two Uppercase alphabets 34 overall");
 
    assertEquals(matches, 28);
}

匹配给定的数字范围:


@Test
public void givenNumberRange_whenMatchesAccurately_
  thenCorrect() {
    int matches = runTest(
      "[1-5]", "Two Uppercase alphabets 34 overall");
 
    assertEquals(matches, 2);
}

匹配另一个数字范围:

@Test
public void givenNumberRange_whenMatchesAccurately_
  thenCorrect2(){
    int matches = runTest(
      "3[0-5]", "Two Uppercase alphabets 34 overall");
  
    assertEquals(matches, 1);
}

6.4 联合类

联合字符类是通过组合两个或更多字符类产生的结果:

@Test
public void givenTwoSets_whenMatchesUnion_thenCorrect() {
    int matches = runTest("[1-3[7-9]]", "123456789");
 
    assertEquals(matches, 6);
}

上面的测试只会匹配九个整数中的六个,因为联合集跳过了4、5和6。

6.5 交叉点类

类似于联合类,这个类是通过从两个或更多集合中挑选公共元素得到的。要应用交集,我们使用&&:

@Test
public void givenTwoSets_whenMatchesIntersection_thenCorrect() {
    int matches = runTest("[1-6&&[3-9]]", "123456789");
 
    assertEquals(matches, 4);
}

我们会得到四个匹配,因为这两个集合的交集中只有四个元素。

6.6 减法类

我们可以使用减法来否定一个或多个字符类。例如,我们可以匹配一组奇数十进制数:

@Test
public void givenSetWithSubtraction_whenMatchesAccurately_thenCorrect() {
    int matches = runTest("[0-9&&[^2468]]", "123456789");
 
    assertEquals(matches, 5);
}

只有1、3、5、7、9会被匹配。

7. 预定义字符类

Java正则表达式API还接受预定义的字符类。上述的一些字符类可以用更简短的形式表示,虽然这会让代码不那么直观。Java正则表达式的一个特殊方面是转义字符。

正如我们将看到的,大多数字符将以反斜杠开头,这在Java中有特殊的含义。要使这些由Pattern类编译,前导反斜杠必须被转义,即\\d变为\\\\d。

匹配数字,相当于[0-9]:

@Test
public void givenDigits_whenMatches_thenCorrect() {
    int matches = runTest("\\\\d", "123");
 
    assertEquals(matches, 3);
}

匹配非数字,相当于[^0-9]:

@Test
public void givenNonDigits_whenMatches_thenCorrect() {
    int mathces = runTest("\\\\D", "a6c");
 
    assertEquals(matches, 2);
}

匹配空格:

@Test
public void givenWhiteSpace_whenMatches_thenCorrect() {
    int matches = runTest("\\\\s", "a c");
 
    assertEquals(matches, 1);
}

匹配非空格:

@Test
public void givenNonWhiteSpace_whenMatches_thenCorrect() {
    int matches = runTest("\\\\S", "a c");
 
    assertEquals(matches, 2);
}

匹配单词字符,相当于[a-zA-Z_0-9]:


@Test
public void givenWordCharacter_whenMatches_thenCorrect() {
    int matches = runTest("\\\\w", "hi!");
 
    assertEquals(matches, 2);
}

匹配非单词字符:

@Test
public void givenNonWordCharacter_whenMatches_thenCorrect() {
    int matches = runTest("\\\\W", "hi!");
 
    assertEquals(matches, 1);
}

8. 量词

Java 正则表达式 API 也允许我们使用量词。这使我们可以通过指定匹配的出现次数来进一步调整匹配的行为。

要匹配文本零次或一次,我们使用?量词:

@Test
public void givenZeroOrOneQuantifier_whenMatches_thenCorrect() {
    int matches = runTest("\\\\a?", "hi");
    assertEquals(matches, 3);
}

或者,我们可以使用大括号语法,Java 正则表达式 API 也支持这种语法:

@Test
public void givenZeroOrOneQuantifier_whenMatches_thenCorrect2() {
    int matches = runTest("\\\\a{0,1}", "hi");
    assertEquals(matches, 3);
}

这个示例引入了零长度匹配的概念。当量词的匹配阈值为零时,它总是会匹配文本中的所有内容,包括每个输入末尾的空字符串。这意味着即使输入为空,它也会返回一个零长度的匹配。

这解释了为什么我们在上述示例中得到了三次匹配,尽管字符串的长度只有两个。第三次匹配是零长度的空匹配。

要匹配文本零次或无限次,我们使用*量词,它类似于?:


@Test
public void givenZeroOrManyQuantifier_whenMatches_thenCorrect() {
     int matches = runTest("\\\\a*", "hi");
     assertEquals(matches, 3);
}

支持的替代方案:


@Test
public void givenZeroOrManyQuantifier_whenMatches_thenCorrect2() {
    int matches = runTest("\\\\a{0,}", "hi");
    assertEquals(matches, 3);
}

有区别的量词是+,它的匹配阈值为1。如果所需的字符串根本没有出现,则不会有任何匹配,甚至连零长度的字符串也不会匹配:


@Test
public void givenOneOrManyQuantifier_whenMatches_thenCorrect() {
    int matches = runTest("\\\\a+", "hi");
    assertFalse(matches);
}

支持的替代方案:


@Test
public void givenOneOrManyQuantifier_whenMatches_thenCorrect2() {
    int matches = runTest("\\\\a{1,}", "hi");
    assertFalse(matches);
}

与 Perl 和其他语言一样,我们可以使用大括号语法来匹配特定次数的文本:


@Test
public void givenBraceQuantifier_whenMatches_thenCorrect() {
    int matches = runTest("a{3}", "aaaaaa");
    assertEquals(matches, 2);
}

在上述示例中,我们得到了两次匹配,因为只有当a连续出现三次时才会产生匹配。然而,在下一个测试中,我们不会得到匹配,因为文本只连续出现了两次:


@Test
public void givenBraceQuantifier_whenFailsToMatch_thenCorrect() {
    int matches = runTest("a{3}", "aa");
    assertFalse(matches > 0);
}

当我们在大括号中使用范围时,匹配将是贪婪的,从范围的高端开始匹配:


@Test
public void givenBraceQuantifierWithRange_whenMatches_thenCorrect() {
    int matches = runTest("a{2,3}", "aaaa");
    assertEquals(matches, 1);
}

在这里我们指定了至少两次出现,但不超过三次,所以我们得到了一次匹配,匹配器看到的是一个aaa和一个孤立的a,它无法匹配。

然而,API 允许我们指定惰性或非贪婪的方法,使得匹配器可以从范围的低端开始匹配,匹配两个连续的aa:


@Test
public void givenBraceQuantifierWithRange_whenMatchesLazily_thenCorrect() {
    int matches = runTest("a{2,3}?", "aaaa");
    assertEquals(matches, 2);
}

9. 捕获组

API 还允许我们通过捕获组将多个字符视为一个单元。它会为捕获组附加编号,并允许使用这些编号进行反向引用。

在本节中,我们将看到一些如何在 Java 正则表达式 API 中使用捕获组的示例。

让我们使用一个捕获组,仅当输入文本包含两个相邻的数字时才进行匹配:


@Test
public void givenCapturingGroup_whenMatches_thenCorrect() {
    int matches = runTest("(\\\\d\\\\d)", "12");
    assertEquals(matches, 1);
}

上面匹配的编号是1,使用反向引用告诉匹配器我们要匹配文本的另一部分。这样,输入不会有两个单独的匹配:


@Test
public void givenCapturingGroup_whenMatches_thenCorrect2() {
    int matches = runTest("(\\\\d\\\\d)", "1212");
    assertEquals(matches, 2);
}

我们可以获得一次匹配,但通过反向引用将相同的正则表达式匹配扩展到整个输入的长度:


@Test
public void givenCapturingGroup_whenMatchesWithBackReference_thenCorrect() {
    int matches = runTest("(\\\\d\\\\d)\\\\1", "1212");
    assertEquals(matches, 1);
}

我们必须重复正则表达式而不使用反向引用才能实现相同的效果:


@Test
public void givenCapturingGroup_whenMatches_thenCorrect3() {
    int matches = runTest("(\\\\d\\\\d)(\\\\d\\\\d)", "1212");
    assertEquals(matches, 1);
}

同样,对于任何其他数量的重复,反向引用可以使匹配器将输入视为一次匹配:


@Test
public void givenCapturingGroup_whenMatchesWithBackReference_thenCorrect2() {
    int matches = runTest("(\\\\d\\\\d)\\\\1\\\\1\\\\1", "12121212");
    assertEquals(matches, 1);
}

但如果我们更改最后一个数字,匹配将失败:


@Test
public void givenCapturingGroupAndWrongInput_whenMatchFailsWithBackReference_thenCorrect() {
    int matches = runTest("(\\\\d\\\\d)\\\\1", "1213");
    assertFalse(matches > 0);
}

请务必记住转义反斜杠,它们在 Java 语法中至关重要。

10. 边界匹配器

Java 正则表达式 API 也支持边界匹配。如果我们关心匹配应该出现在输入文本的确切位置,那么这就是我们要找的。在前面的示例中,我们关心的只是是否找到了匹配。

要仅在所需正则表达式在文本开头为真时匹配,我们使用插入符号^。

由于文本dog可以在开头找到,这个测试将通过:


@Test
public void givenText_whenMatchesAtBeginning_thenCorrect() {
    int matches = runTest("^dog", "dogs are friendly");
    assertTrue(matches > 0);
}

以下测试将失败:


@Test
public void givenTextAndWrongInput_whenMatchFailsAtBeginning_thenCorrect() {
    int matches = runTest("^dog", "are dogs are friendly?");
    assertFalse(matches > 0);
}

要仅在所需正则表达式在文本末尾为真时匹配,我们使用美元符号$。我们将在以下情况下找到匹配:


@Test
public void givenText_whenMatchesAtEnd_thenCorrect() {
    int matches = runTest("dog$", "Man's best friend is a dog");
    assertTrue(matches > 0);
}

而在这里我们不会找到匹配:


@Test
public void givenTextAndWrongInput_whenMatchFailsAtEnd_thenCorrect() {
    int matches = runTest("dog$", "is a dog man's best friend?");
    assertFalse(matches > 0);
}

如果我们只想在找到所需文本时进行匹配,我们在正则表达式的开头和结尾使用\\b:

空格是一个单词边界:


@Test
public void givenText_whenMatchesAtWordBoundary_thenCorrect() {
    int matches = runTest("\\bdog\\b", "a dog is friendly");
    assertTrue(matches > 0);
}

而这个测试将失败:


@Test
public void givenTextAndWrongInput_whenMatchFailsAtWordBoundary_thenCorrect() {
    int matches = runTest("\\bdog\\b", "snoop dogg is a rapper");
    assertFalse(matches > 0);
}

类似地,我们可以匹配整个文本的边界,而不仅仅是单词的边界:


@Test
public void givenText_whenMatchesAtWordAndTextBoundary_thenCorrect() {
    int matches = runTest("^dog$", "dog");
    assertTrue(matches > 0);
}

11. Pattern 类方法

之前,我们只用基本方式创建了 Pattern 对象。然而,这个类还有一个编译方法的变体,它接受一组标志以及正则表达式参数,这会影响我们匹配模式的方式。

这些标志只是抽象的整数值。让我们重载测试类中的 runTest 方法,使它可以将标志作为第三个参数:

public static int runTest(String regex, String text, int flags) {
    pattern = Pattern.compile(regex, flags);
    matcher = pattern.matcher(text);
    int matches = 0;
    while (matcher.find()){
        matches++;
    }
    return matches;
}

在本节中,我们将介绍不同支持的标志及其用法。

Pattern.CANON_EQ

此标志启用了规范等效性。指定时,只有当两个字符的完整规范分解匹配时,它们才被视为匹配。

考虑带重音符号的 Unicode 字符é。其复合代码点是 u00E9。然而,Unicode 也有其组件字符的独立代码点:eu0065)和重音符号 u0301。在这种情况下,复合字符 u00E9 与字符序列 u0065u0301 无法区分。

默认情况下,匹配不考虑规范等效性:

@Test
public void givenRegexWithoutCanonEq_whenMatchFailsOnEquivalentUnicode_thenCorrect() {
    int matches = runTest("\u00E9", "\u0065\u0301");
    assertFalse(matches > 0);
}

但如果我们添加了标志,则测试将通过:

@Test
public void givenRegexWithCanonEq_whenMatchesOnEquivalentUnicode_thenCorrect() {
    int matches = runTest("\u00E9", "\u0065\u0301", Pattern.CANON_EQ);
    assertTrue(matches > 0);
}

Pattern.CASE_INSENSITIVE

此标志允许忽略大小写进行匹配。默认情况下,匹配时考虑大小写:

@Test
public void givenRegexWithDefaultMatcher_whenMatchFailsOnDifferentCases_thenCorrect() {
    int matches = runTest("dog", "This is a Dog");
    assertFalse(matches > 0);
}

使用此标志后,我们可以改变默认行为:

@Test
public void givenRegexWithCaseInsensitiveMatcher_whenMatchesOnDifferentCases_thenCorrect() {
    int matches = runTest("dog", "This is a Dog", Pattern.CASE_INSENSITIVE);
    assertTrue(matches > 0);
}

我们还可以使用等效的嵌入标志表达式来实现相同的效果:

@Test
public void givenRegexWithEmbeddedCaseInsensitiveMatcher_whenMatchesOnDifferentCases_thenCorrect() {
    int matches = runTest("(?i)dog", "This is a Dog");
    assertTrue(matches > 0);
}

Pattern.COMMENTS

Java API 允许我们使用 # 在正则表达式中包含注释。这有助于为可能对其他程序员不太明显的复杂正则表达式添加文档说明。

注释标志使匹配器忽略正则表达式中的任何空格或注释,并只考虑模式。在默认匹配模式下,以下测试将失败:

@Test
public void givenRegexWithComments_whenMatchFailsWithoutFlag_thenCorrect() {
    int matches = runTest("dog$  #check for word dog at end of text", "This is a dog");
    assertFalse(matches > 0);
}

这是因为匹配器将在输入文本中查找整个正则表达式,包括空格和 # 字符。但是,当我们使用标志时,它会忽略多余的空格,并将 # 开头的所有文本视为注释,忽略每行的注释:

@Test
public void givenRegexWithComments_whenMatchesWithFlag_thenCorrect() {
    int matches = runTest("dog$  #check end of text", "This is a dog", Pattern.COMMENTS);
    assertTrue(matches > 0);
}

还有一个用于此的替代嵌入标志表达式:

@Test
public void givenRegexWithComments_whenMatchesWithEmbeddedFlag_thenCorrect() {
    int matches = runTest("(?x)dog$  #check end of text", "This is a dog");
    assertTrue(matches > 0);
}

Pattern.DOTALL

默认情况下,当我们在正则表达式中使用点“.”表达式时,我们会匹配输入字符串中的每个字符,直到遇到换行符。

使用此标志,匹配将包含行终止符。通过以下示例我们将更好地理解这一点。由于我们希望对匹配的字符串进行断言,我们将使用 matchergroup 方法,它返回上一个匹配项。

首先,让我们看看默认行为:

@Test
public void givenRegexWithLineTerminator_whenMatchFails_thenCorrect() {
    Pattern pattern = Pattern.compile("(.*)");
    Matcher matcher = pattern.matcher("this is a text" + System.getProperty("line.separator") + " continued on another line");
    matcher.find();
    assertEquals("this is a text", matcher.group(1));
}

如我们所见,只有在行终止符之前的输入部分被匹配。

现在在 dotall 模式中,整个文本(包括行终止符)将被匹配:

@Test
public void givenRegexWithLineTerminator_whenMatchesWithDotall_thenCorrect() {
    Pattern pattern = Pattern.compile("(.*)", Pattern.DOTALL);
    Matcher matcher = pattern.matcher("this is a text" + System.getProperty("line.separator") + " continued on another line");
    matcher.find();
    assertEquals("this is a text" + System.getProperty("line.separator") + " continued on another line", matcher.group(1));
}

我们还可以使用嵌入标志表达式启用 dotall 模式:

@Test
public void givenRegexWithLineTerminator_whenMatchesWithEmbeddedDotall_thenCorrect() {
    Pattern pattern = Pattern.compile("(?s)(.*)");
    Matcher matcher = pattern.matcher("this is a text" + System.getProperty("line.separator") + " continued on another line");
    matcher.find();
    assertEquals("this is a text" + System.getProperty("line.separator") + " continued on another line", matcher.group(1));
}

Pattern.LITERAL

在此模式下,匹配器不会给任何元字符、转义字符或正则表达式语法赋予特殊含义。如果没有此标志,匹配器将匹配以下正则表达式与任意输入字符串:

@Test
public void givenRegex_whenMatchesWithoutLiteralFlag_thenCorrect() {
    int matches = runTest("(.*)", "text");
    assertTrue(matches > 0);
}

这是我们在所有示例中看到的默认行为。但是,使用此标志后,我们将找不到匹配,因为匹配器将查找 (.*) 而不是解释它:

@Test
public void givenRegex_whenMatchFailsWithLiteralFlag_thenCorrect() {
    int matches = runTest("(.*)", "text", Pattern.LITERAL);
    assertFalse(matches > 0);
}

现在如果我们添加所需的字符串,测试将通过:

@Test
public void givenRegex_whenMatchesWithLiteralFlag_thenCorrect() {
    int matches = runTest("(.*)", "text(.*)", Pattern.LITERAL);
    assertTrue(matches > 0);
}

没有用于启用文字解析的嵌入标志字符。

Pattern.MULTILINE

默认情况下,^$ 元字符分别绝对匹配整个输入字符串的开始和结束,匹配器忽略任何行终止符:

@Test
public void givenRegex_whenMatchFailsWithoutMultilineFlag_thenCorrect() {
    int matches = runTest("dog$", "This is a dog" + System.getProperty("line.separator") + "this is a fox");
    assertFalse(matches > 0);
}

通过启用此标志,匹配器将匹配每一行,而不仅仅是整个输入文本:

@Test
public void givenRegex_whenMatchesWithMultilineFlag_thenCorrect() {
    int matches = runTest("dog$", "This is a dog" + System.getProperty("line.separator") + "this is a fox", Pattern.MULTILINE);
    assertTrue(matches > 0);
}

我们还可以使用嵌入表达式来启用多行模式:

@Test
public void givenRegex_whenMatchesWithEmbeddedMultilineFlag_thenCorrect() {
    int matches = runTest("(?m)dog$", "This is a dog" + System.getProperty("line.separator") + "this is a fox");
    assertTrue(matches > 0);
}

12. Matcher 类方法

在本节中,我们将学习 Matcher 类中的一些有用方法。为了清晰起见,我们将根据功能对其进行分类。

12.1 索引方法

索引方法提供了有用的索引值,准确显示在输入字符串中找到匹配的位置。在下面的测试中,我们将确认输入字符串中 "dog" 的匹配起始和结束索引:


@Test
public void givenMatch_whenGetsIndices_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher("This dog is mine");
    matcher.find();
    assertEquals(5, matcher.start());
    assertEquals(8, matcher.end());
}

12.2 研究方法

研究方法遍历输入字符串,并返回一个布尔值,指示是否找到了模式。常用的方法有 matches 和 lookingAt。

matches 和 lookingAt 方法都尝试将输入序列与模式进行匹配。区别在于 matches 要求整个输入序列完全匹配,而 lookingAt 不需要。

这两个方法都从输入字符串的开头开始匹配:

@Test
public void whenStudyMethodsWork_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher("dogs are friendly");
    assertTrue(matcher.lookingAt());
    assertFalse(matcher.matches());
}

在这种情况下,matches 方法将返回 true:

@Test
public void whenMatchesStudyMethodWorks_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher("dog");
    assertTrue(matcher.matches());
}

12.3 替换方法

替换方法用于替换输入字符串中的文本。常见的方法有 replaceFirst 和 replaceAll。

replaceFirst 和 replaceAll 方法用于替换与给定正则表达式匹配的文本。顾名思义,replaceFirst 只替换第一次出现的匹配项,replaceAll 替换所有出现的匹配项:

@Test
public void whenReplaceFirstWorks_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher(
      "dogs are domestic animals, dogs are friendly");
    String newStr = matcher.replaceFirst("cat");
    assertEquals(
      "cats are domestic animals, dogs are friendly", newStr);
}

替换所有出现的匹配项:

@Test
public void whenReplaceAllWorks_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher(
      "dogs are domestic animals, dogs are friendly");
    String newStr = matcher.replaceAll("cat");
    assertEquals("cats are domestic animals, cats are friendly", newStr);
}

replaceAll 方法允许我们用相同的替换文本替换所有匹配项。如果我们想基于具体情况替换匹配项,则需要使用一种令牌替换技术。

13. 结论

在本文中,我们学习了如何在 Java 中使用正则表达式。我们还探讨了 java.util.regex 包中的最重要功能。

若你想提升Java技能,可关注我们的Java培训课程。