Java正则表达式详解

这是一个 Java 快速入门项目,非常适合用来练手。相关源码已上传至 GitHub,👉 点击查看 GitHub 仓库。欢迎交流指正、提交 issue。

正则表达式(Regular Expression)用于匹配、查找和替换字符串。Java 通过 java.util.regex 包提供支持。

一、核心类

用途
Pattern 编译正则表达式
Matcher 执行匹配操作的引擎
PatternSyntaxException 语法错误异常

Java 正则表达式主要通过 java.util.regex 包中的 PatternMatcher 两个类来实现。


1.1 Pattern 类(编译正则表达式)

方法 说明
Pattern.compile(String regex) 将正则字符串编译为 Pattern 对象
Pattern.compile(String regex, int flags) 编译时指定标志(如 CASE_INSENSITIVE
pattern.matcher(String input) 用 Pattern 创建 Matcher 对象
pattern.split(String input) 按正则分割字符串,返回 String[]

1.2 Matcher 类(执行匹配操作)

方法 说明
matches() 整个字符串是否完全匹配正则
find() 在字符串中查找下一个匹配的子串
lookingAt() 字符串开头是否匹配正则
group() 返回本次匹配的子串(等同于 group(0)
group(int group) 返回第 n 个捕获组的内容
groupCount() 返回捕获组的数量
start() / end() 返回匹配子串的起始/结束索引
replaceAll(String replacement) 替换所有匹配子串
replaceFirst(String replacement) 替换第一个匹配子串

1.3 String 类内置方法(日常最常用)

方法 说明
str.matches(String regex) 判断整个字符串是否匹配(内部调 Pattern.matches
str.replaceAll(String regex, String replacement) 替换所有匹配
str.replaceFirst(String regex, String replacement) 替换第一个匹配
str.split(String regex) 按正则分割字符串
str.split(String regex, int limit) 分割,限制最多分成 limit 部分

二、常用正则语法

语法 含义 示例
. 任意字符(除换行) a.b 匹配 a1b
\d 数字 \d{3} 匹配三位数
\w 单词字符 \w+ 匹配单词
\s 空白字符 空格、制表符、换行
* 0次或多次 a* 匹配 "", "a", "aaa"
+ 1次或多次 \d+ 匹配至少一位数字
? 0次或1次 colou?r 匹配 color, colour
{n,m} n到m次 \d{2,4} 匹配2~4位数字
^ 行开头 ^Hello 匹配行首的 Hello
$ 行结尾 end$ 匹配行尾的 end
[] 字符集 [aeiou] 匹配任意元音
() 分组捕获 (\\w+)@(\\w+) 提取用户名和域名

三、简单示例

java 复制代码
// 1. 匹配
// 使用 Pattern.matches() 静态方法判断输入字符串是否完全匹配给定的正则表达式。
// "\\d+" 是正则表达式,\d 表示数字 [0-9],+ 表示出现一次或多次,即匹配一个或多个连续数字。
// "123" 是待匹配的目标字符串。由于它完全由数字组成,所以匹配成功,isMatch 的值为 true。
boolean isMatch = Pattern.matches("\\d+", "123"); // true

// 验证手机号
String phone = "13812345678";
boolean valid = phone.matches("^1[3-9]\\d{9}$");

// 2. 查找
// 编译正则表达式:匹配以 a 开头、b 结尾,中间为任意一个字符的模式
Pattern p = Pattern.compile("a.b");
// 创建 Matcher 对象,用于在目标字符串中进行匹配查找
Matcher m = p.matcher("a2b aXb acb");
// 循环查找所有匹配的子串
while (m.find()) {
    // 输出当前匹配到的内容(依次输出 a2b、aXb、acb)
    System.out.println(m.group()); // a2b aXb acb
}

// 3. 替换
// 定义一个包含数字的字符串
String s = "Hello 123 World 456";
// 使用 replaceAll 方法将字符串中所有匹配正则的部分替换为指定文本
// 正则表达式 \\d+ 的含义:
//   - \\d 匹配任意数字字符(0-9),Java 字符串中 \ 需写成 \\
//   - + 表示前面的 \\d 出现 1 次或多次,即匹配连续的数字序列
// 因此 "123" 和 "456" 这两个数字子串都会被替换为 "数字"
String result = s.replaceAll("\\d+", "数字");
// 输出结果:Hello 数字 World 数字
System.out.println(result);

// 4. 分割字符串
String[] parts = "a,b;;c".split("[,;]+"); // ["a", "b", "c"]

小提示 :Java 正则中的反斜杠需要写成 \\,因为 Java 字符串本身会先解析一次 \


四、分组捕获示例

分组捕获是正则表达式中非常实用的功能,它允许我们将匹配到的内容按规则拆分成多个独立的部分,便于后续提取和处理。在 Java 中,分组通过圆括号 () 定义,每个左括号对应一个分组编号,编号从 1 开始,group(0) 表示整个匹配结果。

4.1 基本分组捕获

以下示例从日期字符串中分别提取年、月、日:

java 复制代码
Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = p.matcher("日期:2026-06-01");
if (m.find()) {
    System.out.println(m.group(0)); // 2026-06-01(整个匹配)
    System.out.println(m.group(1)); // 2026(年)
    System.out.println(m.group(2)); // 06(月)
    System.out.println(m.group(3)); // 01(日)
}

正则 (\\d{4})-(\\d{2})-(\\d{2}) 中包含了三个分组,分别匹配四位年份、两位月份和两位日期。通过 group(n) 即可按编号取出对应内容,非常直观。

4.2 命名分组(Java 7+)

当分组较多时,数字编号不易记忆。Java 7 引入了命名分组语法 (?<name>...),可以用名称代替编号:

java 复制代码
Pattern p = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher m = p.matcher("2026-06-01");
if (m.matches()) {
    System.out.println(m.group("year"));  // 2026
    System.out.println(m.group("month")); // 06
    System.out.println(m.group("day"));   // 01
}

命名分组让代码可读性大幅提升,尤其在处理多个分组时,能避免混淆编号顺序。

4.3 非捕获分组

如果只需要用括号进行逻辑分组,而不想捕获内容,可以使用 (?:...) 语法。非捕获分组不会占用分组编号,也不会产生额外的内存开销:

java 复制代码
Pattern p = Pattern.compile("(?:https?://)?(www\\.\\w+\\.com)");
Matcher m = p.matcher("https://www.example.com");
if (m.find()) {
    System.out.println(m.group(1)); // www.example.com(协议部分未被捕获)
}

这里 (?:https?://)? 匹配可选的协议前缀,但不会将其存入分组,group(1) 直接得到域名部分。

4.4 分组嵌套与反向引用

分组可以嵌套,编号按左括号出现的顺序从左到右计数。反向引用 \\n 可以在正则内部引用第 n 个分组已匹配的内容,常用于匹配成对标签或重复单词:

java 复制代码
// 匹配 HTML 标签对,如 <div>...</div>
Pattern p = Pattern.compile("<(\\w+)>.*?</\\1>");
Matcher m = p.matcher("<div>内容</div>");
System.out.println(m.matches()); // true

\\1 引用了第一个分组捕获的标签名,确保闭合标签与开始标签一致。

4.5 分组在替换中的应用

分组捕获的内容也可以在字符串替换时通过 $n${name} 引用:

java 复制代码
String date = "2026-06-01";
String reformatted = date.replaceAll(
    "(\\d{4})-(\\d{2})-(\\d{2})",
    "$2/$3/$1"
);
System.out.println(reformatted); // 06/01/2026

这在数据格式转换、日志解析等场景中非常实用,一行代码即可完成复杂的重组操作。


五、正则性能优化建议

5.1 优先使用懒惰模式(Lazy Quantifier)

默认的 *+ 是贪婪模式,会尽可能多地匹配字符,可能导致回溯过多。在量词后加 ? 转为懒惰模式,匹配到第一个满足条件的位置即停止。

java 复制代码
// 贪婪模式:可能匹配到最后一个 >,回溯严重
Pattern greedy = Pattern.compile("<.*>");
// 懒惰模式:匹配到第一个 > 即停止,性能更优
Pattern lazy = Pattern.compile("<.*?>");

5.2 预编译并缓存 Pattern 对象

String.matches()String.split() 等每次调用都会重新编译正则,开销极大。应复用预编译的 Pattern 实例,尤其在循环或高频调用场景。

原因分析:

  • String.matches()

    java 复制代码
    public boolean matches(String regex) {
        return Pattern.matches(regex, this); // 每次调用都重新编译
    }
  • String.split()

    java 复制代码
    public String[] split(String regex, int limit) {
        // 每次调用都 Pattern.compile(regex)
    }

每次调用都会执行 Pattern.compile(regex),编译阶段需要将正则字符串解析为有限状态自动机(NFA/DFA),这是一个开销较大的过程。

性能差距有多大?

java 复制代码
// ❌ 低效写法:循环中反复编译
for (String s : list) {
    s.matches("\\d+");  // 每次都 compile
}

// ✅ 高效写法:预编译,复用 Pattern
Pattern p = Pattern.compile("\\d+");  // 只编译一次
for (String s : list) {
    p.matcher(s).matches();  // 只创建 Matcher,开销极小
}

在循环 100 万次的场景下,预编译写法通常能快 5~10 倍,因为编译开销被均摊掉了。

什么时候需要关心?

场景 是否需要预编译
循环/高频调用 必须预编译
只调用一次 ❌ 无所谓,str.matches() 更简洁
正则固定不变 ✅ 可以声明为 static final 常量

最佳实践:

java 复制代码
public class RegexCache {
	// 正则作为类级别常量,只编译一次
    private static final Pattern EMAIL_PATTERN =
            Pattern.compile("^[\\w.-]+@[\\w.-]+\\.\\w{2,}$");

    public static boolean isValidEmail(String email) {
        return EMAIL_PATTERN.matcher(email).matches(); // 复用预编译 Pattern
    }
}

5.3 拆分复杂正则,降低回溯风险

一个过于复杂的正则表达式难以维护且容易导致灾难性回溯。将其拆分为多个简单正则,分步验证,既提高可读性又提升性能。

java 复制代码
// ❌ 一个正则包揽所有校验,回溯风险高
Pattern complex = Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,20}$");

// ✅ 拆分为多个简单条件,逐项验证
public static boolean isValidPassword(String pwd) {
    if (pwd.length() < 8 || pwd.length() > 20) return false;
    if (!pwd.matches("[a-zA-Z\\d]+")) return false;  // 仅允许字母数字
    if (!pwd.matches(".*[a-z].*")) return false;     // 至少一个小写
    if (!pwd.matches(".*[A-Z].*")) return false;     // 至少一个大写
    if (!pwd.matches(".*\\d.*")) return false;       // 至少一个数字
    return true;
}

什么时候该拆?什么时候不拆?

场景 建议
逻辑可明确分步 ✅ 拆分
有嵌套/递归结构 ✅ 拆分(正则本就不擅长)
正则超过一行就难读 ✅ 拆分
只需简单的格式校验 ❌ 单个正则即可,别过度设计
对性能极端敏感的热路径 ⚠️ 需实测,拆分有额外的字符串操作开销

六、实战案例:用户输入验证

下面通过一个完整的邮箱验证示例,展示 Pattern 编译、Matcher 匹配以及验证结果输出的完整流程。

java 复制代码
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class EmailValidator {

    // 编译正则表达式为 Pattern 对象(复用)
    private static final Pattern EMAIL_PATTERN =
            Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");

    /**
     * 验证邮箱格式是否合法
     * @param email 待验证的邮箱字符串
     * @return true 表示合法,false 表示非法
     */
    public static boolean isValidEmail(String email) {
        if (email == null || email.isEmpty()) {
            return false;
        }
        // 创建 Matcher 对象
        Matcher matcher = EMAIL_PATTERN.matcher(email);
        // 执行匹配并返回结果
        return matcher.matches();
    }

    public static void main(String[] args) {
        // 测试用例
        String[] testEmails = {
                "user@example.com",
                "hello.world@mail.co.uk",
                "invalid-email@",
                "user@.com",
                "name@domain.c",
                "test@sub.domain.com"
        };

        System.out.println("=== 邮箱格式验证结果 ===");
        for (String email : testEmails) {
            boolean result = isValidEmail(email);
            System.out.printf("%-30s → %s%n", email, result ? "✅ 合法" : "❌ 非法");
        }
    }
}

输出结果:

kotlin 复制代码
=== 邮箱格式验证结果 ===
user@example.com               → ✅ 合法
hello.world@mail.co.uk         → ✅ 合法
invalid-email@                 → ❌ 非法
user@.com                      → ❌ 非法
name@domain.c                  → ❌ 非法
test@sub.domain.com            → ✅ 合法

代码要点说明:

  1. Pattern.compile() :将正则表达式编译为 Pattern 对象,作为静态常量复用,避免重复编译开销
  2. Pattern.matcher() :创建 Matcher 对象,绑定待匹配的输入字符串
  3. matcher.matches() :尝试将整个输入字符串与正则进行完全匹配,返回 boolean 结果
  4. 空值检查 :先对输入做 null 和空字符串判断,避免 NullPointerException

七、注意事项

  • . 默认不匹配换行符,可用 Pattern.DOTALL 改变
  • Java 字符串中反斜杠需转义,如 \d 写成 "\\d"
  • 大量重复匹配时复用 Pattern,避免每次调用 String.matches() 重新编译
  • 常用标志:Pattern.CASE_INSENSITIVE(忽略大小写)、Pattern.MULTILINE(多行模式)
相关推荐
Flynt2 小时前
我试了Spring Boot 4的原生镜像,聊聊那些数字和坑
java·spring boot
M-Robots echo9 小时前
轻量化裁剪方案解析:M-Robots 模块化构建,适配高低端各类机器人硬件平台
java·机器人·ros·开源社区·atomgit·m-robots
不可求~9 小时前
多模型路由怎么落地:把任务分流写进项目的最小实现
java·前端·数据库
全栈弄潮儿10 小时前
如何向 AI 清楚描述一个编程需求
chatgpt·openai·ai编程
mldong10 小时前
用 DDD 设计工作流引擎:聚合根与充血模型
java·架构
candyTong10 小时前
Claude Code 如何恢复一段会话
后端·架构·ai编程
mqiqe11 小时前
AgentScope Java Harness:4. 双层记忆系统 让 Agent 拥有真正的“长期大脑“
java·开发语言
伍一5111 小时前
03-文件管理模块-一个看似简单的模块藏着多少细节
java·ruoyi·若依