正则表达式基础回顾
在深入技巧之前,我们需要明确Java中正则表达式的几个核心概念。Pattern和Matcher是Java.util.regex包中的两个核心类,分别用于编译正则表达式和执行匹配操作。Java的正则语法基于Perl风格,但有一些细微差别,比如在Java中需要用双反斜杠(\\)来表示特殊字符。
预编译Pattern提升性能
频繁使用同一个正则表达式时,预编译Pattern对象可以显著提升性能:
```java
// 错误做法:每次调用都重新编译
boolean matches = "text".matches("regex");
// 正确做法:预编译Pattern
private static final Pattern PATTERN = Pattern.compile("regex");
boolean matches = PATTERN.matcher("text").matches();
预编译后的Pattern是线程安全的,适合在多线程环境中共享使用。根据测试,预编译可以使匹配速度提升5-10倍。
### 合理使用边界匹配符
边界匹配符经常被忽视但非常实用:
- `^` 匹配行开头(在多行模式下)
- `$` 匹配行结尾
- `\b` 匹配单词边界
- `\B` 匹配非单词边界
例如,精确匹配整行内容:
```java
Pattern.compile("^\\d{3}-\\d{2}-\\d{4}$"); // 匹配SSN格式
非贪婪量词优化匹配
默认情况下,量词(*, +, ?)是贪婪的,会尽可能多地匹配字符。添加?可使其变为非贪婪:
// 贪婪匹配
"aabab".matches("a.*b"); // 匹配整个字符串
// 非贪婪匹配
"aabab".matches("a.*?b"); // 只匹配到"aab"
在处理HTML或XML等嵌套结构时,非贪婪匹配能避免过度匹配问题。
分组与反向引用技巧
捕获组不仅可以提取子串,还能用于反向引用:
// 查找重复单词
Pattern.compile("\\b(\\w+)\\b\\s+\\1\\b");
// 格式化电话号码
"1234567890".replaceAll("(\\d{3})(\\d{3})(\\d{4})", "($1) $2-$3");
命名捕获组(?
前瞻后顾断言应用
零宽断言可以在不消耗字符的情况下进行条件判断:
- (?=pattern)
正向肯定前瞻
- (?!pattern)
正向否定前瞻
- (?<=pattern)
反向肯定后顾
- (?<!pattern)
反向否定后顾
示例:匹配后面不是数字的字母q:
Pattern.compile("q(?!\\d)");
字符类优化策略
合理使用字符类可以提升可读性和性能:
- 使用[a-z]而非a|b|c|...|z
- 预定义字符类如\d(数字)、\s(空白符)等
- 排除型字符类[^...]
特殊技巧:使用&&进行字符类交集
Pattern.compile("[a-z&&[^aeiou]]"); // 匹配辅音字母
常见性能陷阱与优化
- 灾难性回溯:避免嵌套量词如
(a+)+
- 过度匹配:尽量使用具体范围而非.*
- 不必要的捕获组:用(?:...)替代(...)
- 重复编译:如前所述预编译Pattern
检测方法:对长字符串匹配时出现性能骤降。
Unicode处理技巧
Java正则表达式完全支持Unicode:
- \p{L}
匹配任何语言的字母
- \p{Sc}
匹配货币符号
- \P{M}
匹配非组合标记
示例:匹配包含重音符号的单词:
Pattern.compile("\\b\\p{L}+\\b", Pattern.UNICODE_CHARACTER_CLASS);
实用工具方法封装
封装常用正则操作为工具类:
public class RegexUtils {
public static List<String> extractEmails(String text) {
Pattern pattern = Pattern.compile("\\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}\\b",
Pattern.CASE_INSENSITIVE);
return pattern.matcher(text)
.results()
.map(MatchResult::group)
.collect(Collectors.toList());
}
// 更多实用方法...
}
调试与测试技巧
- 使用在线工具如regex101.com测试正则
- 分步构建复杂正则表达式
- 使用Matcher的groupCount()和group(int)方法调试
- 单元测试边界情况:
@Test
public void testPhonePattern() {
assertTrue(Pattern.matches("\\d{3}-\\d{3}-\\d{4}", "123-456-7890"));
assertFalse(Pattern.matches("\\d{3}-\\d{3}-\\d{4}", "123-456-789"));
}