文章目录
-
- 一、核心类
-
- 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.DOTALL 改变
- Java 字符串中反斜杠需转义,如 \\d 写成 "\\\\d"
- 大量重复匹配时复用 Pattern,避免每次调用 String.matches() 重新编译
- 常用标志:Pattern.CASE_INSENSITIVE(忽略大小写)、Pattern.MULTILINE(多行模式)





