这是一个 Java 快速入门项目,非常适合用来练手。相关源码已上传至 GitHub,👉 点击查看 GitHub 仓库。欢迎交流指正、提交 issue。
正则表达式(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+) 提取用户名和域名 |
三、简单示例
java
// 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 基本分组捕获
以下示例从日期字符串中分别提取年、月、日:
java
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>...),可以用名称代替编号:
java
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 非捕获分组
如果只需要用括号进行逻辑分组,而不想捕获内容,可以使用 (?:...) 语法。非捕获分组不会占用分组编号,也不会产生额外的内存开销:
java
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 个分组已匹配的内容,常用于匹配成对标签或重复单词:
java
// 匹配 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} 引用:
java
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)
默认的 *、+ 是贪婪模式,会尽可能多地匹配字符,可能导致回溯过多。在量词后加 ? 转为懒惰模式,匹配到第一个满足条件的位置即停止。
java
// 贪婪模式:可能匹配到最后一个 >,回溯严重
Pattern greedy = Pattern.compile("<.*>");
// 懒惰模式:匹配到第一个 > 即停止,性能更优
Pattern lazy = Pattern.compile("<.*?>");
5.2 预编译并缓存 Pattern 对象
String.matches()、String.split() 等每次调用都会重新编译正则,开销极大。应复用预编译的 Pattern 实例,尤其在循环或高频调用场景。
原因分析:
-
String.matches()javapublic boolean matches(String regex) { return Pattern.matches(regex, this); // 每次调用都重新编译 } -
String.split()javapublic String[] split(String regex, int limit) { // 每次调用都 Pattern.compile(regex) }
每次调用都会执行 Pattern.compile(regex),编译阶段需要将正则字符串解析为有限状态自动机(NFA/DFA),这是一个开销较大的过程。
性能差距有多大?
java
// ❌ 低效写法:循环中反复编译
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 常量 |
最佳实践:
java
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 拆分复杂正则,降低回溯风险
一个过于复杂的正则表达式难以维护且容易导致灾难性回溯。将其拆分为多个简单正则,分步验证,既提高可读性又提升性能。
java
// ❌ 一个正则包揽所有校验,回溯风险高
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 匹配以及验证结果输出的完整流程。
java
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 ? "✅ 合法" : "❌ 非法");
}
}
}
输出结果:
kotlin
=== 邮箱格式验证结果 ===
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(多行模式)