【Java】Pattern 与 Matcher 类的常见应用

本文详细介绍了Java中Pattern类和Matcher类在字符串匹配、替换、捕获组、范围匹配等方面的应用,包括常用正则表达式示例和处理特殊字符的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在 Java 编程中,我们经常需要处理字符串的匹配和替换操作。为了便捷地实现这些功能,Java 提供了 Pattern 类和 Matcher 类。Pattern 类用于定义正则表达式模式,而 Matcher 类用于在给定的输入字符串中进行匹配操作。

本文将介绍 Pattern 类和 Matcher 类在实际场景中常见的应用,以及列举一些常见的正则表达式示例。

基本定义

Java 正则表达式通过 java.util.regex 包下的 Pattern 类与 Matcher 类实现

Pattern 类的实例适应正则表达式的编译表示形式,指定为字符串的正则表达式必须首先被编译为此类的实例。然后,可将得到的模式用于创建 Matcher 对象,根据正则表达式,该对象可以与任意字符序列匹配。执行匹配所涉及的所有状态都会保留在匹配器中,因此多个匹配器可以共享同一模式。

因此,典型的调用顺序是:

  1. Pattern.compile() 生成编译后的正则表达式
  2. matcher() 生成匹配器
  3. matches() 执行匹配操作,并记录下匹配结果状态

单独用 Pattern 只能使用 Pattern.matches(String regex,CharSequence input) 这一种最基础最简单的匹配。

这种匹配的模式,只能满足一次性使用,不能重复使用,因此它的效率不高。但是从另一个角度来看,此类的实例是不可变的,可供多个并发线程安全使用。相比而言,Matcher 类的实例在并发下则没那么安全了。

Pattern 类

Pattern 类是 java.util.regex 包中的一个类,它用于定义正则表达式模式。正则表达式是一种强大的文本匹配工具,它可以用于在字符串中查找、替换或提取特定模式的文本。

Pattern 类用于创建一个正则表达式,也可以说创建一个匹配模式。它的构造方法是私有的,不可以直接创建,但可以通过静态方法 Pattern.complie(String regex) 这个简单工厂方法编译一个正则表达式模式。编译后的 Pattern 对象可以用于创建 Matcher 对象,后者用于在输入字符串中执行匹配操作。

Pattern p = Pattern.compile("\\w+");

如果直接使用 matches 静态方法,则对应返回一个 boolean 结果,表示是否匹配上目标表达式:

Pattern.matches("\\d+", "154234");

Matcher 类

Matcher类是Pattern类的内部类,用于在给定的输入字符串中执行匹配操作。Matcher对象可以通过调用Pattern对象的 matcher() 方法来创建,然后可以使用各种方法执行匹配操作。

Matcher m = Pattern.compile("\\w+").matcher("");

得到 Matcher 实例后,可以通过一系列的方法进行操作:

  • find():判断是否有下一组匹配成功的结果
  • lookingAt():起始位置是否匹配成功
  • group():等价于 group(0),返回匹配整个表达式的结果
  • start():等价于 start(0),返回整个结果的起始下标(包含)
  • end():等价于 end(0),返回整个结果的末尾下标(不包含)

下面给出一段实例代码:

@Test
public void testMatchSinglePattern() {
    String s1 = "vnkjslifgoia123aoivcmaiosdjfaio456vmldfjviodf789avoasdmfoi010";
    String s2 = "000vnkjslifgoia123aoivcmaiosdjfaio456vmldfjviodf789avoasdmfoi010";

    Pattern p = Pattern.compile("\\d+");
    Matcher m1 = p.matcher(s1);
    Matcher m2 = p.matcher(s2);

    // 包含字母,全匹配失败
    Assert.assertFalse(m1.matches());
    Assert.assertFalse(m2.matches());

    // 存在数字,有匹配的部分
    Assert.assertTrue(m2.find());
    // 开头部分是否匹配成功
    Assert.assertFalse(m1.lookingAt());
    Assert.assertTrue(m2.lookingAt());

    // 分别取出匹配的部分,每执行一次find()方法,获取一次下一个匹配项的信息
    if (m1.find()) {
        Assert.assertEquals(m1.group(), "123");
        Assert.assertEquals(m1.start(), 12);
        Assert.assertEquals(m1.end(), 15);
    }
    if (m1.find()) {
        Assert.assertEquals(m1.group(), "456");
        Assert.assertEquals(m1.start(), 31);
        Assert.assertEquals(m1.end(), 34);
    }
}

捕获组 group()

