Appearance
第42课:正则表达式
🎯 学习目标
- 掌握 Pattern/Matcher 的用法与预编译
- 掌握正则语法(字符类/量词/分组/边界/预查)
- 掌握 matches/find/group、替换、分割
- 理解贪婪与非贪婪、性能陷阱
- 知道常用正则(邮箱/手机/身份证)
📖 一、概念讲解:正则与 Java API
1. 正则是什么
正则表达式是描述字符串模式的"小语言"——用一个模式匹配一类字符串。
\d+ 匹配一个或多个数字
[a-z]+ 匹配小写字母
^\d{11}$ 11位数字(手机号)2. Java 的两个核心类
- Pattern:编译后的正则(
Pattern.compile("..."))。 - Matcher:对输入串执行匹配(
pattern.matcher(input))。
java
Pattern p = Pattern.compile("\\d+");
Matcher m = p.matcher("abc123def456");
while (m.find()) {
System.out.println(m.group()); // 123, 456
}3. String 的正则方法
java
"abc123".matches("\\w+"); // true,整体匹配
"a,b,c".split(","); // [a,b,c]
"123abc".replaceAll("\\d", "*"); // ***abcString 方法内部用 Pattern/Matcher,简单场景够用,复杂/重复用 Pattern 预编译。
📖 二、正则语法
1. 字符类
\d 数字 [0-9] \D 非数字
\w 单词字符 [a-zA-Z0-9_] \W 非单词字符
\s 空白(空格/tab/换行) \S 非空白
. 任意字符(默认不含换行)
[abc] a或b或c [^abc] 非abc [a-z] a到z2. 量词
a? 0或1次
a* 0或多次
a+ 1或多次
a{3} 恰好3次
a{3,} 至少3次
a{3,5} 3到5次3. 分组与引用
(abc) 捕获组,group(1) 取
(?:abc) 非捕获组(不存储)
\1 引用第1个捕获组4. 边界
^ 字符串开头 $ 字符串结尾
\b 单词边界5. 预查(零宽断言)
(?=pattern) 正向预查(后面是 pattern)
(?!pattern) 负向预查(后面不是)
(?<=pattern) 反向预查(前面是)
(?<!pattern) 负向反向预查📖 三、贪婪与非贪婪
java
"<a><b>".matches("<.*>") // 贪婪,匹配整个 <a><b>(尽可能多)
"<a><b>".matches("<.*?>") // 非贪婪,匹配 <a>(尽可能少)- 贪婪(默认):
*+尽可能多匹配。 - 非贪婪(加
?):*?+?尽可能少匹配。
提取标签内容、引号内字符串用非贪婪。
📖 四、匹配方法
1. matches vs find
matches():整体匹配(整个串符合模式)。find():查找子串匹配(串中找符合模式的部分)。
java
"abc123".matches("\\d+"); // false(整体不是纯数字)
Pattern.compile("\\d+").matcher("abc123").find(); // true(含数字子串)2. group 分组
java
Pattern p = Pattern.compile("(\\d+)-(\\d+)");
Matcher m = p.matcher("12-34");
if (m.matches()) {
m.group(0); // "12-34"(整体)
m.group(1); // "12"(第1组)
m.group(2); // "34"(第2组)
}3. 替换与分割
java
"123abc456".replaceAll("\\d+", "#"); // #abc#
"a, b;;c".split("[,\\s;]+"); // [a,b,c]⚠️ 五、常见陷阱
陷阱1:忘记双重转义
Java 字符串里 \d 要写 \\d(\ 是字符串转义符)。"\\d+" 才是正则 \d+。
陷阱2:贪婪过度匹配
<.*> 贪婪会匹配到最后的 >,提取标签应用 <.*?>(非贪婪)。
陷阱3:未预编译重复正则
循环里 str.matches("\\d+") 每次编译正则,性能差。预编译 Pattern 复用。
陷阱4:正则灾难(ReDoS)
复杂正则(如嵌套量词 (a+)+)可能指数级回溯,恶意输入导致卡死。避免复杂回溯,用简单正则或专门的解析器。
陷阱5:matches 整体匹配误解
matches() 要求整个串匹配,不是部分。要部分用 find() 或加 .* 包围。
🆚 六、Java vs C / 其他
| 特性 | C(regex.h/PCRE) | Java |
|---|---|---|
| 引擎 | PCRE/POSIX | java.util.regex |
| API | regcomp/regexec | Pattern/Matcher |
| 预编译 | 手动 | Pattern.compile |
| 性能 | PCRE 快 | 够用 |
对 C 程序员:Java 正则对应 C 的 regex.h/PCRE,语法基本一致。Java 用 Pattern/Matcher 封装,预编译复用提升性能。正则语法是通用技能,跨语言一致。
💡 七、最佳实践
- 预编译 Pattern 复用(static final),别循环里 matches。
- 双重转义:
\\d不是\d。 - 非贪婪提取:
.*?避免过度匹配。 - matches 整体,find 子串,按需选。
- 用 group 提取分组,别再 substring 拆。
- 避免 ReDoS:简单正则,复杂解析用专门库。
- 复杂校验用现成正则(邮箱/手机),但测试边界。
📝 练习预告
完成 练习/Ex42_Regex.java 中的 6 道题:
- 数字匹配(find 提取所有数字)
- 手机号验证(matches 整体)
- 邮箱提取(find + group)
- 字符串替换(replaceAll 脱敏)
- 多分隔符分割(split)
- 综合:日志解析(分组提取时间/级别/消息)
完成后对比 答案/Sol42.java,查看逐行讲解与多解法。
📖 八、Pattern 与 Matcher 深入
Pattern 是编译后的正则,线程安全;Matcher 保存匹配状态,不是线程安全对象。
推荐写法:
java
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");
public static boolean isEmail(String input) {
return EMAIL_PATTERN.matcher(input).matches();
}原因:
text
Pattern.compile 有成本。
重复使用 static final Pattern 能减少编译开销。
Matcher 每次根据输入创建,避免状态污染。Matcher 常用方法:
text
matches:整体匹配。
find:查找下一个匹配片段。
group:取匹配内容。
start/end:匹配片段的起止位置。
replaceAll:替换所有匹配。
appendReplacement/appendTail:复杂替换。📖 九、贪婪、非贪婪与占有量词
Java 正则量词有三种常见模式:
| 类型 | 示例 | 含义 |
|---|---|---|
| 贪婪 | .* | 尽可能多匹配,必要时回溯 |
| 非贪婪 | .*? | 尽可能少匹配,必要时扩展 |
| 占有 | .*+ | 尽可能多匹配,不回溯 |
示例:
java
String html = "<b>one</b><b>two</b>";
Pattern greedy = Pattern.compile("<b>.*</b>");
Pattern lazy = Pattern.compile("<b>.*?</b>");结果:
text
greedy 匹配:<b>one</b><b>two</b>
lazy 第一次匹配:<b>one</b>占有量词适合减少回溯,但也可能导致本可成功的匹配失败。初学阶段重点掌握贪婪和非贪婪即可。
📖 十、命名分组与复杂提取
普通分组用数字索引:
java
Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = p.matcher("2026-06-30");
if (m.matches()) {
System.out.println(m.group(1));
}命名分组更清晰:
java
Pattern p = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher m = p.matcher("2026-06-30");
if (m.matches()) {
System.out.println(m.group("year"));
System.out.println(m.group("month"));
System.out.println(m.group("day"));
}命名分组适合日志解析、协议字段提取、复杂文本结构。
🧪 十一、实战案例:日志解析
日志格式:
text
2026-06-30 10:15:30 INFO [main] com.example.App - server started正则:
java
private static final Pattern LOG_PATTERN = Pattern.compile(
"^(?<time>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\s+" +
"(?<level>INFO|WARN|ERROR|DEBUG)\\s+" +
"\\[(?<thread>[^]]+)]\\s+" +
"(?<logger>[\\w.]+)\\s+-\\s+" +
"(?<message>.*)$"
);解析:
java
public static Map<String, String> parseLog(String line) {
Matcher matcher = LOG_PATTERN.matcher(line);
if (!matcher.matches()) {
return Map.of();
}
return Map.of(
"time", matcher.group("time"),
"level", matcher.group("level"),
"thread", matcher.group("thread"),
"logger", matcher.group("logger"),
"message", matcher.group("message")
);
}这个例子体现了正则的适用边界:固定格式文本很适合正则;层级复杂、可嵌套的语言不适合用正则硬解析。
🛡 十二、ReDoS 防护
ReDoS 是正则拒绝服务攻击,核心来自灾难性回溯。
危险模式:
text
(a+)+
([a-zA-Z]+)*
(.*a){10}恶意输入可能让匹配时间指数级增长。
防护策略:
text
避免嵌套量词。
限制输入长度。
优先使用明确字符类。
使用非捕获组减少无意义捕获。
对用户可配置正则做审核。
复杂格式用解析器,不用正则硬扛。
必要时使用超时控制或 RE2 类无回溯引擎。示例改造:
text
危险:^(a+)+$
更好:^a+$🛠 十三、正则排查清单
当正则结果不对时,按下面顺序查:
text
Java 字符串是否正确双重转义。
matches 和 find 是否用错。
是否忘记 ^ 和 $。
点号是否需要匹配换行。
量词是否过于贪婪。
分组编号是否因为新增括号而变化。
是否需要非捕获组 (?:...)。
输入中是否有不可见空白。
是否存在 Unicode 字符范围问题。
是否可能出现灾难性回溯。调试建议:
text
先用最小输入验证。
逐段增加正则复杂度。
给关键部分加命名分组。
把长正则拆成多行字符串并写注释。
为边界样例写单元测试。📊 十四、常见模式建议
常见需求的建议:
| 需求 | 建议 |
|---|---|
| 判断是否全数字 | ^\\d+$ |
| 提取文本中数字 | \\d+ + find |
| 多分隔符 split | [,;\\s]+ |
| 简单邮箱校验 | 使用宽松正则,复杂交给业务确认 |
| 手机号校验 | 根据国家和业务规则定义 |
| HTML 解析 | 不建议正则,使用 HTML parser |
| JSON 解析 | 不建议正则,使用 Jackson/Gson |
不要追求一个“完美邮箱正则”。很多校验应分成格式校验和业务校验两层。
🔍 十五、自测问题
text
Pattern 和 Matcher 谁是线程安全的?
matches 和 find 的区别是什么?
为什么 Java 正则要写 \\d 而不是 \d?
贪婪和非贪婪的区别是什么?
命名分组适合什么场景?
什么是灾难性回溯?
为什么不建议用正则解析 HTML 或 JSON?
循环中为什么应预编译 Pattern?✅ 十六、掌握标准
学完本课后,应能做到:
text
能用 Pattern/Matcher 完成匹配、提取、替换。
能正确处理 Java 字符串双重转义。
能区分 matches、find、group。
能使用分组和命名分组提取字段。
能解释贪婪、非贪婪和回溯。
能预编译 Pattern 提升重复匹配性能。
能识别 ReDoS 风险。
能判断什么时候不该用正则。正则是强大的文本工具,但不是通用解析器。越复杂的正则,越需要测试、边界控制和性能意识。
🎓 下一步
- 高级篇(30-42)完成!进入企业开发篇 43-JDBC。