Java 正则表达式实用教程:从入门到实践
正则表达式(Regular Expression,简称 Regex 或 RegExp)是一种强大而灵活的文本处理工具,它使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。在处理字符串数据时,正则表达式提供了极高的便利性和效率,无论是数据验证、信息提取还是文本替换,它都扮演着重要的角色。
Java 语言通过 java.util.regex
包提供了对正则表达式的全面支持。本教程将带您深入理解 Java 中如何使用正则表达式,从基础概念到高级应用,并通过丰富的代码示例,帮助您掌握这项强大的技能。
1. 正则表达式基础:模式的语法
在深入 Java API 之前,我们首先需要了解正则表达式本身的语法。一个正则表达式就是一个模式(Pattern),它由一系列字符和特殊符号组成,用于定义要匹配的字符串的规则。
1.1 字面值字符 (Literal Characters)
大多数字符,如字母(a-z
, A-Z
)、数字(0-9
)、下划线(_
)等,都只匹配其本身的字面值。
例如:cat
会匹配字符串中的 “cat”。
1.2 特殊字符 (Metacharacters)
有一些字符在正则表达式中具有特殊含义,它们不是匹配自身,而是表示某种规则。常见的特殊字符包括:
.
:匹配除换行符\n
、回车符\r
以外的任何单个字符。^
:匹配行的开头。$
:匹配行的结尾。*
:匹配前一个元素零次或多次(贪婪模式)。+
:匹配前一个元素一次或多次(贪婪模式)。?
:匹配前一个元素零次或一次(贪婪模式)。{n}
:匹配前一个元素恰好 n 次。{n,}
:匹配前一个元素至少 n 次。{n,m}
:匹配前一个元素至少 n 次,但不超过 m 次。|
:或运算符,匹配|
前或后的表达式。()
:分组,将多个元素视为一个单元,并可以用于捕获匹配的文本。[]
:字符类,匹配方括号内的任何一个字符。\
:转义符,用于转义特殊字符,使其匹配字面值。例如\.
匹配字面值的点号,\\
匹配字面值的反斜杠。
示例:
a.c
:匹配 “abc”, “adc”, “axc” 等。^Hello
:匹配以 “Hello” 开头的行。World$
:匹配以 “World” 结尾的行。go*gle
:匹配 “ggle”, “gogle”, “googggle” 等。go+gle
:匹配 “gogle”, “googggle” 等,但不匹配 “ggle”。colou?r
:匹配 “color” 或 “colour”。a{3}
:匹配 “aaa”。a{2,}
:匹配 “aa”, “aaa”, “aaaa” 等。a{1,3}
:匹配 “a”, “aa”, “aaa”。cat|dog
:匹配 “cat” 或 “dog”。(ab)+
:匹配 “ab”, “abab”, “ababab” 等。[aeiou]
:匹配任何一个小写元音字母。
1.3 预定义字符类 (Predefined Character Classes)
为了方便,正则表达式提供了一些预定义的字符类,它们是常用字符集合的缩写:
\d
:匹配任何数字,等价于[0-9]
。\D
:匹配任何非数字字符,等价于[^0-9]
。\s
:匹配任何空白字符,包括空格、制表符\t
、换行符\n
、回车符\r
、换页符\f
等。\S
:匹配任何非空白字符,等价于[^\s]
。\w
:匹配任何单词字符,包括字母、数字和下划线,等价于[a-zA-Z0-9_]
。\W
:匹配任何非单词字符,等价于[^\w]
。
示例:
\d+
:匹配一个或多个数字。\s
:匹配一个空白字符。\w{5}
:匹配任意 5 个单词字符。
1.4 字符类 ([]
)
字符类 []
允许您定义一个字符集合,匹配其中的任何一个字符。
[abc]
:匹配 “a”, “b”, 或 “c”。[a-z]
:匹配任何一个小写字母。[A-Z]
:匹配任何一个大写字母。[a-zA-Z]
:匹配任何一个字母。[0-9]
:匹配任何一个数字 (等价于\d
)。[a-zA-Z0-9]
:匹配任何一个字母或数字。[^...]
:在[]
内使用^
表示取反,匹配不在集合内的任何字符。例如[^0-9]
匹配任何非数字字符 (等价于\D
)。
示例:
[aeiou]
:匹配任意一个小写元音。[0-9a-fA-F]
:匹配任何一个十六进制数字。[^.!?]
:匹配除点、问号、感叹号以外的任何字符。
1.5 边界匹配器 (Boundary Matchers)
边界匹配器用于指定匹配发生的位置:
^
:行的开头。$
:行的结尾。\b
:单词边界。单词边界是指一个单词字符\w
和一个非单词字符\W
之间的位置,或者字符串的开头/结尾与一个单词字符之间的位置。\B
:非单词边界。
示例:
^abc$
:匹配正好是 “abc” 的整行字符串。\bcat\b
:只匹配独立的 “cat” 单词,而不匹配 “catalog” 或 “concatenate” 中的 “cat”。
1.6 贪婪、勉强和独占量词 (Greedy, Reluctant, and Possessive Quantifiers)
默认情况下,量词 (*
, +
, ?
, {n,}
, {n,m}
) 是贪婪的。它们会尽可能多地匹配字符,直到无法匹配为止。
通过在量词后加上 ?
可以使其变为勉强的(或非贪婪的)。勉强量词会尽可能少地匹配字符。
通过在量词后加上 +
可以使其变为独占的(或强占的)。独占量词会尽可能多地匹配,但 不会回溯。
量词 | 贪婪 (Greedy) | 勉强 (Reluctant) | 独占 (Possessive) | 描述 |
---|---|---|---|---|
Zero or more | * |
*? |
*+ |
匹配零次或多次 |
One or more | + |
+? |
++ |
匹配一次或多次 |
Zero or one | ? |
?? |
?+ |
匹配零次或一次 |
Exactly n | {n} |
{n}? |
{n}+ |
匹配恰好 n 次 |
At least n | {n,} |
{n,}? |
{n,}+ |
匹配至少 n 次 |
Between n and m | {n,m} |
{n,m}? |
{n,m}+ |
匹配至少 n 次,但不超过 m 次 |
示例 (HTML 标签匹配):
考虑字符串 <p><b>Hello</b> world</p>
- 贪婪:
<.*>
匹配整个字符串<p><b>Hello</b> world</p>
。.
会一直匹配到最后一个>
。 - 勉强:
<.*?>
匹配<p>
,<b>
,</b>
,</p>
。.*?
只会匹配到 最近 的>
。
在大多数实际应用中,勉强量词在提取被特定标记分隔的内容时非常有用,比如 HTML/XML 标签内的内容。
2. Java 中的正则表达式 API (java.util.regex
)
Java 的 java.util.regex
包主要包含两个核心类:
Pattern
:表示一个编译后的正则表达式模式。它是线程安全的。Matcher
:一个匹配器对象,用于对输入字符串执行模式匹配操作。它不是线程安全的,每次匹配操作都需要一个新的Matcher
实例。
基本工作流程:
- 使用
Pattern.compile()
方法将正则表达式字符串编译成一个Pattern
对象。 - 使用
Pattern
对象的matcher()
方法创建一个Matcher
对象,并指定要匹配的输入字符串。 - 使用
Matcher
对象的方法执行各种匹配操作(查找、替换、验证等)。
2.1 Pattern
类
Pattern.compile(String regex)
:
这是最常用的方法,用于编译给定的正则表达式字符串。如果正则表达式语法错误,会抛出 PatternSyntaxException
。
Pattern.compile(String regex, int flags)
:
允许在编译时指定匹配标志,如:
* Pattern.CASE_INSENSITIVE
:忽略大小写匹配。
* Pattern.MULTILINE
:^
和 $
匹配行的开始和结束,而不仅仅是整个字符串的开始和结束。
* Pattern.DOTALL
:.
匹配包括换行符在内的任何字符。
* Pattern.COMMENTS
:允许在模式中包含空白和注释 (#
)。
* Pattern.UNICODE_CASE
:使用 Unicode 规则进行大小写折叠。
* Pattern.UNICODE_CHARACTER_CLASS
:启用 Unicode 版本的预定义字符类 (\d
, \s
, \w
等)。
示例:
“`java
import java.util.regex.Pattern;
// 编译一个简单的模式
Pattern pattern = Pattern.compile(“abc”);
// 编译一个忽略大小写的模式
Pattern patternIgnoreCase = Pattern.compile(“abc”, Pattern.CASE_INSENSITIVE);
“`
Pattern.matcher(CharSequence input)
:
创建一个 Matcher
对象,用于对给定的输入字符串执行匹配操作。
Pattern.matches(String regex, CharSequence input)
:
这是一个方便的静态方法,等价于 Pattern.compile(regex).matcher(input).matches()
。它尝试匹配整个输入字符串。适用于一次性的简单匹配,但不推荐用于频繁或复杂的匹配,因为它每次都会重新编译模式。
示例:
java
boolean isMatch = Pattern.matches("a*b", "aaaaab"); // true
2.2 Matcher
类
Matcher
类提供了多种执行匹配操作的方法:
查找匹配项:
boolean matches()
:尝试将整个输入序列与模式匹配。如果整个输入序列与模式匹配,则返回 true。boolean lookingAt()
:尝试将输入序列从开头开始匹配模式。如果输入序列的前缀与模式匹配,则返回 true。与matches()
不同,即使输入序列有剩余部分,只要开头匹配就返回 true。boolean find()
:尝试查找与模式匹配的输入序列的下一个子序列。如果找到,返回 true,并将匹配器移动到匹配子序列的末尾之后的位置。这是最常用的查找方法,用于遍历所有匹配项。boolean find(int start)
:从指定的索引位置开始搜索下一个匹配项。
获取匹配结果:
在调用 find()
或 matches()
或 lookingAt()
并返回 true
后,可以使用以下方法获取匹配结果:
int start()
:返回当前匹配项在输入字符串中的起始索引(包含)。int end()
:返回当前匹配项在输入字符串中的结束索引(不包含)。String group()
:返回当前匹配到的整个字符串(即 group 0)。String group(int group)
:返回给定捕获组(group 1, 2, …)匹配的子字符串。捕获组是通过在模式中使用()
定义的。int groupCount()
:返回此匹配器的模式中的捕获组数量。不包括 group 0。
替换操作:
String replaceAll(String replacement)
:替换输入序列中所有与模式匹配的子序列,用给定的替换字符串替换。String replaceFirst(String replacement)
:替换输入序列中第一个与模式匹配的子序列。Matcher appendReplacement(StringBuffer sb, String replacement)
:执行非终端替换。它将当前匹配项之前的内容追加到StringBuffer
中,然后将当前匹配项替换为替换字符串,并追加到StringBuffer
中。StringBuffer appendTail(StringBuffer sb)
:将输入序列中最后一个匹配项之后的所有剩余内容追加到StringBuffer
中。
appendReplacement
和 appendTail
通常一起使用,用于构建一个更灵活的替换逻辑,例如在替换字符串中包含捕获组的内容。
示例:基本查找与获取
“`java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexExample {
public static void main(String[] args) {
String text = “Java is fun. Java is powerful.”;
String regex = “Java”;
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
// 使用 find() 查找所有匹配项
System.out.println("查找所有 'Java':");
while (matcher.find()) {
System.out.println("找到匹配项: " + matcher.group() +
" 从索引 " + matcher.start() +
" 到 " + matcher.end());
}
// 重置 matcher 以进行新的匹配操作
matcher.reset();
// 使用 matches() 尝试完全匹配
System.out.println("\n尝试完全匹配 'Java is fun. Java is powerful.':");
boolean fullMatch = matcher.matches(); // 会返回 false,因为模式 "Java" 不匹配整个字符串
System.out.println("完全匹配? " + fullMatch);
// 使用 lookingAt() 尝试从开头匹配
matcher.reset();
System.out.println("\n尝试从开头匹配 'Java':");
boolean lookingAtMatch = matcher.lookingAt(); // 会返回 true
System.out.println("从开头匹配? " + lookingAtMatch);
if (lookingAtMatch) {
System.out.println("匹配到的开头部分: " + matcher.group());
}
}
}
“`
示例:使用捕获组
“`java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GroupExample {
public static void main(String[] args) {
String text = “My phone number is 123-456-7890 and her number is 987-654-3210.”;
// 匹配 xxx-yyy-zzzz 格式的电话号码
// () 创建捕获组
String regex = “(\d{3})-(\d{3})-(\d{4})”;
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
System.out.println("提取电话号码及其部分:");
while (matcher.find()) {
System.out.println("找到完整的号码: " + matcher.group(0)); // group(0) 是整个匹配项
System.out.println(" 区号 (group 1): " + matcher.group(1));
System.out.println(" 中间三位 (group 2): " + matcher.group(2));
System.out.println(" 后四位 (group 3): " + matcher.group(3));
System.out.println(" 匹配从索引 " + matcher.start() + " 到 " + matcher.end());
}
}
}
**输出示例:**
提取电话号码及其部分:
找到完整的号码: 123-456-7890
区号 (group 1): 123
中间三位 (group 2): 456
后四位 (group 3): 7890
匹配从索引 21 到 33
找到完整的号码: 987-654-3210
区号 (group 1): 987
中间三位 (group 2): 654
后四位 (group 3): 3210
匹配从索引 52 到 64
“`
示例:替换操作
“`java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ReplaceExample {
public static void main(String[] args) {
String text = “The price is $100 and the discount is $20.”;
// 匹配美元符号后跟着数字
String regex = “\$(\d+)”; // 使用 \$ 匹配字面值 $
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
// 使用 replaceAll 替换所有匹配项
String newTextAll = matcher.replaceAll("USD $1"); // $1 引用第一个捕获组 ($100 或 $20 中的数字部分)
System.out.println("替换所有: " + newTextAll); // 输出: The price is USD 100 and the discount is USD 20.
// 使用 replaceFirst 替换第一个匹配项 (需要新的 Matcher 实例或 reset())
matcher.reset(); // 重置 matcher
String newTextFirst = matcher.replaceFirst("Cost $1");
System.out.println("替换第一个: " + newTextFirst); // 输出: The price is Cost 100 and the discount is $20.
// 使用 appendReplacement 和 appendTail 进行更复杂的替换
matcher.reset();
StringBuffer sb = new StringBuffer();
System.out.println("\n使用 appendReplacement/appendTail:");
while (matcher.find()) {
// 在匹配项前添加前缀和后缀,并引用捕获组
matcher.appendReplacement(sb, "Price: $" + matcher.group(1) + "!");
System.out.println("After appendReplacement: " + sb.toString());
}
matcher.appendTail(sb); // 将最后一个匹配项之后的内容追加
System.out.println("Final result with appendTail: " + sb.toString());
}
}
**输出示例:**
替换所有: The price is USD 100 and the discount is USD 20.
替换第一个: The price is Cost 100 and the discount is $20.
使用 appendReplacement/appendTail:
After appendReplacement: The price is Price: $100!
After appendReplacement: The price is Price: $100! and the discount is Price: $20!
Final result with appendTail: The price is Price: $100! and the discount is Price: $20!.
``
appendReplacement
请注意会在每次
find成功后执行,它将上次匹配的结束位置到当前匹配开始位置之间的文本以及当前匹配被替换后的文本追加到 StringBuffer。
appendTail` 处理最后一个匹配结束位置到字符串结尾的文本。
2.3 String
类中的正则表达式方法
Java 的 String
类也提供了几个方便的使用正则表达式的方法,它们内部也是调用 java.util.regex
包:
boolean matches(String regex)
:尝试将整个字符串与给定的正则表达式匹配。等同于Pattern.matches(regex, this)
。String replaceAll(String regex, String replacement)
:使用给定的替换项替换字符串中所有与给定正则表达式匹配的子字符串。String replaceFirst(String regex, String replacement)
:使用给定的替换项替换字符串中第一个与给定正则表达式匹配的子字符串。String[] split(String regex)
:根据匹配给定正则表达式的子字符串,将字符串分割成一个字符串数组。String[] split(String regex, int limit)
:根据匹配给定正则表达式的子字符串,将字符串分割成最多 limit 个字符串。
示例:
“`java
public class StringRegexMethods {
public static void main(String[] args) {
String text = “apple,banana,cherry”;
String numbers = “12345”;
// matches
boolean isAllDigits = numbers.matches("\\d+"); // true
System.out.println(numbers + " is all digits? " + isAllDigits);
// replaceAll
String replacedText = text.replaceAll(",", "-"); // apple-banana-cherry
System.out.println("Replaced commas: " + replacedText);
// split
String[] fruits = text.split(","); // {"apple", "banana", "cherry"}
System.out.println("Split fruits:");
for (String fruit : fruits) {
System.out.println(fruit);
}
// split with limit
String textWithMultipleCommas = "a,,b,,,c";
String[] parts = textWithMultipleCommas.split(",", 3); // {"a", "", "b,,,c"}
System.out.println("\nSplit with limit 3:");
for (String part : parts) {
System.out.println(part);
}
String[] partsUnlimited = textWithMultipleCommas.split(","); // {"a", "", "b", "", "", "c"}
System.out.println("\nSplit with no limit:");
for (String part : partsUnlimited) {
System.out.println(part);
}
}
}
``
String
**注意:**类的这些方法虽然使用方便,但每次调用都会在内部编译正则表达式并创建 Matcher 对象。如果需要在循环中或频繁地执行相同的匹配操作,使用
Pattern和
Matcher` 对象会更高效,因为模式只需要编译一次。
3. 常见实用场景及示例
3.1 验证输入
正则表达式常用于验证用户输入的格式,如邮箱、电话号码、URL 等。
示例:简单的邮箱格式验证
“`java
import java.util.regex.Pattern;
public class EmailValidator {
// 这是一个简化的邮箱正则表达式,实际场景可能需要更复杂的规则
private static final String EMAIL_REGEX =
“^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$”; // 注意对特殊字符 . 的转义 \.
private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX);
public static boolean isValidEmail(String email) {
if (email == null) {
return false;
}
return EMAIL_PATTERN.matcher(email).matches(); // 使用 matches() 验证整个字符串
}
public static void main(String[] args) {
System.out.println("[email protected] is valid? " + isValidEmail("[email protected]")); // true
System.out.println("[email protected] is valid? " + isValidEmail("[email protected]")); // true
System.out.println("test@example is valid? " + isValidEmail("test@example")); // false
System.out.println("test example.com is valid? " + isValidEmail("test example.com")); // false
System.out.println("[email protected] is valid? " + isValidEmail("[email protected]")); // false
}
}
“`
3.2 搜索和提取信息
从大段文本中提取特定格式的数据是正则表达式的常见用途。
示例:从日志行中提取时间戳和消息
假设日志格式是 [YYYY-MM-DD HH:MM:SS] Message content...
“`java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class LogParser {
// 匹配 [YYYY-MM-DD HH:MM:SS] 后面的任意内容
private static final String LOG_REGEX = “\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (.*)”;
private static final Pattern LOG_PATTERN = Pattern.compile(LOG_REGEX);
public static void parseLogLine(String logLine) {
Matcher matcher = LOG_PATTERN.matcher(logLine);
if (matcher.matches()) { // 使用 matches() 确保整行符合模式
String timestamp = matcher.group(1); // 捕获组 1: 时间戳部分
String message = matcher.group(2); // 捕获组 2: 消息内容部分
System.out.println("Timestamp: " + timestamp);
System.out.println("Message: " + message);
} else {
System.out.println("Log line format mismatch: " + logLine);
}
}
public static void main(String[] args) {
String log1 = "[2023-10-27 10:30:00] Server started.";
String log2 = "[2023-10-27 10:31:15] User logged in: Alice.";
String log3 = "Invalid log format line.";
parseLogLine(log1);
System.out.println("---");
parseLogLine(log2);
System.out.println("---");
parseLogLine(log3);
}
}
“`
3.3 文本替换
替换文本中符合特定模式的部分。
示例:将文本中的所有电子邮件地址替换为占位符
“`java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TextSanitizer {
private static final String EMAIL_REGEX = “[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}”; // 不使用 ^$ 因为我们要查找并替换,而不是验证整行
private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX);
public static String hideEmails(String text) {
if (text == null) {
return null;
}
Matcher matcher = EMAIL_PATTERN.matcher(text);
// 使用 replaceAll 替换找到的邮箱地址为 "[EMAIL_HIDDEN]"
return matcher.replaceAll("[EMAIL_HIDDEN]");
}
public static void main(String[] args) {
String text = "Please contact support at [email protected] or [email protected] for help.";
String sanitizedText = hideEmails(text);
System.out.println("Original: " + text);
System.out.println("Sanitized: " + sanitizedText);
}
}
“`
3.4 分割字符串
使用正则表达式作为分隔符来分割字符串。
示例:根据多种分隔符分割字符串 (逗号、分号、空格)
“`java
import java.util.regex.Pattern;
public class StringSplitter {
public static void main(String[] args) {
String text = “apple,banana;cherry grape orange”;
// 使用正则表达式 “[;,\s]+” 匹配一个或多个逗号、分号或空白字符作为分隔符
String[] items = text.split(“[;,\s]+”);
System.out.println("Split items:");
for (String item : items) {
System.out.println(item);
}
}
}
“`
4. 高级概念
4.1 标志 (Flags)
前面提到,可以在编译 Pattern 时使用标志来修改匹配行为。
java
// 忽略大小写和多行模式
Pattern pattern = Pattern.compile("^(hello)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
4.2 非捕获组 (Non-capturing Groups)
使用 (?:...)
创建非捕获组。非捕获组用于分组,但不创建捕获组,因此不会有对应的 group(int)
方法返回其匹配的内容,也不会计入 groupCount()
。这在只需要分组而不需要提取匹配内容时非常有用,可以略微提高性能。
示例:
(abc)+
:匹配 “abc”, “abcabc” 等,并创建一个捕获组abc
。(?:abc)+
:匹配 “abc”, “abcabc” 等,但没有捕获组。
“`java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NonCapturingGroupExample {
public static void main(String[] args) {
String text = “prefix_item1-item2”;
// (?:prefix_)? 非捕获组,匹配可选的 “prefix_”
// ([a-zA-Z0-9]+) 捕获组 1: 匹配单词字符
// (-[a-zA-Z0-9]+)? 捕获组 2: 匹配可选的 “-item” 部分 (勉强匹配)
String regex = “(?:prefix_)?([a-zA-Z0-9]+)(-[a-zA-Z0-9]+)?”;
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
if (matcher.matches()) { // 尝试完全匹配
System.out.println("Group count: " + matcher.groupCount()); // 输出 2 (不包括非捕获组和group 0)
System.out.println("Full match (group 0): " + matcher.group(0));
System.out.println("First item (group 1): " + matcher.group(1));
// group(2) 可能匹配 "-item2" 或 null
System.out.println("Remaining items group (group 2): " + matcher.group(2));
}
String text2 = "itemA-itemB-itemC";
Matcher matcher2 = pattern.matcher(text2);
if (matcher2.matches()) {
System.out.println("\nGroup count: " + matcher2.groupCount()); // 输出 2
System.out.println("Full match (group 0): " + matcher2.group(0));
System.out.println("First item (group 1): " + matcher2.group(1));
System.out.println("Remaining items group (group 2): " + matcher2.group(2)); // 匹配 "-itemC" (勉强匹配)
}
}
}
“`
4.3 零宽度断言 (Lookarounds)
零宽度断言匹配位置,而不是字符。它们不会消耗字符,只检查匹配位置是否符合某个条件。
(?=...)
:正向前瞻 (Positive Lookahead)。断言当前位置后面紧跟着...
。(?!...)
:负向前瞻 (Negative Lookahead)。断言当前位置后面 不 紧跟着...
。(?<=...)
:正向后顾 (Positive Lookbehind)。断言当前位置前面紧跟着...
。(?<!...)
:负向后顾 (Negative Lookbehind)。断言当前位置前面 不 紧跟着...
。
示例:
Java(?=Script)
:匹配 “JavaScript” 中的 “Java”,但不包括 “Script”。Java(?!Script)
:匹配后面 不是 “Script” 的 “Java”。(?<=http://)www
:匹配前面是 “http://” 的 “www”。(?<!https://)www
:匹配前面 不是 “https://” 的 “www”。
“`java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class LookaroundExample {
public static void main(String[] args) {
String text = “cat and catalog”;
// 正向前瞻: 匹配后面跟着 "alog" 的 "cat"
Pattern pattern1 = Pattern.compile("cat(?=alog)");
Matcher matcher1 = pattern1.matcher(text);
System.out.println("Positive Lookahead (cat(?=alog)):");
while (matcher1.find()) {
System.out.println("Found: " + matcher1.group() + " at index " + matcher1.start()); // 输出 Found: cat at index 8
}
// 负向前瞻: 匹配后面 *不是* 跟着 "alog" 的 "cat"
Pattern pattern2 = Pattern.compile("cat(?!alog)");
Matcher matcher2 = pattern2.matcher(text);
System.out.println("\nNegative Lookahead (cat(?!alog)):");
while (matcher2.find()) {
System.out.println("Found: " + matcher2.group() + " at index " + matcher2.start()); // 输出 Found: cat at index 0
}
String priceText = "The price is $100.";
// 正向后顾: 匹配前面是 "$" 的数字序列
Pattern pattern3 = Pattern.compile("(?<=\$)\\d+"); // 注意对 $ 的转义,以及后顾内部不能包含数量不确定的匹配(如* + ?)
Matcher matcher3 = pattern3.matcher(priceText);
System.out.println("\nPositive Lookbehind ((?<=\$)\\d+):");
while (matcher3.find()) {
System.out.println("Found: " + matcher3.group() + " at index " + matcher3.start()); // 输出 Found: 100 at index 13
}
}
}
``
*
**注意:** Java 正向后顾和负向后顾中的模式必须是固定长度或有限长度的(虽然 Java 8+ 对此限制有所放宽,允许有限量词,但无限量词如,
+` 仍然是不允许的)。
5. 性能考虑和最佳实践
- 编译 Pattern: 如果您需要多次使用同一个正则表达式,务必将其编译成
Pattern
对象,而不是每次都使用String.matches()
等方法。Pattern
对象是线程安全的,可以重复使用。Matcher
对象不是线程安全的,每次新的匹配操作(或在同一匹配器上处理不同的输入/进行多次find
调用之前)通常需要调用matcher.reset()
或获取新的 Matcher。 - 避免创建不必要的 Matcher: 如果只是简单地判断整个字符串是否匹配某个模式,使用
Pattern.matches(regex, input)
或input.matches(regex)
静态/实例方法可能更简洁,但这牺牲了模式编译的复用性。 - 简化正则表达式: 过于复杂或模糊的正则表达式可能导致性能下降甚至回溯陷阱(Catastrophic Backtracking)。使用勉强量词
*?
,+?
,??
有助于避免不必要的贪婪匹配和回溯。 - 使用字符类代替或:
[abc]
通常比a|b|c
更高效且清晰。 - 使用预定义字符类:
\d
,\s
,\w
通常比[0-9]
,[\t\n\x0B\f\r ]
,[a-zA-Z_0-9]
更简洁。 - 考虑字符串的替代方法: 对于简单的子字符串查找或替换,
String.contains()
,String.indexOf()
,String.replace()
等方法可能比正则表达式更快。只有在需要模式匹配的强大功能时才使用正则表达式。 - 转义特殊字符: 在 Java 字符串中表示正则表达式时,反斜杠
\
本身是 Java 字符串的转义符。所以,要在正则表达式中匹配字面值的反斜杠\
,您需要在 Java 字符串中写成\\
。同理,要匹配字面值的点.
,在正则表达式中是\.
,在 Java 字符串中需要写成\\.
。这是新手常犯的错误。 - 使用在线工具测试: 在编写复杂的正则表达式时,强烈推荐使用在线正则表达式测试工具来验证您的模式是否按预期工作。
6. 总结
Java 的 java.util.regex
包提供了一个强大且完整的正则表达式处理框架。掌握 Pattern
和 Matcher
类的使用是关键。通过理解正则表达式的语法和 Java API 的工作方式,您可以有效地进行文本的搜索、验证、提取和替换。从简单的验证到复杂的日志解析,正则表达式都是一个不可或缺的工具。
虽然正则表达式功能强大,但也可能变得复杂难以阅读和调试。因此,编写清晰、简洁的模式,并进行充分的测试是至关重要的。对于性能敏感的应用,考虑模式编译和量词的选择也是必要的。
希望本教程能帮助您建立坚实的 Java 正则表达式基础,并在实际开发中灵活运用它来解决各种字符串处理问题。多加练习,您将能驾驭这项强大的技能!