当匹配的正则表达式模式通过括号 () 分组创建了多个匹配单元时,成功匹配的结果则成为捕获组。对于一个 Matcher 对象匹配字符串 s 的结果来说, group()group(0) 对应匹配整个表达式,m.group() 的结果与 s.substring(m.start(), m.end()) 的结果是等价的。

通过 groupCount() 可以返回当前捕获组的数量,可以通过 group(1)group(2) 依次取出每个子单元匹配的内容。

start()end() 方法,则对应获取匹配内容的起始下标和末尾下标(与字符串一样,遵循“包头不包尾”原则)。类似的,start(1)end(1)start(2)end(2) 则是获取每个单元捕获组的首尾下标

下面给出一个例子来进行说明:

@Test
public void testMatchMultiPattern() {
    String s = "aaa111AAA bbb222 cccccc33CCCCCC  ";

    // 两组匹配内容,用括号()封装
    Pattern p = Pattern.compile("([a-z]+)(\\d+)([A-Z]+)");
    Matcher m = p.matcher(s);

    if (m.find()) {
        // 匹配模式中有三个()子序列,因此捕获组内count为3
        Assert.assertEquals(m.group(), "aaa111AAA");
        Assert.assertEquals(m.group(0), "aaa111AAA");
        Assert.assertEquals(m.groupCount(), 3);

        Assert.assertEquals(m.group(1), "aaa");
        Assert.assertEquals(m.group(2), "111");
        Assert.assertEquals(m.group(3), "AAA");

        Assert.assertEquals(m.start(), 0);
        Assert.assertEquals(m.start(1), 0);
        Assert.assertEquals(m.start(2), 3);
        Assert.assertEquals(m.start(3), 6);

        Assert.assertEquals(m.end(), 9);
        Assert.assertEquals(m.end(1), 3);
        Assert.assertEquals(m.end(2), 6);
        Assert.assertEquals(m.end(3), 9);
    }

    if (m.find()) {
        Assert.assertEquals(m.group(), "cccccc33CCCCCC");
        Assert.assertEquals(m.group(0), "cccccc33CCCCCC");
        Assert.assertEquals(m.groupCount(), 3);

        Assert.assertEquals(m.group(1), "cccccc");
        Assert.assertEquals(m.group(2), "33");
        Assert.assertEquals(m.group(3), "CCCCCC");

        Assert.assertEquals(m.start(), 17);
        Assert.assertEquals(m.start(1), 17);
        Assert.assertEquals(m.start(2), 23);
        Assert.assertEquals(m.start(3), 25);

        Assert.assertEquals(m.end(), 31);
        Assert.assertEquals(m.end(1), 23);
        Assert.assertEquals(m.end(2), 25);
        Assert.assertEquals(m.end(3), 31);
    }
}

范围匹配 region

region 用于设定查找范围,设定之后之前查找保存在 mathcer 中的结果清空,随后调用 find() 可在指定的下标范围中进行匹配。

@Test
public void testMatcherRegion() {
    String s = "aaa111AAACCCCCC  ";

    Pattern p = Pattern.compile("\\d+");
    Matcher m = p.matcher(s);
    m.region(0, 3);

    Assert.assertFalse(m.find());

    m.region(4, 10);
    m.find();
    Assert.assertEquals(m.group(), "11");
}

重置匹配 reset

顾名思义,reset 重置 matcher 实例中的匹配结果,重置后匹配记录被清空,需要再次调用 find() 方法重新进行匹配。

@Test
public void testMatcherReset() {
    String s = "aaa111AAA bbb222 cccccc33CCCCCC  ";

    // 两组匹配内容,用括号()封装
    Pattern p = Pattern.compile("[a-z]+");
    Matcher m = p.matcher(s);

    if (m.find()) {
        Assert.assertEquals(m.group(), "aaa");
    }
    if (m.find()) {
        Assert.assertEquals(m.group(), "bbb");
    }
    m.reset();
    if (m.find()) {
        Assert.assertEquals(m.group(), "aaa");
    }
}

替换字符串的预处理 quoteReplacement

当我们在模式匹配时通过调试模式查看参数,浏览 Pattern 或者 Mathcer 类的源码时,就会发现,像 \$ 这两个字符在整个机制当中,有特殊的含义。前者起到转义的作用,会与其下一个字符组合表示特殊的含义;后者是捕获组的标识,与其下一个字符(通常是数字)组合表示指定的捕获组。

如果需要识别或者替换的文本中带有上述两个特殊字符,则可能会导致匹配替换后的文本出现错误,抑或是程序运行抛出异常。因此,Mathcer 提供了一个静态方法,可以对待处理的文本进行预处理,以避免这两个字符影响了模式匹配的正常执行。具体源码如下:

