java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java正则表达式

Java中正则表达式Pattern和Matcher的使用终极指南

作者:tianyu234

Java正则表达式是文本处理的强大工具,本文从底层实现讲起,详解Pattern和Matcher的用法,带你彻底搞懂matches与find的区别、分组提取、替换技巧,以及如何缓存Pattern避免性能问题,掌握这些,你也能轻松驾驭Java正则表达式

正则表达式是文本处理的瑞士 军刀,用于匹配、查找、替换、校验字符串。但在 Java 中,它有一个让无数新手抓狂的特点——双重转义

别再硬背正则语法了!这篇文章从 Java 底层实现讲起,彻底搞懂匹配、提取、替换和性能优化

正则表达式是处理字符串的“核武器”。但很多 Java 开发者对它的认知停留在“复制粘贴网上现成的表达式”,一旦遇到 \ 转义、Pattern.compile 报错、matches() 返回 false 就一脸懵。

今天这篇文章,我们从 Java 正则引擎的底层实现(Pattern + Matcher)双重转义的根源性能优化(Pattern 缓存)分组提取与替换 四个维度,彻底把 Java 正则讲透。

一、Java 正则核心类(一张表看懂)

所属包核心职责是否线程安全关键方法
Patternjava.util.regex编译正则表达式(将字符串转为内部状态机)✅ 线程安全(不可变)compile(String regex)、matcher(CharSequence input)、split(CharSequence input)
Matcherjava.util.regex执行匹配操作(维护匹配状态和位置)❌ 线程不安全(有状态)matches()、find()、group()、replaceAll()
MatchResultjava.util.regex匹配结果的只读视图(接口)——group()、start()、end()

核心流程String(正则文本) → Pattern.compile()(编译) → Pattern.matcher()(创建匹配器) → Matcher 方法(执行匹配)。

二、基础语法速查(Java 版)

