Java 正则表达式实战指南:基础篇
引言:为什么需要正则表达式?
在软件开发的世界里,处理文本数据是一项司空见惯的任务。无论是验证用户输入、解析日志文件、提取网页信息,还是进行复杂的文本替换,我们都离不开强大的文本处理工具。虽然 Java 提供了丰富的字符串操作方法(如 substring(), indexOf(), startsWith(), endsWith(), replace() 等),但在面对复杂、灵活的模式匹配和文本操作需求时,这些基础方法往往显得力不从心,代码也会变得冗长且难以维护。
这时,正则表达式(Regular Expression,简称 Regex 或 RegExp)便闪亮登场了。正则表达式是一种使用单个字符串来描述、匹配一系列符合某个句法规则的字符串的“描述性语言”。它极其强大和灵活,能够以简洁的方式表达复杂的文本匹配逻辑。
Java 自 JDK 1.4 起,通过 java.util.regex 包提供了对正则表达式的全面支持。掌握 Java 正则表达式,能显著提升你处理文本数据的效率和代码质量,是 Java 开发者必备的核心技能之一。
本指南(基础篇)将带你从零开始,系统学习 Java 正则表达式的基础知识、核心 API 以及常见应用场景,并通过丰富的实例让你快速上手,为后续深入学习和实战打下坚实的基础。
一、 正则表达式基础概念:模式的构建块
正则表达式本身就是一种微型语言,有其特定的语法和元字符(Metacharacters)。在深入 Java API 之前,我们必须先理解这些构建块。
1.1 普通字符(Literal Characters)
最简单的正则表达式由普通字符组成,它们只匹配自身。例如,正则表达式 hello 只会精确匹配字符串 “hello”。
1.2 元字符(Metacharacters)
元字符是正则表达式中有特殊含义的字符,它们不代表自身,而是用于表达某种匹配规则。常见的元字符包括:
.(点号): 匹配除换行符\n(在某些模式下可能包括)之外的任何单个字符。\(反斜杠): 转义字符,用于将后续的元字符转义为普通字符,或将普通字符转义为特殊含义(如\d)。[](方括号): 定义字符类(Character Class),匹配方括号中列出的任意一个字符。[abc]:匹配 ‘a’、’b’ 或 ‘c’。[^abc]:否定字符类,匹配除 ‘a’、’b’、’c’ 之外的任何字符。[a-zA-Z]:范围表示,匹配任何一个小写或大写字母。[0-9]:匹配任何一个数字。
()(圆括号): 定义分组(Grouping)和捕获(Capturing)。- 将多个字符组合成一个单元,以便应用量词或进行捕获。例如
(abc)+匹配一个或多个 “abc”。 - 捕获匹配到的子字符串,以便后续引用或提取。
- 将多个字符组合成一个单元,以便应用量词或进行捕获。例如
|(竖线): 表示或(Alternation),匹配|左边或右边的表达式。例如cat|dog匹配 “cat” 或 “dog”。^(脱字符):- 在方括号
[]内开头时,表示否定,如[^abc]。 - 在正则表达式开头时,匹配输入的开头。例如
^Start匹配以 “Start” 开头的字符串。
- 在方括号
$(美元符): 匹配输入的结尾。例如end$匹配以 “end” 结尾的字符串。- 量词(Quantifiers): 指定前面的元素(字符、字符类或分组)必须出现多少次。
*(星号): 匹配前一个元素零次或多次。等价于{0,}。例如a*匹配 “”、”a”、”aa” 等。+(加号): 匹配前一个元素一次或多次。等价于{1,}。例如a+匹配 “a”、”aa” 等,但不匹配 “”。?(问号): 匹配前一个元素零次或一次。等价于{0,1}。例如colou?r匹配 “color” 或 “colour”。{n}: 匹配前一个元素恰好 n 次。例如\d{3}匹配恰好三个数字。{n,}: 匹配前一个元素至少 n 次。例如\d{3,}匹配三个或更多数字。{n,m}: 匹配前一个元素至少 n 次,但不超过 m 次。例如\d{3,5}匹配三到五个数字。
1.3 预定义字符类(Predefined Character Classes)
为了方便,正则表达式提供了一些常用的预定义字符类:
\d: 匹配任何一个数字字符。等价于[0-9]。\D: 匹配任何一个非数字字符。等价于[^0-9]。\s: 匹配任何一个空白字符(包括空格、制表符\t、换页符\f、回车符\r、换行符\n等)。等价于[ \t\n\x0B\f\r]。\S: 匹配任何一个非空白字符。等价于[^\s]。\w: 匹配任何一个单词字符(字母、数字或下划线)。等价于[a-zA-Z_0-9]。\W: 匹配任何一个非单词字符。等价于[^\w]。
注意: 在 Java 字符串字面量中,反斜杠 \ 本身就是一个转义字符。因此,要在 Java 字符串中表示正则表达式的 \,需要写成 \\。例如,正则表达式的 \d 在 Java 字符串中要写成 "\\d"。
1.4 边界匹配器(Boundary Matchers)
^: 匹配行的开头。$: 匹配行的结尾。\b: 匹配单词边界。单词边界是指单词字符\w和非单词字符\W之间的位置,或者字符串的开头/结尾与单词字符之间的位置。例如\bcat\b匹配独立的单词 “cat”,但不匹配 “category” 中的 “cat”。\B: 匹配非单词边界。\A: 匹配输入的开头(不受多行模式影响)。\Z: 匹配输入的结尾,除了最后的终止符(如果有的话)。\z: 匹配输入的结尾(绝对结尾)。
1.5 贪婪、懒惰与独占量词(Greedy, Reluctant/Lazy, Possessive Quantifiers)
默认情况下,量词是 贪婪 的(Greedy),它们会尽可能多地匹配字符。例如,对于字符串 “abbbbbbc”,正则表达式 ab+ 会匹配 “abbbbbb”,因为 b+ 会尽可能多地匹配 ‘b’。
通过在量词后面添加 ?,可以使其变为 懒惰 或 非贪婪 的(Reluctant / Lazy),它们会尽可能少地匹配字符,但仍然确保整个模式能匹配成功。对于 “abbbbbbc”,ab+? 会匹配 “ab”,因为 b+? 只匹配了一个 ‘b’ 就满足了 +(一次或多次)的要求。
通过在量词后面添加 +,可以使其变为 独占 的(Possessive),它们会尽可能多地匹配字符,并且不会回溯(give back characters)。这通常用于性能优化或防止意外匹配。对于 “abbbbbbc”,ab++ 也会匹配 “abbbbbb”。独占模式与贪婪模式的主要区别在于回溯行为,这在更复杂的模式中会体现出来。
| 类型 | 语法 | 描述 | 示例 (X代表前面的元素) |
|---|---|---|---|
| 贪婪 | *, +, ?, {n,m} |
尽可能多地匹配 | X*, X+, X?, X{n,m} |
| 懒惰/非贪婪 | *?, +?, ??, {n,m}? |
尽可能少地匹配 | X*?, X+?, X??, X{n,m}? |
| 独占 | *+, ++, ?+, {n,m}+ |
尽可能多地匹配,且不回溯(吞噬匹配) | X*+, X++, X?+, X{n,m}+ |
二、 Java java.util.regex 包核心类
Java 的正则表达式功能主要由 java.util.regex 包下的两个核心类提供:Pattern 和 Matcher。
2.1 Pattern 类
Pattern 对象是正则表达式的编译表示。一个正则表达式字符串首先需要被编译成一个 Pattern 实例,然后才能用于匹配。将正则表达式编译为 Pattern 对象可以提高匹配性能,特别是当一个模式需要被多次使用时。
创建 Pattern 对象:
“`java
import java.util.regex.Pattern;
// 正则表达式字符串
String regex = “\b[A-Za-z]+\b”; // 匹配独立的单词
// 编译正则表达式
Pattern pattern = Pattern.compile(regex);
// 也可以在编译时指定标志 (Flags)
// Pattern patternIgnoreCase = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
“`
Pattern.compile() 方法接受一个正则表达式字符串作为参数,并返回一个 Pattern 对象。如果正则表达式语法有误,它会抛出 PatternSyntaxException。
编译标志(Flags):
Pattern.compile() 方法可以接受一个可选的 int 类型的 flags 参数,用于修改匹配行为。常用的标志包括:
Pattern.CASE_INSENSITIVE(或Pattern.UNICODE_CASE): 启用不区分大小写的匹配。Pattern.MULTILINE: 启用多行模式。在这种模式下,^和$会匹配行的开头和结尾,而不仅仅是整个输入的开头和结尾。Pattern.DOTALL: 启用点号全匹配模式。在这种模式下,元字符.会匹配包括行终止符在内的任何字符。Pattern.UNICODE_CHARACTER_CLASS: 启用基于 Unicode 的预定义字符类和 POSIX 字符类。Pattern.COMMENTS: 允许在模式中使用空白和注释(以#开头到行尾)。
可以同时使用多个标志,通过位或运算符 | 连接:
java
Pattern patternMulti = Pattern.compile("^\\w+$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
Pattern 类的常用方法:
static Pattern compile(String regex): 编译给定的正则表达式。static Pattern compile(String regex, int flags): 编译给定的正则表达式,并使用指定的标志。Matcher matcher(CharSequence input): 创建一个Matcher对象,用于在给定的输入CharSequence上执行匹配操作。String pattern(): 返回编译该模式的原始正则表达式字符串。int flags(): 返回该模式的匹配标志。String[] split(CharSequence input): 根据模式拆分给定的输入序列。static boolean matches(String regex, CharSequence input): 一个便捷的静态方法,用于快速检查整个输入序列是否匹配给定的正则表达式。注意: 此方法内部会编译模式并创建匹配器,如果需要多次使用同一模式,建议先编译Pattern对象。
2.2 Matcher 类
Matcher 对象是解释模式并对输入字符串执行匹配操作的引擎。Matcher 对象是通过 Pattern 对象的 matcher() 方法创建的,它与特定的输入 CharSequence(如 String, StringBuilder, CharBuffer)相关联。
创建 Matcher 对象:
“`java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
String text = “Hello World! This is Java Regex Example 123.”;
String regex = “\b\w+\b”; // 匹配单词
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text); // 将模式应用于文本
“`
Matcher 类的核心方法:
Matcher 提供了多种执行匹配操作的方法:
-
查找方法 (Finding Methods):
boolean find(): 尝试查找输入序列中与模式匹配的下一个子序列。每次调用find()都会从上一次匹配结束的位置之后开始搜索。如果找到匹配项,则返回true,否则返回false。通常与while循环结合使用来查找所有匹配项。boolean find(int start): 重置此匹配器,然后尝试从指定的索引开始查找下一个匹配项。boolean lookingAt(): 尝试从输入序列的开头开始匹配模式。它不要求整个输入序列都匹配,只要开头部分匹配即可。返回true或false。boolean matches(): 尝试将整个输入序列与模式进行匹配。只有当整个输入完全匹配模式时才返回true。
-
获取匹配结果的方法 (Group Methods):
这些方法通常在find(),lookingAt(),matches()返回true后调用。String group(): 返回由上一次匹配操作(如find())匹配到的整个子序列。等价于group(0)。String group(int group): 返回由上一次匹配操作中指定编号的捕获组匹配到的子序列。组号从 1 开始。组 0 代表整个匹配。String group(String name): (需要命名捕获组(?<name>...))返回指定名称的捕获组匹配到的子序列。int groupCount(): 返回此匹配器模式中定义的捕获组数量(不包括组 0)。int start(): 返回上一次匹配到的子序列的起始索引。等价于start(0)。int start(int group): 返回上一次匹配中指定捕获组匹配到的子序列的起始索引。int end(): 返回上一次匹配到的子序列的最后一个字符之后的偏移量(即结束索引 + 1)。等价于end(0)。int end(int group): 返回上一次匹配中指定捕获组匹配到的子序列的最后一个字符之后的偏移量。
-
替换方法 (Replacement Methods):
Matcher appendReplacement(StringBuffer sb, String replacement): 将从上一次匹配结束(或输入开头)到当前匹配开始之间的文本,以及指定的替换字符串,追加到StringBuffer中。replacement字符串可以包含对捕获组的反向引用 ($1,$2等)。注意: 此方法需要与appendTail结合使用。StringBuffer appendTail(StringBuffer sb): 将输入序列中从最后一个匹配结束位置到输入末尾的剩余部分追加到StringBuffer中。String replaceAll(String replacement): 将输入序列中所有匹配模式的子序列替换为给定的替换字符串。返回替换后的新字符串。String replaceFirst(String replacement): 将输入序列中第一个匹配模式的子序列替换为给定的替换字符串。返回替换后的新字符串。
-
重置方法 (Reset Methods):
Matcher reset(): 重置匹配器状态,清除匹配结果,并将搜索位置重置到输入的开头。Matcher reset(CharSequence input): 重置匹配器,使用新的输入序列,并清除状态。
典型的查找所有匹配项的循环:
“`java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexFindExample {
public static void main(String[] args) {
String text = “Order P123, Order Q456, Item X789”;
String regex = “Order ([A-Z]\d{3})”; // 匹配 “Order ” 后跟一个大写字母和三个数字,并捕获编号
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
System.out.println("Finding all matches for pattern: " + regex);
System.out.println("Input text: " + text);
// 使用 while 循环和 find() 查找所有匹配项
while (matcher.find()) {
String fullMatch = matcher.group(); // 获取整个匹配 "Order P123"
String orderId = matcher.group(1); // 获取第一个捕获组的内容 "P123"
int startIndex = matcher.start(); // 获取匹配的开始索引
int endIndex = matcher.end(); // 获取匹配的结束索引 + 1
System.out.println("--------------------");
System.out.println("Found match: '" + fullMatch + "'");
System.out.println(" Order ID (Group 1): '" + orderId + "'");
System.out.println(" Start index: " + startIndex);
System.out.println(" End index: " + endIndex);
}
}
}
“`
输出:
“`
Finding all matches for pattern: Order ([A-Z]\d{3})
Input text: Order P123, Order Q456, Item X789
Found match: ‘Order P123’
Order ID (Group 1): ‘P123’
Start index: 0
End index: 10
Found match: ‘Order Q456’
Order ID (Group 1): ‘Q456’
Start index: 12
End index: 22
“`
2.3 String 类对正则表达式的支持
Java 的 String 类也提供了一些便捷的方法来直接使用正则表达式,它们内部封装了 Pattern 和 Matcher 的操作:
boolean matches(String regex): 判断 整个 字符串是否匹配给定的正则表达式。等价于Pattern.matches(regex, this)。String replaceAll(String regex, String replacement): 将字符串中所有匹配regex的子串替换为replacement。String replaceFirst(String regex, String replacement): 将字符串中第一个匹配regex的子串替换为replacement。String[] split(String regex): 根据匹配regex的分隔符来拆分字符串。String[] split(String regex, int limit): 带限制次数的拆分。
使用场景:
- 如果只需要进行一次性的、简单的匹配或替换操作,并且对性能要求不高,使用
String类的方法更简洁。 - 如果需要多次使用同一个正则表达式,或者需要更复杂的控制(如查找所有匹配、访问捕获组、使用编译标志等),则应该显式使用
Pattern和Matcher,因为预编译Pattern对象可以获得更好的性能。
三、 Java 正则表达式实战案例
下面通过一些常见的实例来演示 Java 正则表达式的应用。
案例一:验证邮箱格式(简化版)
邮箱格式复杂多样,一个完全符合 RFC 标准的正则表达式非常复杂。这里提供一个常用的简化版:
“`java
import java.util.regex.Pattern;
public class EmailValidator {
// 简化版邮箱正则:用户名@域名
// 用户名:字母、数字、下划线、点、减号,点和减号不能连续出现,不能在开头结尾
// 域名:字母、数字、减号,点分隔,顶级域名至少2个字母
private static final String EMAIL_REGEX =
“^[a-zA-Z0-9_]+([-.][a-zA-Z0-9_]+)@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)(\.[a-zA-Z]{2,})$”;
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]: " + isValidEmail("[email protected]")); // true
System.out.println("[email protected]: " + isValidEmail("[email protected]")); // true
System.out.println("[email protected]: " + isValidEmail("[email protected]")); // true
System.out.println("[email protected]: " + isValidEmail("[email protected]")); // false (域名部分不合法)
System.out.println("[email protected]: " + isValidEmail("[email protected]")); // false (用户名开头不能是.)
System.out.println("test@example: " + isValidEmail("test@example")); // false (缺少顶级域名)
System.out.println("[email protected]: " + isValidEmail("[email protected]")); // false (域名点连续)
System.out.println("[email protected]: " + isValidEmail("[email protected]")); // false (用户名点连续)
}
}
“`
案例二:提取字符串中的所有数字
“`java
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ExtractNumbers {
public static List
List
// \d+ 匹配一个或多个数字
// \b 确保是独立的数字(或与其他非单词字符相邻),避免匹配单词内的数字如 “v1”
Pattern pattern = Pattern.compile(“\b\d+\b”);
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
numbers.add(matcher.group());
}
return numbers;
}
public static void main(String[] args) {
String data = "Invoice date: 2023-10-27, Amount: 1500, Quantity: 30, Discount: 50. SKU: P98765";
List<String> foundNumbers = findAllNumbers(data);
System.out.println("Found numbers: " + foundNumbers); // Output: Found numbers: [2023, 10, 27, 1500, 30, 50, 98765]
}
}
“`
案例三:替换手机号中间四位为星号
“`java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MaskPhoneNumber {
public static String maskMiddleDigits(String phoneNumber) {
if (phoneNumber == null || phoneNumber.length() != 11 || !phoneNumber.matches(“\d{11}”)) {
return “Invalid phone number”; // 或者返回原号码,或抛异常
}
// 正则:(组1: 前3位数字)(\d{4})(组3: 后4位数字)
// 使用 $1*$3 进行替换
String regex = “(\d{3})\d{4}(\d{4})”;
return phoneNumber.replaceAll(regex, “$1*$3″);
/*
// 或者使用 Pattern 和 Matcher (更底层,但这里 replaceAll 更简洁)
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(phoneNumber);
if (matcher.matches()) { // 因为要替换整个串,用 matches()
// 获取捕获组
String prefix = matcher.group(1);
String suffix = matcher.group(3);
return prefix + "****" + suffix;
}
return "Masking failed"; //理论上如果上面检查通过,这里总能匹配
*/
}
public static void main(String[] args) {
String phone = "13812345678";
String maskedPhone = maskMiddleDigits(phone);
System.out.println("Original: " + phone); // Original: 13812345678
System.out.println("Masked: " + maskedPhone); // Masked: 138****5678
String invalidPhone = "12345";
System.out.println("Invalid: " + maskMiddleDigits(invalidPhone)); // Invalid: Invalid phone number
}
}
“`
案例四:按多种分隔符拆分字符串
“`java
import java.util.Arrays;
public class SplitExample {
public static void main(String[] args) {
String data = “apple,banana;orange pear\tgrape”;
// 按逗号、分号、或一个或多个空白字符拆分
String regex = “[,;\s]+”;
String[] fruits = data.split(regex);
System.out.println(Arrays.toString(fruits));
// Output: [apple, banana, orange, pear, grape]
}
}
“`
四、性能考虑与最佳实践
- 缓存
Pattern对象:编译正则表达式 (Pattern.compile()) 是一个相对耗时的操作。如果一个正则表达式需要被反复使用,务必将其编译结果Pattern对象缓存起来,而不是每次都重新编译。可以将Pattern对象定义为静态常量 (private static final Pattern MY_PATTERN = Pattern.compile(...))。 - 优先使用
String类方法进行简单操作:对于一次性的、简单的匹配(如matches)或替换(replaceAll),直接使用String类的方法代码更简洁。但请记住,它们内部仍然会进行编译,性能上不如缓存的Pattern。 - 选择合适的量词(贪婪 vs. 懒惰):理解贪婪和懒惰量词的区别,根据需要选择合适的量词。错误的量词可能导致匹配不到预期结果或性能下降。懒惰量词
*?,+?在处理嵌套结构或需要最小匹配时很有用。 - 注意回溯(Backtracking):复杂的正则表达式,尤其是包含嵌套量词和交替
|时,可能导致“灾难性回溯”(Catastrophic Backtracking),使得匹配时间呈指数级增长。尽量编写精确、高效的模式,避免不必要的回溯。独占量词 (*+,++等) 可以阻止回溯,有时能提高性能,但需谨慎使用。 - 使用非捕获组
(?:...):如果只是需要将一部分模式组合起来(例如应用量词),但不需要捕获这部分匹配的内容,使用非捕获组(?:...)而不是捕获组(...)。这可以略微提高性能,并减少Matcher对象需要管理的组的数量。 - Java 字符串转义:时刻牢记在 Java 字符串字面量中,
\是转义字符。因此,正则表达式中的\需要写成\\,\d写成\\d,\b写成\\b等。匹配字面量的\则需要\\\\。 - 充分测试:正则表达式的行为可能很微妙。务必使用各种边界情况和预期内外的输入来充分测试你的正则表达式,确保它能正确处理所有情况。可以使用在线 Regex 测试工具辅助调试。
五、总结
正则表达式是 Java 开发者处理文本数据的强大武器。本指南(基础篇)介绍了正则表达式的基本语法、元字符、量词、预定义字符类、边界匹配器等核心概念,并详细讲解了 Java java.util.regex 包中的 Pattern 和 Matcher 类的用法,包括编译模式、执行匹配、查找、获取捕获组、替换等操作。我们还探讨了 String 类提供的便捷方法,并通过实战案例展示了正则表达式在验证、提取、替换、拆分等场景的应用。最后,强调了性能优化和最佳实践的重要性。
掌握这些基础知识,你已经能够解决许多常见的文本处理问题。然而,正则表达式的世界远不止于此,还有更高级的主题如零宽断言(Lookarounds: (?=...), (?!...), (?<=...), (?<!...))、命名捕获组 ((?<name>...))、原子组 ((?>...)) 等,它们能让你构建更强大、更精确的模式。希望本指南能为你打开 Java 正则表达式的大门,激发你进一步探索和实践的兴趣。