Java 正则表达式核心教程与实战
摘要
正则表达式是处理字符串的强大工具,广泛应用于文本搜索、格式验证、内容替换等多种场景。Java 通过 java.util.regex 包提供了完整的正则表达式支持。本教程将从基础概念讲起,深入探讨 Java 中正则表达式的核心 API、语法规则,并通过丰富的实战示例,帮助你完全掌握在 Java 项目中应用正则表达式的技巧。
目录
1. 什么是正则表达式?
正则表达式(Regular Expression, Regex)是一种特殊的字符串序列,它定义了一个“搜索模式”。通过这个模式,你可以在文本中:
- 验证:检查一个字符串是否符合特定格式(如邮箱、手机号)。
- 搜索:在一个大文本中查找所有符合模式的子字符串。
- 替换:找到匹配的子字符串并将其替换为其他内容。
- 提取:从字符串中抽取出需要的部分(如URL中的域名)。
它的强大之处在于其表达能力,能够用简洁的模式描述复杂的文本规则。
2. Java 正则表达式核心 API
Java 的正则表达式功能主要由 java.util.regex 包中的三个核心类提供。
Pattern 类
Pattern 对象是正则表达式的编译后表示。一个正则表达式字符串首先需要被编译成一个 Pattern 实例,这个过程会进行语法检查和性能优化。
- 创建:
Pattern类没有公共构造函数。我们必须使用其静态方法compile()来创建实例。
java
Pattern pattern = Pattern.compile("a*b"); - 重要性:
Pattern对象是线程安全的,并且可以被重复使用。对于需要多次使用的正则表达式,强烈建议将其编译一次并缓存起来,而不是每次使用都重新编译,这样可以显著提升性能。
Matcher 类
Matcher 对象是解释 Pattern 并对输入字符串执行匹配操作的引擎。
- 创建:
Matcher实例通过Pattern对象的matcher()方法创建。
java
Matcher matcher = pattern.matcher("aaaaab"); - 重要性:
Matcher对象不是线程安全的,每个线程都应该有自己的Matcher实例。它存储了针对特定字符串的匹配状态(如上次匹配的位置)。
PatternSyntaxException
这是一个非受检异常(Unchecked Exception),当 Pattern.compile() 方法接收到一个语法不正确的正则表达式字符串时,会抛出此异常。
java
try {
Pattern.compile("[a-z"); // 缺少闭合的 ']'
} catch (PatternSyntaxException e) {
System.err.println("正则表达式语法错误: " + e.getMessage());
}
3. 正则表达式核心语法
掌握正则表达式的关键在于理解其语法和元字符。
元字符
元字符是在正则表达式中具有特殊含义的字符。
| 元字符 | 描述 | 示例 |
|---|---|---|
. |
匹配除换行符 \n 之外的任何单个字符。 |
a.c 匹配 “abc”, “a_c” |
\d |
匹配一个数字,等价于 [0-9]。 |
\d{3} 匹配 “123” |
\D |
匹配一个非数字字符。 | \D 匹配 “a”, “#” |
\s |
匹配任何空白字符(空格, \t, \n)。 |
a\sb 匹配 “a b” |
\S |
匹配任何非空白字符。 | \S+ 匹配 “hello” |
\w |
匹配字母、数字或下划线,等价于 [a-zA-Z0-9_]。 |
\w+ 匹配 “word_123” |
\W |
匹配非字母、数字或下划线字符。 | \W 匹配 “@”, ” “ |
字符类
使用方括号 [] 定义一组字符。
| 字符类 | 描述 | 示例 |
|---|---|---|
[abc] |
匹配 “a”, “b”, 或 “c” 中的任意一个。 | [aeiou] 匹配任何元音字母 |
[^abc] |
否定,匹配除 “a”, “b”, “c” 之外的任何字符。 | [^0-9] 匹配任何非数字字符 |
[a-zA-Z] |
范围,匹配从 “a”到”z”或”A”到”Z”的任何一个字符。 | [a-z]{5} 匹配5个小写字母 |
边界匹配
边界匹配符用于定位在字符串中特定位置的匹配。
| 边界符 | 描述 | 示例 |
|---|---|---|
^ |
匹配行的开头。 | ^Hello 匹配以 “Hello” 开头的字符串 |
$ |
匹配行的结尾。 | world$ 匹配以 “world” 结尾的字符串 |
\b |
单词边界。匹配单词字符和非单词字符之间的位置。 | \bcat\b 匹配独立的 “cat” |
\B |
非单词边界。 | \Bcat\B 匹配 “Tomcat” 中的 “cat” |
量词(Quantifiers)
量词用于指定一个模式需要匹配的次数。
| 量词 | 描述 | 示例 |
|---|---|---|
* |
匹配前一个元素零次或多次。 | go*gle 匹配 “ggle”, “google” |
+ |
匹配前一个元素一次或多次。 | go+gle 匹配 “gogle”, “google” |
? |
匹配前一个元素零次或一次。 | colou?r 匹配 “color”, “colour” |
{n} |
匹配前一个元素恰好 n 次。 | \d{4} 匹配 “2024” |
{n,} |
匹配前一个元素至少 n 次。 | \d{2,} 匹配 “12”, “1234” |
{n,m} |
匹配前一个元素从 n 次到 m 次。 | \d{2,4} 匹配 “12”, “123”, “1234” |
贪婪、懒惰与独占模式
- 贪婪模式 (Greedy): 默认模式。量词会尽可能多地匹配字符。例如,
a.*b对于 “axbyazb” 会匹配整个字符串。 - 懒惰模式 (Reluctant/Lazy): 在量词后添加
?。会尽可能少地匹配字符。例如,a.*?b对于 “axbyazb” 会先匹配 “axb”,再次调用find()会匹配 “azb”。 - 独占模式 (Possessive): 在量词后添加
+。会尽可能多地匹配,但不会回溯。性能稍好,但使用场景较少。
分组与捕获
使用圆括号 () 可以将多个字符作为一个整体对待,并且会“捕获”这个分组匹配到的内容,以便后续引用。
- 分组:
(dog)+会匹配 “dog”, “dogdog” 等。 - 捕获:
(\d{4})-(\d{2})在匹配 “2024-12” 时,会捕获两个分组:group(1)的内容是 “2024”。group(2)的内容是 “12”。group(0)始终是整个匹配的字符串 “2024-12″。
特殊构造(非捕获)
| 构造 | 描述 |
|---|---|
(?:...) |
非捕获分组:只分组,不捕获,不计入 groupCount。 |
(?=...) |
正向先行断言:匹配位置右侧必须满足表达式。 |
(?!...) |
负向先行断言:匹配位置右侧必须不满足表达式。 |
(?<=...) |
正向后行断言:匹配位置左侧必须满足表达式。 |
(?<!...) |
负向后行断言:匹配位置左侧必须不满足表达式。 |
4. Matcher 类的常用方法
假设我们有 Pattern pattern = Pattern.compile(regex); 和 Matcher matcher = pattern.matcher(input);
boolean matches(): 尝试将整个输入字符串与模式进行匹配。如果整个字符串完全匹配,则返回true。boolean find(): 扫描输入字符串,查找与模式匹配的下一个子序列。每次调用都会从上一次匹配结束的位置继续查找。boolean lookingAt(): 尝试将输入字符串从开头开始与模式进行匹配。String group(): 返回由上一次匹配操作(如find())所捕获的子序列。等价于group(0)。String group(int group): 返回在匹配期间由给定分组捕获的子序列。int groupCount(): 返回此匹配器模式中的捕获分组数量。String replaceAll(String replacement): 替换输入字符串中所有匹配模式的子序列。String replaceFirst(String replacement): 替换输入字符串中第一个匹配模式的子序列。
5. 实战示例
示例 1:验证邮箱地址
一个相对简单但常用的邮箱正则表达式。
“`java
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class EmailValidator {
// 邮箱正则:用户名@域名
// 用户名: 字母、数字、下划线、点、减号
// 域名: 字母、数字、减号,后跟点,最后是2-6个字母的顶级域名
private static final String EMAIL_REGEX =
“^[\w\.-]+@([\w-]+\.)+[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;
}
Matcher matcher = EMAIL_PATTERN.matcher(email);
return matcher.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("[email protected] is valid: " + isValidEmail("[email protected]")); // false
System.out.println("test@com is valid: " + isValidEmail("test@com")); // false
}
}
“`
示例 2:提取字符串中的电话号码
从一段文本中找出所有可能是电话号码的数字串(假设为11位数字)。
“`java
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.ArrayList;
import java.util.List;
public class PhoneNumberExtractor {
// 匹配11位数字的电话号码
private static final Pattern PHONE_PATTERN = Pattern.compile(“\b1\d{10}\b”);
public static List<String> findPhoneNumbers(String text) {
List<String> numbers = new ArrayList<>();
Matcher matcher = PHONE_PATTERN.matcher(text);
// 使用 find() 方法循环查找所有匹配项
while (matcher.find()) {
numbers.add(matcher.group());
}
return numbers;
}
public static void main(String[] args) {
String text = "联系人: 张三 13812345678, 李四 15987654321。无效号码: 12345。";
List<String> foundNumbers = findPhoneNumbers(text);
System.out.println("找到的电话号码: " + foundNumbers); // [13812345678, 15987654321]
}
}
“`
示例 3:替换敏感词
将文本中的敏感词替换为星号。
“`java
import java.util.regex.Pattern;
public class SensitiveWordFilter {
public static void main(String[] args) {
String text = “这是一段包含不良言论和违禁词的文本。”;
String filteredText = text.replaceAll(“不良言论|违禁词”, “***”);
System.out.println(“原文: ” + text);
System.out.println(“过滤后: ” + filteredText);
}
}
``String
**注意**:类自带的replaceAll` 方法内部就是基于正则表达式的,对于简单的一次性替换,可以直接使用。
示例 4:从 HTML 中提取链接
从一个简单的 HTML 字符串中提取所有 <a> 标签的 href 属性值。
警告: 正则表达式不适合解析复杂的、结构不规则的 HTML/XML。对于正式的应用,请使用专用的 HTML 解析库(如 Jsoup)。这里仅作演示。
“`java
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class LinkExtractor {
// 使用懒惰量詞 .? 来避免贪婪匹配
private static final Pattern LINK_PATTERN = Pattern.compile(“<a\s+href=\”(.?)\””);
public static void main(String[] args) {
String html = "<ul>" +
"<li><a href=\"https://example.com/page1\">Page 1</a></li>" +
"<li><a href=\"/local/page2.html\">Page 2</a></li>" +
"<li><a href=\"https://google.com\">Google</a></li>" +
"</ul>";
Matcher matcher = LINK_PATTERN.matcher(html);
while (matcher.find()) {
// group(1) 捕获的是 href="..." 中括号内的内容
System.out.println("找到链接: " + matcher.group(1));
}
}
}
“`
示例 5:使用 split 方法分割字符串
Pattern.split() 方法允许你使用复杂的模式来分割字符串。
“`java
import java.util.regex.Pattern;
import java.util.Arrays;
public class SplitExample {
public static void main(String[] args) {
// 使用一个或多个逗号或分号作为分隔符
Pattern pattern = Pattern.compile(“[,;]+”);
String text = “apple,banana;;cherry,date”;
String[] fruits = pattern.split(text);
System.out.println(Arrays.toString(fruits)); // [apple, banana, cherry, date]
}
}
“`
6. 性能与最佳实践
- 缓存
Pattern对象: 如前所述,如果一个正则表达式需要被多次使用,请将其编译为一个静态的Pattern常量。Pattern.compile()是一个昂贵的操作。 - 使用
matches()进行全匹配验证: 当你需要验证整个字符串是否符合格式时(如邮箱、密码),matches()是最直接且意图最清晰的方法。 - 注意贪婪量词: 贪婪量词(
*,+)可能会导致性能问题,尤其是在处理大文本时,可能引发“灾難性回溯”。在可能的情况下,优先使用懒惰量词(*?,+?)或更精确的模式。 - 使用非捕获分组: 如果你只需要分组来应用量词,但不需要引用分组的内容,请使用非捕获分组
(?:...)。这样可以减少内存开销,并略微提升性能。 - 为简单场景选择
String方法: 对于简单的、一次性的匹配或替换,直接使用String.matches(),String.replaceAll(),String.split()更方便。
7. 总结
Java 的正则表达式功能强大且高效。掌握 Pattern 和 Matcher 类的使用方法,并熟悉正则表达式的核心语法,将极大地提升你处理文本数据的能力。从简单的格式验证到复杂的数据提取,正则表达式都是你工具箱中不可或缺的一员。
多写多练是掌握正则表达式的唯一途径。希望这篇教程能为你打下坚实的基础。