类别语法含义
字符类[abc]a、b 或 c(任意一个)
[^abc]除 a、b、c 外的任意字符
[a-zA-Z]a-z 或 A-Z 的任意字母
[0-9]任意数字(等价于 \\d
预定义字符类\\d数字([0-9]
\\D非数字([^0-9]
\\w单词字符([a-zA-Z_0-9]
\\W非单词字符
\\s空白字符(空格、制表符、换行等)
\\S非空白字符
.任意字符(除换行符外,默认模式)
数量词*0 次或多次(贪婪)
+1 次或多次(贪婪)
?0 次或 1 次(贪婪)
{n}恰好 n 次
{n,}至少 n 次
{n,m}n 到 m 次
边界匹配^行的开头
$行的结尾
\\b单词边界
\\B非单词边界
分组与捕获(exp)捕获分组,编号从 1 开始
(?:exp)非捕获分组(不保存,性能更好)
(?<name>exp)命名分组(Java 7+)
逻辑``
()改变优先级
转义\\反斜杠转义(Java 中写两个 \\

特别注意:在 Java 字符串中,\ 需要用 \\ 表示。比如匹配数字,正则写 \d,但在 Java 代码中要写成 "\\d"

三、Java 中的“双重转义”问题(新手最大痛点)

这是几乎所有 Java 初学者都会踩的坑。我们来看一个例子:

需求:匹配一个反斜杠字符 \

// 正则表达式本身应该是:\\
// 但在 Java 字符串中,\ 需要转义,所以每个 \ 都要变成 \\
// 结果:String regex = "\\\\";

String regex = "\\\\";  // 匹配一个反斜杠字符
String text = "\\";     // 字符串内容是一个反斜杠
System.out.println(text.matches(regex));  // true

图解(为什么是 4 个反斜杠?)

┌─────────────────────────────────────────────────────────────────┐
│  Java 字符串中的 "\\\\"                                         │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │  第1个 \  → 转义第2个 \  → 产生一个普通的 \              │ │
│  │  第3个 \  → 转义第4个 \  → 再产生一个普通的 \            │ │
│  │  最终:两个普通的 \  → 正则引擎识别为 "匹配一个 \ 字符"  │ │
│  └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

常见转义对照表

想要匹配的字符正则表达式写法Java 字符串中写法
数字\d"\\d"
点号(.\."\\."
反斜杠(\\\"\\\\"
双引号("\""\\\""
换行符\n"\\n"
制表符\t"\\t"

记忆口诀Java 字符串里写正则,反斜杠统统要翻倍

四、matches()vsfind():完全匹配 vs 部分匹配

这是面试中被问得最多的一对方法。

方法匹配模式通俗解释常见用途
matches()完全匹配正则必须匹配整个字符串(从头到尾)表单校验(手机号、邮箱)
find()部分匹配(子串查找)在字符串中查找符合正则的子串日志提取、关键词检索
import java.util.regex.*;

String text = "我的手机号是 13812345678,请保存。";
Pattern p = Pattern.compile("\\d{11}");

// ❌ matches():要求整个字符串都是 11 位数字,返回 false
System.out.println(p.matcher(text).matches());  // false

// ✅ find():查找子串,找到 13812345678,返回 true
Matcher m = p.matcher(text);
System.out.println(m.find());  // true
System.out.println(m.group()); // 13812345678

黄金法则

五、分组提取:group()的妙用(爬虫必备)

import java.util.regex.*;

String text = "姓名:张三,年龄:25,姓名:李四,年龄:30";
Pattern p = Pattern.compile("姓名:([\\u4e00-\\u9fa5]+),年龄:(\\d+)");
Matcher m = p.matcher(text);

while (m.find()) {
    String name = m.group(1);   // 第1个括号捕获的内容
    String age = m.group(2);    // 第2个括号捕获的内容
    System.out.println("姓名:" + name + ",年龄:" + age);
}
// 输出:
// 姓名:张三,年龄:25
// 姓名:李四,年龄:30

分组编号规则

Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = p.matcher("2026-08-18");
if (m.matches()) {
    System.out.println(m.group(0));  // 2026-08-18(整个匹配)
    System.out.println(m.group(1));  // 2026
    System.out.println(m.group(2));  // 08
    System.out.println(m.group(3));  // 18
}

命名分组(Java 7+,可读性更好)

Pattern p = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher m = p.matcher("2026-08-18");
if (m.matches()) {
    System.out.println(m.group("year"));   // 2026
    System.out.println(m.group("month"));  // 08
    System.out.println(m.group("day"));    // 18
}

六、替换:replaceAll()与appendReplacement()

1. 简单替换(最常用)

String text = "我的电话是 13812345678,备用电话 13987654321";
String result = text.replaceAll("\\d{11}", "****");
System.out.println(result);  // 我的电话是 ****,备用电话 ****

2. 使用分组反向引用($1、$2)

// 将日期格式从 yyyy-MM-dd 转为 dd/MM/yyyy
String text = "今天是 2026-08-18,明天是 2026-08-19";
String result = text.replaceAll("(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1");
System.out.println(result);
// 今天是 18/08/2026,明天是 19/08/2026

3. 高级替换:appendReplacement()(流式处理,内存友好)

适合分批处理长文本,而不是一次性 replaceAll

Pattern p = Pattern.compile("\\d{11}");
Matcher m = p.matcher("我的电话是 13812345678,备用电话 13987654321");
StringBuffer sb = new StringBuffer();

while (m.find()) {
    // 对每个匹配到的手机号,用脱敏后的内容替换
    String masked = m.group().substring(0, 3) + "****" + m.group().substring(7);
    m.appendReplacement(sb, masked);
}
m.appendTail(sb);  // 追加剩余未匹配的尾部

System.out.println(sb.toString());  // 我的电话是 138****5678,备用电话 139****4321

七、性能优化(高频场景必看)

1. 缓存Pattern实例(最重要!)

Pattern.compile() 是一个昂贵的操作(需要编译正则表达式为内部状态机)。禁止在循环中重复编译!

// ❌ 错误:每次调用都重新编译(性能极差)
public boolean isValidPhone(String phone) {
    return phone.matches("\\d{11}");  // 内部调用 Pattern.compile()
}

// ✅ 正确:预先编译,复用 Pattern
public class PhoneValidator {
    private static final Pattern PHONE_PATTERN = Pattern.compile("\\d{11}");
    
    public static boolean isValidPhone(String phone) {
        return PHONE_PATTERN.matcher(phone).matches();
    }
}

2. 非捕获分组(?: )提升性能

如果不需要捕获分组内容,用 (?: ) 代替 ( ),减少内存开销。

// ❌ 不需要捕获却用了捕获分组(有性能开销)
Pattern.compile("(\\d{3})-(\\d{4})-(\\d{4})");

// ✅ 非捕获分组(性能更好)
Pattern.compile("\\d{3}-\\d{4}-\\d{4}");  // 如果不需要提取
// 或部分需要捕获
Pattern.compile("(\\d{3})-(?:\\d{4})-(\\d{4})");  // 只捕获第1段和第3段

3. 字符类优化

// ❌ 效率低(复杂选择)
Pattern.compile("(a|b|c|d|e)");

// ✅ 效率高(字符类)
Pattern.compile("[a-e]");

4. 注意灾难性回溯(Catastrophic Backtracking)

危险的正则(a+)+(a|aa)+(.*)*嵌套量词会导致指数级性能下降,极端情况下会让 CPU 飙到 100%。

// ❌ 高危正则:当输入为 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab" 时,回溯爆炸
Pattern.compile("(a+)+b");

八、常用正则表达式速查(生产直接复制)

场景正则表达式Java 代码
手机号(国内)^1[3-9]\d{9}$"^1[3-9]\\\\d{9}$"
邮箱^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$略长,建议封装
身份证号(18位)^\d{17}[\dXx]$"^\\\\d{17}[\\\\dXx]$"
IPv4 地址`^((25[0-5]2[0-4]\d
日期(YYYY-MM-DD)`^\d{4}-(0[1-9]1[0-2])-(0[1-9]
URL^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/.*)?$——
中文字符[\u4e00-\u9fa5]"[\\\\u4e00-\\\\u9fa5]"
空白行^\s*$"^\\\\s*$"
正整数^[1-9]\d*$"^[1-9]\\\\d*$"
金额(保留2位小数)^\d+(\.\d{1,2})?$"^\\\\d+(\\\\.\\\\d{1,2})?$"

九、完整工具类封装(生产直接复制)

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.ArrayList;
import java.util.List;

public final class RegexUtils {
    private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
    private static final Pattern EMAIL_PATTERN = 
        Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
    
    private RegexUtils() {}
    
    // ===== 校验 =====
    public static boolean isPhone(String str) {
        return str != null && PHONE_PATTERN.matcher(str).matches();
    }
    
    public static boolean isEmail(String str) {
        return str != null && EMAIL_PATTERN.matcher(str).matches();
    }
    
    public static boolean match(String str, String regex) {
        if (str == null || regex == null) return false;
        return Pattern.compile(regex).matcher(str).matches();
    }
    
    // ===== 提取 =====
    public static String extractFirst(String str, String regex) {
        if (str == null || regex == null) return null;
        Matcher m = Pattern.compile(regex).matcher(str);
        return m.find() ? m.group(1) : null;
    }
    
    public static List<String> extractAll(String str, String regex) {
        List<String> result = new ArrayList<>();
        if (str == null || regex == null) return result;
        Matcher m = Pattern.compile(regex).matcher(str);
        while (m.find()) {
            result.add(m.group(1));
        }
        return result;
    }
    
    // ===== 替换 =====
    public static String replaceAll(String str, String regex, String replacement) {
        if (str == null || regex == null) return str;
        return Pattern.compile(regex).matcher(str).replaceAll(replacement);
    }
    
    // ===== 分割 =====
    public static String[] split(String str, String regex) {
        if (str == null || regex == null) return new String[0];
        return Pattern.compile(regex).split(str);
    }
}

十、思考题(检验是否真的懂了)

// 问题1:下面代码输出什么?为什么?
String text = "123";
System.out.println(text.matches("\\d"));    // A
System.out.println(text.matches("\\d+"));   // B
System.out.println(text.matches("\\d{3}")); // C

// 问题2:如何用正则从 "订单号:ORD-20260818-001" 中提取出 "20260818"?
String str = "订单号:ORD-20260818-001";
// 请写出代码

// 问题3:下面两段代码,哪一段性能更好?为什么?
// 方法 A
for (String s : list) {
    s.matches("\\d{11}");
}
// 方法 B
Pattern p = Pattern.compile("\\d{11}");
for (String s : list) {
    p.matcher(s).matches();
}

答案(选中下方空白区域查看):

  1. A 输出 falsematches() 要求完全匹配,"123" 不等于单个数字);B 输出 true"123" 是一个或多个数字);C 输出 true"123" 恰好 3 位数字)。
  2. Pattern.compile("ORD-(\\d{8})").matcher(str);m.find()m.group(1)
  3. 方法 B 性能更好。方法 A 每次循环都调用 String.matches(),内部会重新 Pattern.compile(),开销巨大。方法 B 只编译一次,复用了 Pattern 实例。

总结(终极速查表)

场景推荐做法
表单校验(手机号、邮箱)Pattern 编译为 static final,用 matches() 完全匹配
日志/文本提取信息find() + group() 循环提取
批量替换文本replaceAll()appendReplacement()
频繁校验务必缓存 Pattern,禁止在循环中 String.matches()
不需要捕获分组(?: ) 代替 ( ),提升性能
复杂正则Pattern.COMMENTS 加注释,提高可读性
遇到性能问题检查是否出现嵌套量词,防止灾难性回溯

以上就是Java中正则表达式Pattern和Matcher的使用终极指南的详细内容,更多关于Java正则表达式的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文