Skip to content

正则表达式

概述

正则表达式(Regular Expression,常简写为 regex)用于描述字符串的模式,可用来检查「是否匹配」、查找子串、替换文本等。Java 通过 java.util.regex 包提供支持,核心类为 Pattern(编译正则)和 Matcher(在目标字符串上执行匹配)。掌握正则能大大简化字符串校验、解析和清洗等场景的代码。

前置建议

已掌握 String 与字符串操作,了解字符串不可变性与常用方法。


核心类:Pattern 与 Matcher

基本流程

  1. Pattern.compile(正则) 将正则字符串编译为 Pattern 对象(可复用,线程安全)。
  2. pattern.matcher(目标字符串) 得到 Matcher,在其上执行「是否整体匹配」「查找」「替换」等操作。
java
import java.util.regex.Pattern;
import java.util.regex.Matcher;

String regex = "\\d{3}-\\d{4}";  // 例如 123-4567 这样的格式
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher("电话: 123-4567");

// 判断整个目标串是否与正则完全匹配(从开头到结尾)
boolean fullMatch = matcher.matches();  // false,因为前面有 "电话: "

// 判断是否存在子串匹配
matcher.reset();  // 重置到开头,以便再次使用
boolean found = matcher.find();  // true,找到 "123-4567"

提示

在 Java 字符串中,反斜杠 \ 是转义符,所以正则里的 \d 要写成 "\\d",否则 \d 在源码里会被当成非法转义。写正则时心里想的是「一个反斜杠 + 字母」,代码里就要写两个反斜杠。


基本语法速览

类别写法含义
字符类[abc]任一字符 a、b、c
[a-z]小写 a 到 z
[^0-9]非数字(^ 在 [] 内表示取反)
预定义\d数字,等价于 [0-9]
\w单词字符:字母、数字、下划线
\s空白(空格、制表、换行等)
量词*0 次或多次
+1 次或多次
?0 次或 1 次
{n}恰好 n 次
{n,}至少 n 次
{n,m}n 到 m 次
边界^行/串开头(在 [] 外时)
$行/串结尾
分组(…)捕获组,可用 group(1) 取出

以上在 Java 字符串中需对反斜杠转义,例如 \d"\\d"\\w"\\\\w"(正则里是 \w)。


基本用法

判断是否整体匹配(校验格式)

适合校验「整串是否符合规则」,如手机号、身份证、邮箱格式。

java
// 简单示例:是否全为数字
Pattern p = Pattern.compile("\\d+");
Matcher m = p.matcher("12345");
boolean ok = m.matches();  // true

m.reset("12a34");
ok = m.matches();  // false

若只校验一次,可直接用 Pattern.matches(regex, input) 静态方法:

java
boolean valid = Pattern.matches("\\d{11}", "13812345678");  // 11 位数字

查找子串(find + group)

需要「在长文本里找出所有符合模式的片段」时,用 find() 循环,再用 group() 取当前匹配到的子串。

java
String text = "联系张三 13812345678 和李四 13987654321";
Pattern p = Pattern.compile("1[3-9]\\d{9}");
Matcher m = p.matcher(text);

while (m.find()) {
    String phone = m.group();  // 当前匹配到的完整子串
    System.out.println("找到手机号: " + phone);
}
// 输出: 找到手机号: 13812345678
//       找到手机号: 13987654321

分组与捕获

括号 () 形成捕获组,从左到右编号为 1、2、3…,group(0) 表示整个匹配,group(1)group(2) 为各括号对应的子串。常用于提取「日期中的年/月/日」「标签中的属性」等。

java
// 从 "2025-02-07" 中提取年、月、日
String line = "日期: 2025-02-07";
Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = p.matcher(line);

if (m.find()) {
    String year  = m.group(1);  // 2025
    String month = m.group(2);  // 02
    String day   = m.group(3);  // 07
    System.out.println(year + "年" + month + "月" + day + "日");
}

替换

  • replaceAll(replacement):把所有匹配到的子串替换为 replacement。
  • replaceFirst(replacement):只替换第一个匹配。