/**
 * Returns a literal replacement <code>String</code> for the specified
 * <code>String</code>.
 *
 * This method produces a <code>String</code> that will work
 * as a literal replacement <code>s</code> in the
 * <code>appendReplacement</code> method of the {@link Matcher} class.
 * The <code>String</code> produced will match the sequence of characters
 * in <code>s</code> treated as a literal sequence. Slashes ('\') and
 * dollar signs ('$') will be given no special meaning.
 *
 * @param  s The string to be literalized
 * @return  A literal string replacement
 * @since 1.5
 */
public static String quoteReplacement(String s) {
    if ((s.indexOf('\\') == -1) && (s.indexOf('$') == -1))
        return s;
    StringBuilder sb = new StringBuilder();
    for (int i=0; i<s.length(); i++) {
        char c = s.charAt(i);
        if (c == '\\' || c == '$') {
            sb.append('\\');
        }
        sb.append(c);
    }
    return sb.toString();
}

原理其实很简单,就是在这两组字符前手动添加了 \\ 进行转义(因为 \ 本身也要进行转义,所以要添加两个),告诉程序这两组字符在文本中没有特殊的含义,只是普通的文本。

这里再提供一组测试案例进行比较:

@Test
public void testQuoteReplacement() {
    String s1 = "Title: %s";
    String s2 = "New PT of HK$5.8-Upside hampered by high milk cost";
    
    String s = s1;
    Pattern p = Pattern.compile("%s");
    Matcher m = p.matcher(s1);

    // 抛出异常:java.lang.IndexOutOfBoundsException: No group 5 at java.util.regex.Matcher.start(Matcher.java:375)
    // s = s.replaceFirst(p.pattern(), s2);
    
    // quoteReplacement
    String escapedContent = Matcher.quoteReplacement(s2);
    s = s.replaceFirst(p.pattern(), escapedContent);
    
    Assert.assertEquals(s, "Title: New PT of HK$5.8-Upside hampered by high milk cost");
}

其他常用的字符串操作

字符串分割 split

@Test
public void testStringSplit() {
    String s = "Good morning, ladies and gentleman!";

    // 按空格分割字符串,获取各个单词
    Pattern spacePattern = Pattern.compile("\\s+");

    String[] arr = spacePattern.split(s);
    Assert.assertEquals(arr[0], "Good");
    Assert.assertEquals(arr[1], "morning,");
    Assert.assertEquals(arr[2], "ladies");
    Assert.assertEquals(arr[3], "and");
    Assert.assertEquals(arr[4], "gentleman!");
}

字符串替换 replace

@Test
public void testStringReplacement() {
    String s = "apple-red,banana-yellow,grass-green,sky-blue";

    Pattern p = Pattern.compile("-");
    Assert.assertEquals(p.matcher(s).replaceAll(":"), "apple:red,banana:yellow,grass:green,sky:blue");
    Assert.assertEquals(p.matcher(s).replaceFirst("->"), "apple->red,banana-yellow,grass-green,sky-blue");
}

字符串匹配拼接 append

主要包括 appendReplacement()appendTail() ,两个方法执行的动作,简单来说:

  • appendReplacement() 将本次查找匹配到的字符串及其之前的字符追加到一个 StringBuffer 中去。
  • appendTail() 是将匹配后面剩下的未匹配的字符串追加到一个 StringBuffer 中。
@Test
public void testStringBuilderAppend(){
    Pattern pattern = Pattern.compile("a|b");
    Matcher matcher = pattern.matcher("aiiiibqqqqqafffffff@here.com");

    StringBuffer sb1 = new StringBuffer();
    StringBuffer sb2 = new StringBuffer();

    if (matcher.find()) {
        matcher.appendReplacement(sb1, matcher.group());
        Assert.assertEquals(sb1.toString(), "a");
    }
    if (matcher.find()) {
        matcher.appendReplacement(sb1, matcher.group());
        Assert.assertEquals(sb1.toString(), "aiiiib");
    }
    if (matcher.find()) {
        matcher.appendReplacement(sb1, matcher.group());
        Assert.assertEquals(sb1.toString(), "aiiiibqqqqqa");
    }

    matcher.appendTail(sb2);
    Assert.assertEquals(sb2.toString(), "fffffff@here.com");
}

常用的正则表达式

这里整理了一份常用的正则表达式,以及使用时需要注意的一些事项:
https://blog.csdn.net/sinat_36645384/article/details/133344483

参考文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值