Java 正则表达式快速入门指南
正则表达式(Regular Expression),简称 Regex 或 Regexp,是用于匹配、查找、替换文本中特定模式的强大工具。它是一种独立的微语言,几乎被所有编程语言支持。在 Java 中,通过 java.util.regex
包,我们可以充分利用正则表达式的强大功能来处理字符串。
本指南旨在帮助你快速掌握 Java 中正则表达式的基础知识和常用操作。
第一部分:什么是正则表达式?为什么使用它?
想象一下,你需要从一个长文本中找出所有的电子邮件地址,或者验证用户输入的手机号码是否符合规范,或者批量替换文本中的敏感词汇。如果仅使用传统的字符串查找和替换方法(如 indexOf
, substring
, replace
),代码会变得非常复杂、难以维护,并且容易出错。
正则表达式提供了一种简洁、灵活且功能强大的方式来描述和匹配字符串的模式。通过一串特殊的字符序列,你可以定义你想要查找、验证或操作的字符串结构。
为什么在 Java 中使用正则表达式?
- 强大的模式匹配能力: 能够处理复杂的字符串模式,例如匹配任意格式的日期、URL、IP地址等。
- 代码简洁高效: 相比手动编写大量的字符串处理逻辑,正则表达式通常能用更少的代码实现更复杂的功能。
- 跨语言通用性: 正则表达式的语法在不同语言中大同小异,学习一次可以在多种语言中使用。
- Java 标准库支持:
java.util.regex
包提供了完整的正则表达式处理 API,功能完善且性能良好。
第二部分:正则表达式的基础语法
在深入 Java API 之前,我们需要了解正则表达式自身的一些基本语法元素。这些是构建匹配模式的基石。
1. 普通字符 (Literal Characters)
大多数字符都匹配它们自身。例如,a
匹配字符 ‘a’,1
匹配字符 ‘1’,hello
匹配字符串 “hello”。
2. 元字符 (Metacharacters)
元字符是具有特殊含义的字符,它们不匹配自身,而是用于表达某种模式或位置。
.
(点号):匹配除换行符\n
之外的任意单个字符。- 示例:
a.b
可以匹配acb
,a#b
,a3b
等。
- 示例:
^
:匹配输入字符串的开始位置。在多行模式下,也匹配行的开始位置。- 示例:
^hello
只匹配以 “hello” 开头的字符串。
- 示例:
$
:匹配输入字符串的结束位置。在多行模式下,也匹配行的结束位置。- 示例:
world$
只匹配以 “world” 结尾的字符串。
- 示例:
*
:匹配前面的元素零次或多次。- 示例:
a*
可以匹配""
(空字符串),a
,aa
,aaa
等。
- 示例:
+
:匹配前面的元素一次或多次。- 示例:
a+
可以匹配a
,aa
,aaa
等,但不匹配""
。
- 示例:
?
:匹配前面的元素零次或一次。- 示例:
a?
可以匹配""
,a
。常用于表示可选的部分。
- 示例:
{n}
:匹配前面的元素恰好 n 次。- 示例:
a{3}
只匹配aaa
。
- 示例:
{n,}
:匹配前面的元素至少 n 次。- 示例:
a{2,}
匹配aa
,aaa
,aaaa
等。
- 示例:
{n,m}
:匹配前面的元素至少 n 次,但不超过 m 次。- 示例:
a{2,4}
匹配aa
,aaa
,aaaa
。
- 示例:
|
(竖线):作为或运算符,匹配|
符号前或后的模式。- 示例:
cat|dog
匹配 “cat” 或 “dog”。
- 示例:
()
(括号):用于分组或捕获匹配的子串。可以将多个元素视为一个整体应用量词,或者在匹配后提取特定部分。- 示例:
(ab)+
匹配ab
,abab
,ababab
等。
- 示例:
3. 字符类 (Character Classes)
字符类用方括号 []
表示,用于匹配方括号中列出的任意一个字符。
[abc]
:匹配 ‘a’, ‘b’, 或 ‘c’ 中的任意一个字符。[^abc]
:匹配除 ‘a’, ‘b’, ‘c’ 之外的任意一个字符 (^
在[]
内表示取反)。[a-z]
:匹配任意小写字母。[A-Z]
:匹配任意大写字母。[0-9]
:匹配任意数字。[a-zA-Z0-9]
:匹配任意字母或数字。
预定义字符类 (Shorthand Character Classes):
为了方便,正则表达式提供了一些常用的预定义字符类:
\d
:匹配任意一个数字字符 (等同于[0-9]
)。\D
:匹配任意一个非数字字符 (等同于[^0-9]
)。\w
:匹配任意一个“字词”字符 (字母、数字或下划线,等同于[a-zA-Z0-9_]
)。\W
:匹配任意一个非“字词”字符 (等同于[^a-zA-Z0-9_]
)。\s
:匹配任意一个空白字符 (空格、制表符\t
、换页符\f
、回车符\r
、换行符\n
)。\S
:匹配任意一个非空白字符。
4. 转义字符 (Escaping)
如果想匹配元字符本身,而不是它的特殊含义,就需要使用反斜杠 \
进行转义。
- 要匹配点号
.
,使用\.
。 - 要匹配反斜杠
\
,使用\\
。 - 要匹配星号
*
,使用\*
。 - 要匹配问号
?
,使用\?
。 - …以此类推,几乎所有元字符都需要转义才能匹配自身。
特别注意: 在 Java 字符串中,反斜杠 \
本身也是一个转义字符。因此,如果你想在正则表达式中表示一个反斜杠 \
(用于转义元字符,例如 \.
表示匹配点号),你需要在 Java 字符串中使用两个反斜杠 \\
。例如,在 Java 中表示正则表达式 \.
,你需要写成 "\\."
。表示正则表达式 \\
(匹配一个反斜杠自身),你需要写成 "\\\\"
。
5. 锚点 (Anchors)
锚点用于匹配字符串中的特定位置,而不是字符。
^
:匹配输入字符串的开始。$
:匹配输入字符串的结束。\b
:匹配一个单词边界。单词边界是指一个“字词”字符和非“字词”字符之间的位置,或者字符串的开始/结束位置与一个“字词”字符之间的位置。- 示例:
\bcat\b
可以匹配 “The cat sat.” 中的 “cat”,但不能匹配 “catastrophe” 中的 “cat”。
- 示例:
\B
:匹配一个非单词边界。
第三部分:Java 中的正则表达式 API (java.util.regex
)
Java 的正则表达式功能主要由 java.util.regex
包中的三个类提供:
-
Pattern
类:- 代表一个编译后的正则表达式模式。正则表达式字符串必须先编译成
Pattern
对象才能进行匹配操作。 Pattern
对象是线程安全的。- 通常使用静态方法
Pattern.compile(String regex)
来创建一个Pattern
对象。 - 也可以使用静态方法
Pattern.matches(String regex, CharSequence input)
进行一次性的完整匹配检查。这个方法内部会编译正则表达式并创建一个 Matcher 进行匹配,但不推荐在循环或多次匹配中使用,因为它会重复编译模式,效率较低。
- 代表一个编译后的正则表达式模式。正则表达式字符串必须先编译成
-
Matcher
类:- 代表一个模式匹配器,用于对输入字符串执行匹配操作。
Matcher
对象是通过Pattern
对象的matcher(CharSequence input)
方法创建的。Matcher
对象不是线程安全的,每个线程应该有自己的Matcher
实例。- 提供了多种方法来执行不同的匹配操作(查找、验证、替换等)。
-
PatternSyntaxException
类:- 当正则表达式语法不正确时抛出的非受控异常。
基本使用流程:
- 将正则表达式字符串编译成
Pattern
对象。 - 使用
Pattern
对象创建Matcher
对象,并指定要匹配的输入字符串。 - 使用
Matcher
对象的方法执行匹配操作。
Matcher
类的常用方法:
boolean matches()
:尝试将整个输入序列与模式进行匹配。如果整个输入匹配成功则返回true
,否则返回false
。boolean find()
:尝试查找输入序列中与模式匹配的下一个子序列。可以在输入字符串中多次调用此方法,以查找所有匹配项。boolean find(int start)
:重置此匹配器并在指定索引处开始查找下一个匹配项。String group()
:返回上次匹配到的子序列(即整个匹配的内容)。在调用matches()
或find()
成功后调用。String group(int group)
:返回在上次匹配期间由给定捕获组匹配的输入子序列。捕获组从 1 开始编号,group(0)
等同于group()
,表示整个匹配项。int groupCount()
:返回模式中的捕获组数量。int start()
:返回上次匹配到的子序列在输入字符串中的起始索引(包含)。int end()
:返回上次匹配到的子序列在输入字符串中的结束索引(不包含)。Matcher reset()
:重置匹配器,丢弃所有状态信息。Matcher reset(CharSequence input)
:重置匹配器并为其指定新的输入序列。String replaceAll(String replacement)
:将输入序列中所有匹配模式的子序列替换为指定的替换字符串。String replaceFirst(String replacement)
:将输入序列中第一个匹配模式的子序列替换为指定的替换字符串。static String Pattern.quote(String s)
:返回一个字符串,该字符串可以被视为字面值字符串,用于创建匹配s
的模式。这对于需要匹配包含特殊正则表达式元字符的字面值字符串时非常有用,它会自动处理转义。String Pattern.pattern()
:返回创建此模式的正则表达式字符串。
第四部分:实践演练 (Java 代码示例)
下面通过一些常见的例子来演示如何在 Java 中使用正则表达式。
示例 1:验证电子邮件地址格式
假设要验证一个字符串是否符合简单的电子邮件格式:[email protected]
“`java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexExample1 {
public static void main(String[] args) {
String email = “[email protected]”;
String invalidEmail = “invalid-email”;
// 电子邮件的正则表达式模式
// ^ 表示字符串开始
// \w+ 表示匹配一个或多个字词字符 (字母、数字、下划线)
// @ 匹配字面值字符 @
// \w+\.\w+ 表示匹配一个或多个字词字符,后跟一个点,再后跟一个或多个字词字符
// $ 表示字符串结束
// 这个模式比较简单,真实的邮件验证会复杂得多
String emailRegex = "^\\w+@\\w+\\.\\w+$";
// 编译正则表达式
Pattern pattern = Pattern.compile(emailRegex);
// 创建 Matcher 对象
Matcher matcher1 = pattern.matcher(email);
Matcher matcher2 = pattern.matcher(invalidEmail);
// 使用 matches() 方法进行完整匹配
boolean isEmailValid1 = matcher1.matches();
boolean isEmailValid2 = matcher2.matches();
System.out.println("'" + email + "' 是有效邮箱吗? " + isEmailValid1); // 输出: true
System.out.println("'" + invalidEmail + "' 是有效邮箱吗? " + isEmailValid2); // 输出: false
// 也可以使用 Pattern.matches() 简化一次性验证
System.out.println("'" + email + "' 是有效邮箱吗? (简化方式) " + Pattern.matches(emailRegex, email)); // 输出: true
}
}
“`
示例 2:查找和提取字符串中的数字
查找一个字符串中所有的连续数字串。
“`java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexExample2 {
public static void main(String[] args) {
String text = “There are 123 apples and 456 oranges.”;
// 匹配一个或多个数字
String numberRegex = "\\d+";
Pattern pattern = Pattern.compile(numberRegex);
Matcher matcher = pattern.matcher(text);
System.out.println("在字符串中查找数字:");
// 使用 find() 方法查找所有匹配项
while (matcher.find()) {
// group() 返回当前匹配到的子串
String matchedNumber = matcher.group();
// start() 返回匹配到的子串的起始索引
int startIndex = matcher.start();
// end() 返回匹配到的子串的结束索引 (不包含)
int endIndex = matcher.end();
System.out.println("找到匹配: '" + matchedNumber + "',位置: " + startIndex + "-" + (endIndex - 1));
// 输出:
// 找到匹配: '123',位置: 10-12
// 找到匹配: '456',位置: 24-26
}
}
}
“`
示例 3:替换字符串中的内容
将字符串中所有的 “cat” 替换为 “dog”。
“`java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexExample3 {
public static void main(String[] args) {
String text = “The cat sat on the mat. Another cat purred.”;
// 匹配 "cat"
String regex = "cat";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
// 使用 replaceAll() 替换所有匹配项
String newText = matcher.replaceAll("dog");
System.out.println("原始字符串: " + text);
System.out.println("替换后字符串: " + newText);
// 输出:
// 原始字符串: The cat sat on the mat. Another cat purred.
// 替换后字符串: The dog sat on the mat. Another dog purred.
// 也可以使用 String 类的 replaceAll 方法 (内部也是用正则表达式)
String newText2 = text.replaceAll(regex, "dog");
System.out.println("替换后字符串 (String.replaceAll): " + newText2); // 输出: The dog sat on the mat. Another dog purred.
}
}
“`
示例 4:使用捕获组提取信息
提取 HTML 标签 <p>...</p>
中的内容。
“`java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexExample4 {
public static void main(String[] args) {
String html = “
This is the first paragraph.
This is the second paragraph.
“;
// 匹配 <p>...</p> 标签,并捕获标签内的内容
// <p> 匹配字面值 "<p>"
// (.*?) 这是一个捕获组:
// . 匹配任意字符 (除换行)
// * 匹配前面的元素零次或多次
// ? 使 * 变为惰性匹配 (non-greedy),尽可能少地匹配字符,直到遇到下一个模式(即 </p>)
// </p> 匹配字面值 "</p>"
String regex = "<p>(.*?)</p>";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(html);
System.out.println("提取 <p> 标签内容:");
// 使用 find() 查找所有 <p>...</p> 块
while (matcher.find()) {
// group(0) 或 group() 是整个匹配项 (<p>...</p>)
String fullMatch = matcher.group(0);
// group(1) 是第一个捕获组的内容 (.*?),即标签内的内容
String content = matcher.group(1);
System.out.println("完整匹配: " + fullMatch);
System.out.println("提取内容: " + content);
System.out.println("---");
}
// 输出:
// 提取 <p> 标签内容:
// 完整匹配: <p>This is the first paragraph.</p>
// 提取内容: This is the first paragraph.
// ---
// 完整匹配: <p>This is the second paragraph.</p>
// 提取内容: This is the second paragraph.
// ---
}
}
“`
注意: 使用正则表达式解析 HTML 或 XML 通常不是最佳实践,因为它们的结构可能非常复杂和嵌套。对于解析结构化文档,推荐使用专门的解析库(如 Jsoup for HTML, DOM/SAX parsers for XML)。这个例子仅为了演示捕获组的使用。
第五部分:高级概念 (简述)
本指南是快速入门,但正则表达式还有一些更高级的概念,了解它们会让你更强大:
- 贪婪、勉强和独占量词 (Greedy, Reluctant/Lazy, Possessive Quantifiers): 默认量词 (
*
,+
,{n,}
) 是贪婪的,会尽可能多地匹配。在量词后加?
(*?
,+?
,{n,}?
) 使其变为勉强(非贪婪/惰性),尽可能少地匹配。在量词后加+
(*+
,++
,{n,}+
) 使其变为独占量词,匹配后不释放匹配的字符进行回溯。 - 反向引用 (Backreferences): 在正则表达式中使用
\n
(n 是捕获组的编号) 来引用前面第 n 个捕获组匹配到的内容。 - 零宽断言 (Lookarounds): 匹配一个位置,这个位置的后面或前面需要符合某种模式,但这个模式本身不被匹配包含在结果中。包括肯定/否定先行断言 (
(?=...)
,(?!...)
) 和肯定/否定后行断言 ((?<=...)
,(?<!...)
)。 - 标志/模式标志 (Flags): 可以在
Pattern.compile()
方法中添加标志参数,改变匹配的行为,例如Pattern.CASE_INSENSITIVE
(忽略大小写)、Pattern.MULTILINE
(多行模式,使^
和$
匹配行的开始和结束)、Pattern.DOTALL
(单行模式,使.
匹配包括换行符在内的任意字符) 等。
第六部分:实用技巧和注意事项
- 谨慎使用
Pattern.matches()
: 方便进行简单的完整字符串验证,但如果需要多次使用同一个模式,务必使用Pattern.compile()
编译一次,然后创建多个Matcher
对象,效率更高。 - 字符串中的反斜杠转义: 记住在 Java 字符串中,一个正则表达式的反斜杠
\
需要写成两个反斜杠\\
。 - 测试你的正则表达式: 正则表达式可能非常复杂,强烈推荐使用在线正则表达式测试工具(如 regex101.com, javaregex.com 等)来构建和测试你的模式,查看匹配结果、解释以及性能。
- 复杂性与可读性: 过于复杂的正则表达式虽然功能强大,但可读性差,难以理解和维护。在可能的情况下,考虑是否可以通过简单的字符串操作或分步正则表达式来提高代码的可读性。
- 性能: 虽然正则表达式很强大,但对于极大规模的文本处理,复杂的模式或不当的使用(如过度回溯)可能导致性能问题(灾难性回溯 – catastrophic backtracking)。理解贪婪、勉强、独占量词以及适当使用锚点和原子组 (
(?>...)
) 可以帮助优化性能。 String
类的matches
,split
,replaceAll
,replaceFirst
方法: 这些是String
类提供的便捷方法,它们内部使用了java.util.regex
包。例如str.matches(regex)
等同于Pattern.matches(regex, str)
。对于简单的、一次性的操作,使用这些方法很方便。
结论
Java 中的正则表达式是一个极其强大且灵活的工具,掌握它可以极大地提升你处理文本数据的能力。本指南涵盖了正则表达式的基础语法和在 Java 中使用 Pattern
和 Matcher
类进行匹配、查找、提取和替换的基本操作。
从理解元字符、字符类和量词开始,逐步学习如何在 Java 代码中编译模式和创建匹配器。通过实践示例,你可以更好地理解这些概念的应用。虽然正则表达式的完整世界比这里介绍的要广阔得多,但掌握了这些基础知识,你就已经迈出了成功的第一步。多加练习和查阅文档是精通正则表达式的关键。祝你使用 Java 正则表达式愉快!