正则表达式是文本处理的瑞士 军刀,用于匹配、查找、替换、校验字符串。但在 java 中,它有一个让无数新手抓狂的特点——双重转义。
别再硬背正则语法了!这篇文章从 java 底层实现讲起,彻底搞懂匹配、提取、替换和性能优化
正则表达式是处理字符串的“核武器”。但很多 java 开发者对它的认知停留在“复制粘贴网上现成的表达式”,一旦遇到 \ 转义、pattern.compile 报错、matches() 返回 false 就一脸懵。
今天这篇文章,我们从 java 正则引擎的底层实现(pattern + matcher)、双重转义的根源、性能优化(pattern 缓存)、分组提取与替换 四个维度,彻底把 java 正则讲透。
一、java 正则核心类(一张表看懂)
| 类 | 所属包 | 核心职责 | 是否线程安全 | 关键方法 |
|---|---|---|---|---|
| pattern | java.util.regex | 编译正则表达式(将字符串转为内部状态机) | ✅ 线程安全(不可变) | compile(string regex)、matcher(charsequence input)、split(charsequence input) |
| matcher | java.util.regex | 执行匹配操作(维护匹配状态和位置) | ❌ 线程不安全(有状态) | matches()、find()、group()、replaceall() |
| matchresult | java.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
黄金法则:
- 校验整个字符串是否符合格式 →
string.matches()或matcher.matches() - 从长文本中提取内容 →
matcher.find()+matcher.group()
五、分组提取: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();
}
答案(选中下方空白区域查看):
- a 输出
false(matches()要求完全匹配,"123"不等于单个数字);b 输出true("123"是一个或多个数字);c 输出true("123"恰好 3 位数字)。 pattern.compile("ord-(\\d{8})").matcher(str);→m.find()→m.group(1)。- 方法 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正则表达式的资料请关注代码网其它相关文章!
发表评论