在 replacement 中可用 $1、$2 引用捕获组,$0 表示整段匹配。

java
String text = "张三 18 岁,李四 20 岁";
Pattern p = Pattern.compile("(\\d+)");
Matcher m = p.matcher(text);
// 给年龄加「岁」后缀(这里年龄本身没带「岁」,仅作替换示例)
String result = m.replaceAll("$1 岁");
// 若原文本是 "张三 18,李四 20",则 result = "张三 18 岁,李四 20 岁"

使用示例

示例 1:校验邮箱格式(简化版)

仅作格式校验,不保证该邮箱真实存在。

java
public static boolean looksLikeEmail(String input) {
    if (input == null || input.isBlank()) return false;
    // 简化规则:本地部分@域名.后缀
    String regex = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$";
    return Pattern.matches(regex, input);
}

// 使用
System.out.println(looksLikeEmail("user@example.com"));  // true
System.out.println(looksLikeEmail("invalid"));           // false

示例 2:提取所有数字

在一段文字中找出所有连续数字。

java
String text = "价格从 100 涨到 200,涨幅 100%。";
Pattern p = Pattern.compile("\\d+");
Matcher m = p.matcher(text);

while (m.find()) {
    System.out.println(m.group());
}
// 输出: 100  200  100

示例 3:隐藏手机号中间 4 位

用分组保留前 3 位和后 4 位,中间用 **** 替换。

java
String phone = "13812345678";
String masked = phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
System.out.println(masked);  // 138****5678

这里直接用了 String.replaceAll(regex, replacement),底层也是 Pattern/Matcher,适合单次替换。


进阶用法

非捕获组与标志

  • (?:...) 表示非捕获组:括号只用于分组或量词,不参与编号,group(1) 不会包含它。
  • Pattern.compile(regex, flags) 常用标志:
    • Pattern.CASE_INSENSITIVE:忽略大小写;
    • Pattern.MULTILINE^$ 按行匹配(否则仅对整串开头/结尾)。
java
// 忽略大小写匹配 "java"
Pattern p = Pattern.compile("java", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher("I love JAVA");
System.out.println(m.find());  // true
System.out.println(m.group()); // JAVA

贪婪与勉强量词

默认 *+?{n,m}贪婪的(尽可能多匹配)。在量词后加 ? 变为勉强(尽可能少匹配),例如 *?+?。在需要「最小匹配」时使用,可避免匹配过多。

java
// 从 <p>...</p> 中取内容,若用贪婪会匹配到最后一个 </p>
String html = "<p>first</p> and <p>second</p>";
Pattern greedy = Pattern.compile("<p>.*</p>");
Matcher mg = greedy.matcher(html);
if (mg.find()) {
    System.out.println("贪婪: " + mg.group());  // <p>first</p> and <p>second</p>
}

// 勉强:.*? 在遇到第一个 </p> 就停止
Pattern reluctant = Pattern.compile("<p>.*?</p>");
Matcher mr = reluctant.matcher(html);
if (mr.find()) {
    System.out.println("勉强: " + mr.group());  // <p>first</p>
}

注意事项

注意

Java 字符串中的反斜杠:正则里写 \d,在源码中必须写成 "\\d";正则里写 \\(一个反斜杠),源码中要写 "\\\\"。否则会报错或匹配不符合预期。

注意

matches() 与 find() 的区别matches() 要求整个目标串从开头到结尾都符合正则;find() 只要存在一段子串匹配即可,且可多次调用依次找下一个匹配。校验「整串格式」用 matches,查找「所有出现」用 find。

注意

灾难性回溯:某些正则(如 (a+)+ 配长串 aaaaaaaaX)在「不匹配」时可能产生大量回溯,导致 CPU 飙高甚至卡死。避免在重复量词上再嵌套重复量词;对用户输入做正则匹配时,可限制输入长度或使用超时/专用库。

提示

复用 PatternPattern 是线程安全的,建议对同一正则只 compile 一次,存成静态或成员变量反复使用;避免在循环里重复 Pattern.compile(...),以提升性能。


相关链接

基于 VitePress 构建