贪婪 vs. 非贪婪
在正则表达式中,量词(如 , , , {n,m})默认是“贪婪”的,这意味着它会尝试匹配尽可能多的字符。

而“非贪婪”模式则相反,它会尝试匹配尽可能少的字符。
贪婪模式(默认行为)
这是 Java 正则表达式的默认行为,当你使用一个量词时,它会从左到右扫描字符串,并尽可能多地“吃掉”字符,直到无法满足整个正则表达式为止,然后它会回退,尝试找到下一个可能的匹配。
示例:
假设我们有字符串 "<div>第一部分</div><div>第二部分</div>",我们想用正则表达式 "<div>.*</div>" 来匹配一个完整的 div 标签。

import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GreedyExample {
public static void main(String[] args) {
String text = "<div>第一部分</div><div>第二部分</div>";
// .* 会匹配尽可能多的字符
String regex = "<div>.*</div>";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
System.out.println("找到的匹配: " + matcher.group());
}
}
}
输出结果:
找到的匹配: <div>第一部分</div><div>第二部分</div>
为什么?
- 首先从第一个
<div>开始。 - 它贪婪地匹配
第一部分</div><div>第二部分中的所有字符。 - 它尝试匹配
</div>,成功了。 - 因为这是它能找到的最长的、满足条件的字符串,所以它就返回了整个字符串,这通常不是我们想要的结果。
非贪婪模式(如何实现)
要使用非贪婪模式,你只需要在任何一个量词(, , , {n,m})的后面加上一个问号 。
- : 非贪婪匹配前面的元素零次或多次
- : 非贪婪匹配前面的元素一次或多次
- : 非贪婪匹配前面的元素零次或一次
{n,m}?: 非贪婪匹配前面的元素至少n次,但不超过m次
示例:
现在我们修改上面的例子,将 改为 。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NonGreedyExample {
public static void main(String[] args) {
String text = "<div>第一部分</div><div>第二部分</div>";
// .*? 会匹配尽可能少的字符
String regex = "<div>.*?</div>";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
// 使用 while 循环找到所有匹配项
while (matcher.find()) {
System.out.println("找到的匹配: " + matcher.group());
}
}
}
输出结果:
找到的匹配: <div>第一部分</div>
找到的匹配: <div>第二部分</div>
为什么?
- 首先从第一个
<div>开始。 - 它是“懒惰”的,所以它先尝试匹配最少的字符,它先看 能否匹配零个字符。
- 匹配零个字符,那么正则表达式就变成了
<div></div>,这在文本中找不到。 - 匹配一个字符(
第),再检查<div>第</div>是否能找到,还是不行。 - 这个过程继续, 一次只匹配一个字符,直到它匹配了
第一部分。 - 整个正则表达式
<div>第一部分</div>在文本中找到了第一个匹配。 - 匹配完成后,引擎从匹配结束的位置继续向后搜索,用同样的非贪婪逻辑找到了第二个匹配。
更详细的对比
让我们用一个更简单的例子来彻底理解。
文本: xxxxxx
| 正则表达式 | 模式 | 匹配结果 | 解释 |
|---|---|---|---|
x* |
贪婪 | xxxxxx |
匹配所有可用的 x。 |
x*? |
非贪婪 | (空字符串) |
匹配零次,这是最少的可能。 |
x+ |
贪婪 | xxxxxx |
匹配所有可用的 x。 |
x+? |
非贪婪 | x |
匹配一次,这是最少的可能。 |
x?? |
贪婪 | x |
匹配一次( 优先匹配一次)。 |
x?? |
非贪婪 | (空字符串) |
匹配零次,这是最少的可能。 |
x{2,4} |
贪婪 | xxxxxx |
尽可能多地匹配,会尝试匹配4个,但文本有6个,所以匹配了4个。 |
x{2,4}? |
非贪婪 | xx |
尽可能少地匹配,满足至少2个即可。 |
实际应用场景
非贪婪模式在处理 HTML/XML 标签、日志文件、代码等多行结构时非常有用。
场景:从日志中提取每条错误信息
假设日志格式如下:
[INFO] 系统启动成功。
[ERROR] 磁盘空间不足。
[DEBUG] 加载配置文件 config.xml。
[ERROR] 数据库连接失败。
我们想提取所有 [ERROR] 开头和结尾的信息。
错误的贪婪做法:
String log = "[INFO] ...\n[ERROR] 磁盘空间不足,\n[DEBUG] ...\n[ERROR] 数据库连接失败。"; String regex = "\\[ERROR\\].*\\]"; // 错误:.* 会跨行匹配 // ... 代码 ... // 预期输出(但会出错): // [ERROR] 磁盘空间不足。 // [ERROR] 数据库连接失败。
这个正则表达式会因为 的贪婪性而匹配整个字符串,因为它会跨过换行符去寻找最后一个 ]。
正确的非贪婪做法:
String log = "[INFO] ...\n[ERROR] 磁盘空间不足,\n[DEBUG] ...\n[ERROR] 数据库连接失败。";
// 使用 [\s\S] 匹配包括换行符在内的所有字符,并使用非贪婪模式
String regex = "\\[ERROR\\][\\s\S]*?\\]";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(log);
while (matcher.find()) {
System.out.println(matcher.group());
}
输出结果:
[ERROR] 磁盘空间不足。
[ERROR] 数据库连接失败。
关键点:
- 默认不匹配换行符。 不会跨行。
- 如果需要跨行匹配,可以使用
[\s\S](匹配任何空白或非空白字符)或(?s).((?s)是一个内联标志,表示 匹配所有字符,包括换行符)。 - 在跨行匹配的场景下,几乎总是需要使用非贪婪模式 或 来精确地匹配你想要的结构单元。
| 特性 | 贪婪模式 | 非贪婪模式 |
|---|---|---|
| 行为 | 匹配尽可能多的字符 | 匹配尽可能少的字符 |
| 语法 | , , , {n,m} (默认) |
, , , {n,m}? |
| 适用场景 | 当你需要匹配一个整体,且内部结构复杂时(如匹配整个HTML文档) | 当你需要匹配一个由重复模式构成的多个独立单元时(如匹配多个独立的HTML标签) |
| 核心思想 | “给我所有能匹配的!” | “给我第一个能匹配的!” |
记住这个简单的规则:当你发现你的正则表达式匹配的范围比你期望的要大时,很可能就是贪婪模式在作祟,尝试在量词后面加上 ,把它变成非贪婪模式,通常就能解决问题。
