Java 正则表达式完全指南:从入门到实战
正则表达式是每个开发者都绕不开的必修课。它强大、实用,却也以"难学难懂"著称。本文将带你系统梳理正则表达式的核心概念,并结合 Java 实战场景,帮助你真正掌握这门技术。
一、什么是正则表达式?
正则表达式(Regular Expression,简称 Regex)是一组用于描述文本规则的模式语言。你可以用它来:
- 匹配验证:判断字符串是否符合某种格式(如手机号、邮箱、日期)
- 提取信息:从大段文本中抓取关键字段
- 文本处理:对符合规则的内容进行替换、分割等操作
要不要学正则表达式? 答案是肯定的。它是程序员的必备工具,能用极少的代码完成复杂的文本处理,也能在支持正则的编辑器(如 VS Code、IntelliJ IDEA)中大幅提升工作效率。
正则为什么难学? 因为它的语法与日常编程思维差异较大,需要耐心拆解、逐步分析,并借助在线工具(如 regex101.com)反复验证。一开始觉得难是完全正常的,坚持实践是唯一的出路。
二、常见正则表达式示例
在深入语法之前,先来感受几个实用的正则表达式:
| 正则表达式 | 用途说明 | |
|---|---|---|
^\d{5,12}$ | 5 到 12 位的纯数字(如用户名规则) | |
| `^(0 | [1-9][0-9]*)$` | 非负整数(0 或不以 0 开头的正整数) |
^(-?\d+)(.\d+)?$ | 整数或小数(可带负号) | |
| `\d{3}-\d{8} | \d{4}-\d{7}` | 国内固定电话(如 010-12345678、021-1234567) |
^\d{4}-\d{1,2}-\d{1,2}$ | 日期格式 yyyy-MM-dd | |
\n\s*\r | 空白行 |
三、元字符速查表
元字符是正则表达式的基础"词汇",理解它们是学习正则的第一步。
3.1 位置与通配符
| 元字符 | 说明 |
|---|---|
^ | 字符串的开始位置 |
$ | 字符串的结束位置 |
. | 匹配任意单个字符(通常不含换行符) |
\w | 单个"word"字符:字母 / 数字 / 下划线 / 汉字 |
\s | 单个空白字符(包含 \n、\r、\t) |
\d | 单个数字字符(等价于 [0-9]) |
\b | 单词的开始或结束边界 |
3.2 重复限定符
| 符号 | 说明 |
|---|---|
* | 0 次或多次 |
+ | 1 次或多次 |
? | 0 次或 1 次(可选) |
{n} | 恰好 n 次 |
{n,} | 至少 n 次 |
{n,m} | n 到 m 次 |
⚠️ 注意:当这些元字符需要表达字面含义时(如匹配一个真实的 * 号),必须使用反斜杠转义,写成 *。
3.3 选择与字符集
| 正则表达式 | 说明 | ||
|---|---|---|---|
[aeiou] | 匹配 a、e、i、o、u 中的任意一个字符 | ||
[0-9] | 匹配单个数字字符 | ||
[A-Z] | 匹配单个大写字母 | ||
[A-Z0-9_] | 匹配大写字母、数字或下划线之一 | ||
| `Hi | hi` | 匹配 Hi 或 hi(` | ` 表示"或") |
3.4 反义符号(用得较少)
| 正则表达式 | 说明 |
|---|---|
[^aeiou] | 匹配除 a/e/i/o/u 之外的任意字符 |
[^x] | 匹配非 x 的任意字符 |
\W | 匹配非 \w 字符 |
\S | 匹配非空白字符 |
\D | 匹配非数字字符 |
\B | 非单词边界位置 |
四、Java 中的转义规则
Java 字符串本身就有一套转义规则(如 \n 表示换行),而正则表达式也有自己的元字符(如 \d 表示数字)。两者叠加,就产生了"双重转义"的问题。
结论:在 Java 代码中写正则时,所有正则里的 `` 都需要写成 \ 。
| 正则表达式 | Java 字符串中的写法 |
|---|---|
\d | "\d" |
\w | "\w" |
\d{3}-\d{8} | "\d{3}-\d{8}" |
Java 中常用的转义字符:
| 转义字符 | 含义 |
|---|---|
\n | 换行符 |
\r | 回车符 |
\t | 制表符(Tab) |
" ' | 双引号 / 单引号 |
\ | 反斜杠本身 |
\uXXXX | Unicode 字符 |
五、Java 中使用正则的两种方式
5.1 String 类的便捷方法
String 类内置了对正则的直接支持,适合简单、低频的场景,无需手动创建 Pattern 对象。
| 方法 | 作用 | 示例 |
|---|---|---|
split(regex) | 按正则分割字符串 | "a,b;c".split("[,;]") → ["a","b","c"] |
replaceAll(regex, replacement) | 替换所有匹配的子串 | "123abc".replaceAll("\d", "") → "abc" |
replaceFirst(regex, replacement) | 仅替换第一个匹配的子串 | "123abc123".replaceFirst("\d+", "0") → "0abc123" |
matches(regex) | 验证整个字符串是否完全匹配 | "13800138000".matches("1[3-9]\d{9}") → true |
5.2 Pattern + Matcher(推荐用于高频场景)
String 的正则方法每次调用时,JVM 都会在内部将字符串重新编译为 Pattern 对象。在循环或高频调用场景下,这会带来严重的性能损耗。
推荐做法:将 Pattern 预编译为静态常量,复用该对象。
// ❌ 不推荐:每次调用都会重新编译正则
public boolean isValidPhone(String phone) {
return phone.matches("1[3-9]\d{9}");
}
// ✅ 推荐:全局预编译,仅编译 1 次
private static final Pattern PHONE_PATTERN = Pattern.compile("1[3-9]\d{9}");
public boolean isValidPhone(String phone) {
return PHONE_PATTERN.matcher(phone).matches();
}
优化原则总结:
- 低频、一次性场景:直接用
String的便捷方法,代码更简洁。 - 高频、循环或工具类场景:必须用
Pattern预编译,性能提升可达数十倍。 - 永远不要在循环内调用
String的正则方法。
六、分组与捕获
分组是正则表达式最强大的功能之一,允许你把关心的部分"抓"出来单独处理。
6.1 捕获分组 ()
用圆括号包裹的子表达式会被捕获,并按左括号出现的顺序从 1 开始编号,可通过 Matcher.group(n) 获取对应内容。
正则:(a)(b)
匹配:ab
group(1) = "a"
group(2) = "b"
嵌套分组时,编号依然只看左括号的顺序:
正则:((a)(b))
匹配:ab
group(1) = "ab" // 外层括号,编号最靠前
group(2) = "a"
group(3) = "b"
6.2 非捕获分组 (?:)
若只需要括号的分组功能(用于改变匹配优先级或与 | 配合),但不需要捕获内容,应使用 (?:)。它不会分配编号,可以提升正则性能。
正则:(?:姓名|用户名):(.+)
匹配:姓名:张三
group(1) = "张三" // (?:...) 不占编号
6.3 核心 API 说明
| 类 / 方法 | 作用 |
|---|---|
Pattern.compile(regex) | 预编译正则表达式 |
pattern.matcher(input) | 创建 Matcher,关联待匹配的字符串 |
matcher.find() | 在字符串中查找下一个匹配项,返回 boolean |
matcher.matches() | 验证整个字符串是否完全匹配 |
matcher.group(n) | 获取第 n 个捕获分组的内容(group(0) = 整个匹配串) |
⚠️ 必须先调用 find() 或 matches(),才能调用 group(),否则会抛出 IllegalStateException。
七、综合实战示例
示例 1:提取中文文本中的字段
String text = "姓名:张三,年龄:25";
Pattern pattern = Pattern.compile("姓名:(.+),年龄:(\d+)");
Matcher matcher = pattern.matcher(text);
if (matcher.matches()) {
String name = matcher.group(1); // 张三
String age = matcher.group(2); // 25
System.out.println("姓名:" + name + ",年龄:" + age);
}
示例 2:循环提取多个匹配项
String text = "订单号:20240501001,金额:99.99;订单号:20240501002,金额:199.99";
// 预编译,避免重复编译
private static final Pattern ORDER_PATTERN =
Pattern.compile("订单号:(\d+),金额:(\d+\.\d+)");
public static void main(String[] args) {
Matcher matcher = ORDER_PATTERN.matcher(text);
while (matcher.find()) {
String orderNo = matcher.group(1); // 订单号
String amount = matcher.group(2); // 金额
System.out.println("订单号:" + orderNo + ",金额:" + amount);
}
}
// 输出:
// 订单号:20240501001,金额:99.99
// 订单号:20240501002,金额:199.99
示例 3:手机号验证工具类(最佳实践)
public class PhoneValidator {
// 静态常量,全局仅编译一次
private static final Pattern PHONE_PATTERN = Pattern.compile("1[3-9]\d{9}");
public static boolean isValid(String phone) {
if (phone == null) return false;
return PHONE_PATTERN.matcher(phone).matches();
}
}
八、总结
| 知识点 | 核心要点 |
|---|---|
| 元字符 | \d、\w、\s、.、^、$ 是基础,需熟记 |
| 重复限定符 | *、+、?、{n,m} 控制匹配次数 |
| Java 转义 | 正则中的 `` 在 Java 字符串中需写成 \ |
| String 方法 | matches、replaceAll、split 适合简单场景 |
| Pattern 预编译 | 高频场景必须使用,避免重复编译导致性能问题 |
| 捕获分组 | () 捕获内容,编号按左括号顺序,(?:) 不捕获 |
| find() vs matches() | find() 查找子串,matches() 验证完整字符串 |
正则表达式的掌握没有捷径,关键是多写多练,借助工具验证,遇到复杂表达式时耐心拆解——每一个括号、每一个符号都有它的含义。