欢迎光临
我们一直在努力

Java正则表达式详解

文章目录

    • 一、核心类
      • 1.1 Pattern 类(编译正则表达式)
      • 1.2 Matcher 类(执行匹配操作)
      • 1.3 String 类内置方法(日常最常用)
    • 二、常用正则语法
    • 三、简单示例
    • 四、分组捕获示例
      • 4.1 基本分组捕获
      • 4.2 命名分组(Java 7+)
      • 4.3 非捕获分组
      • 4.4 分组嵌套与反向引用
      • 4.5 分组在替换中的应用
    • 五、正则性能优化建议
      • 5.1 优先使用懒惰模式(Lazy Quantifier)
      • 5.2 预编译并缓存 Pattern 对象
      • 5.3 拆分复杂正则,降低回溯风险
    • 六、实战案例:用户输入验证
    • 七、注意事项

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

一、核心类

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

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


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+) 提取用户名和域名

三、简单示例

// 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 基本分组捕获

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

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>…),可以用名称代替编号:

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 非捕获分组

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

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 个分组已匹配的内容,常用于匹配成对标签或重复单词:

// 匹配 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} 引用:

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)

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

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

5.2 预编译并缓存 Pattern 对象

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

原因分析:

  • String.matches()public boolean matches(String regex) {
    return Pattern.matches(regex, this); // 每次调用都重新编译
    }
  • String.split()public String[] split(String regex, int limit) {
    // 每次调用都 Pattern.compile(regex)
    }

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

性能差距有多大?

// ❌ 低效写法:循环中反复编译
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 常量

最佳实践:

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 拆分复杂正则,降低回溯风险

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

// ❌ 一个正则包揽所有校验,回溯风险高
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 匹配以及验证结果输出的完整流程。

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 ? "✅ 合法" : "❌ 非法");
}
}
}

输出结果:

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

代码要点说明:

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

  • 七、注意事项

    • . 默认不匹配换行符,可用 Pattern.DOTALL 改变
    • Java 字符串中反斜杠需转义,如 \\d 写成 "\\\\d"
    • 大量重复匹配时复用 Pattern,避免每次调用 String.matches() 重新编译
    • 常用标志:Pattern.CASE_INSENSITIVE(忽略大小写)、Pattern.MULTILINE(多行模式)
    赞(0)
    未经允许不得转载:171主机测评 » Java正则表达式详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址