Java 正则表达式基础教程:从入门到精通
正则表达式(Regular Expression,简称 Regex 或 Regexp)是一种强大的文本处理工具,用于描述、匹配、查找和操作符合特定模式的字符串。在数据验证、文本搜索与替换、日志分析、爬虫数据提取等众多领域,正则表达式都扮演着至关重要的角色。
Java 语言通过 java.util.regex
包提供了对正则表达式的全面支持。本教程将带您深入了解 Java 正则表达式的基础知识,包括核心类、基本语法、常用方法以及实际应用。无论您是初学者还是希望系统回顾,本文都将为您提供详尽的指导。
1. 什么是正则表达式?为什么在 Java 中使用它?
正则表达式本质上是一个由字符和特殊符号组成的字符串模式。它定义了一套规则,用于匹配其他字符串。想象一下,您想从一大段文本中找出所有符合邮箱格式的字符串,或者检查用户输入的密码是否同时包含大小写字母和数字。使用传统的字符串方法(如 indexOf
、substring
、contains
)来完成这些任务会变得异常复杂和冗长。正则表达式则提供了一种简洁、灵活且强大的方式来表达这些复杂的模式匹配需求。
在 Java 中,java.util.regex
包提供了 Pattern
、Matcher
和 PatternSyntaxException
三个核心类来处理正则表达式。
Pattern
:表示一个已编译的正则表达式。正则表达式在使用前必须先被编译成Pattern
对象。这是因为编译是一个相对耗时的操作,编译后的Pattern
对象可以被重复用于创建多个Matcher
对象,从而提高效率。Matcher
:是一个匹配器对象,它对输入字符串执行匹配操作。您可以通过Pattern
对象创建一个Matcher
对象,然后使用Matcher
的各种方法(如matches
、find
、replace
等)来执行具体的匹配、查找或替换任务。PatternSyntaxException
:当正则表达式的语法不正确时抛出的异常。
使用 Java 的 java.util.regex
包,您可以轻松实现各种复杂的字符串处理任务,例如:
- 验证数据格式:检查邮箱、手机号码、身份证号、URL、IP地址等是否符合特定格式。
- 搜索特定模式:从大量文本中查找所有符合某个模式的子字符串。
- 提取信息:从结构化或半结构化文本(如日志文件、HTML/XML 内容)中提取所需数据。
- 替换文本:将符合某个模式的子字符串替换为其他内容。
- 分割字符串:使用某个模式作为分隔符将字符串分割成多个部分。
总而言之,掌握 Java 正则表达式将极大地提升您处理字符串的能力。
2. 正则表达式基本语法元素
正则表达式的强大源于其丰富的语法元素。这些元素可以组合起来构建复杂的模式。下面我们将介绍一些最基础和常用的语法元素。
2.1. 匹配单个字符
- 普通字符: 大多数字符(如字母、数字、标点符号等)匹配其自身。例如,
cat
匹配字符串 “cat”。 -
点 (.): 匹配除换行符(
\n
)之外的任意单个字符。如果启用DOTALL
标志(或称为s
模式),点号可以匹配包括换行符在内的任意字符。- 示例:
a.b
可以匹配 “aab”, “acb”, “axb” 等,但不能匹配 “a\nb”。 -
转义字符 (\): 用于转义具有特殊含义的字符,使其匹配字面值。例如,要匹配字面上的点号
.
,需要使用\.
。其他需要转义的特殊字符包括^
,$
,*
,+
,?
,(
,)
,[
,{
,|
,\
。 -
示例:
a\.b
只匹配字符串 “a.b”。c\\d
匹配 “c\d”。
- 示例:
2.2. 字符类 ([…])
字符类允许您匹配指定集合中的任意一个字符。
[abc]
: 匹配方括号中列出的任意一个字符(a、b 或 c)。[a-z]
: 匹配从 a 到 z 的任意一个小写字母(这是一个字符范围)。[A-Z]
: 匹配从 A 到 Z 的任意一个大写字母。[0-9]
: 匹配从 0 到 9 的任意一个数字。[a-zA-Z0-9]
: 匹配任意一个字母或数字。[aeiou]
: 匹配任意一个元音字母。
否定字符类 ([^…])
在字符类内部,如果第一个字符是 ^
,则表示匹配不在该集合中的任意一个字符。
[^abc]
: 匹配除 a、b、c 之外的任意一个字符。[^0-9]
: 匹配除数字之外的任意一个字符(与\D
相同)。
预定义字符类
Java 正则表达式提供了一些方便的预定义字符类:
\d
: 匹配任意一个数字字符,等价于[0-9]
。\D
: 匹配任意一个非数字字符,等价于[^0-9]
。\w
: 匹配任意一个“单词”字符,包括字母、数字和下划线_
,等价于[a-zA-Z0-9_]
。\W
: 匹配任意一个非“单词”字符,等价于[^a-zA-Z0-9_]
。\s
: 匹配任意一个空白字符,包括空格、制表符 (\t
)、换页符 (\f
)、回车符 (\r
) 和换行符 (\n
)。-
\S
: 匹配任意一个非空白字符。- 示例:
\d{3}-\d{4}
可以匹配 “123-4567″。\w+
可以匹配一个单词。
- 示例:
2.3. 量词
量词指定了某个字符或字符组应该出现多少次。
?
: 匹配前面的元素零次或一次。使其变为可选的。- 示例:
colou?r
可以匹配 “color” 或 “colour”。
- 示例:
*
: 匹配前面的元素零次或多次。- 示例:
a*
可以匹配 “”, “a”, “aa”, “aaa” 等。go*gle
可以匹配 “ggle”, “gogle”, “googlegle” 等。
- 示例:
+
: 匹配前面的元素一次或多次。- 示例:
a+
可以匹配 “a”, “aa”, “aaa” 等,但不能匹配 “”。go+gle
可以匹配 “gogle”, “googlegle” 等,但不能匹配 “ggle”。
- 示例:
精确量词 ({})
花括号 {}
允许您指定匹配次数的精确范围。
{n}
: 匹配前面的元素恰好 n 次。- 示例:
\d{3}
匹配恰好三个数字。
- 示例:
{n,}
: 匹配前面的元素至少 n 次。- 示例:
\w{2,}
匹配至少两个单词字符。
- 示例:
{n,m}
: 匹配前面的元素至少 n 次,但不超过 m 次。- 示例:
.{5,10}
匹配任意字符(除换行符)至少 5 个,最多 10 个。
- 示例:
量词的贪婪、勉强与独占模式
默认情况下,量词是贪婪(Greedy)的。这意味着它们会尽可能多地匹配字符,直到模式的其余部分无法匹配为止。
您可以通过在量词后加上 ?
使其变为勉强(Reluctant)或非贪婪模式。勉强量词会尽可能少地匹配字符。
- 示例:对于字符串 “” 和模式
<.*?>
,勉强模式会先匹配<a>
。.*?
会尽可能少地匹配(从””开始),然后向前查找是否能匹配>
。找到第一个>
后,模式<.*?>
完成匹配,得到 ““。继续查找,找到下一个匹配 ““。
还有一种独占(Possessive)量词,通过在量词后加上 +
获得。独占量词会尽可能多地匹配字符,但与贪婪量词不同的是,它们永不回溯。这在某些情况下可以提高性能,但也可能导致无法匹配原本能匹配的字符串。
- 示例:对于字符串 “” 和模式
<.*+>
,独占模式会尝试匹配<
,然后.*+
会吞掉所有剩余字符 “a>“。此时,模式需要匹配>
,但输入字符串已经没有字符了,并且独占量词.*+
不回溯,导致匹配失败。
一般来说,贪婪量词是最常用的,勉强量词用于需要最小匹配的情况(如解析HTML/XML标签),而独占量词则在确定不需要回溯且追求性能时使用。
2.4. 位置匹配 (锚点)
位置匹配符(或称锚点)不匹配具体的字符,而是匹配一个位置。
^
: 匹配输入字符串的开头。如果启用MULTILINE
标志(或称m
模式),则匹配每行的开头。- 示例:
^abc
只匹配以 “abc” 开头的字符串,如 “abcdef”。
- 示例:
$
: 匹配输入字符串的结尾。如果启用MULTILINE
标志,则匹配每行的结尾。- 示例:
xyz$
只匹配以 “xyz” 结尾的字符串,如 “uvwxyz”。
- 示例:
\b
: 匹配一个词的边界。词边界是指一个单词字符 (\w
) 和一个非单词字符 (\W
) 之间的位置,或者是字符串的开头/结尾与一个单词字符之间的位置。- 示例:
\bcat\b
可以匹配字符串 “the cat sat” 中的 “cat”,但不能匹配 “catalog” 或 “concatenate” 中的 “cat”。
- 示例:
\B
: 匹配一个非词边界的位置。- 示例:
\Bcat\B
可以匹配 “conccatenate” 中的 “cat”,但不能匹配 “the cat sat”。
- 示例:
2.5. 分组与捕获
圆括号 ()
用于将多个字符组合成一个单元,可以对其应用量词,或者将其作为一个捕获组来提取匹配的子字符串。
()
: 创建一个捕获组。匹配的内容会被”捕获”供后续使用。捕获组从 1 开始编号(组 0 始终代表整个匹配的文本)。- 示例:
(\d{3})-(\d{4})
可以匹配 “123-4567″。第一个捕获组 ((\d{3})
) 捕获 “123”,第二个捕获组 ((\d{4})
) 捕获 “4567”。
- 示例:
(?:...)
: 创建一个非捕获组。它用于分组以便应用量词或进行逻辑分组,但匹配的内容不会被捕获。这有助于提高性能,特别是当您只需要分组而不需要提取匹配内容时。- 示例:
(?:abc)+
匹配一个或多个连续的 “abc” 序列,如 “abc”, “abcabc” 等,但不捕获 “abc” 子串。
- 示例:
2.6. 选择 (或)
管道符 |
用于表示“或”的关系,匹配其左边或右边的模式。
cat|dog
: 匹配 “cat” 或 “dog”。(cat|dog) food
: 匹配 “cat food” 或 “dog food”。注意分组的使用,以确保|
只应用于 “cat” 和 “dog”。
2.7. 反向引用
反向引用 (\n
,其中 n
是一个数字) 引用前面第 n 个捕获组匹配到的内容。
- 示例:
(\w)\1
匹配任意两个连续相同的单词字符,如 “aa”, “bb”, “cc” 等。(\w+)\s+\1
匹配一个单词,后跟一个或多个空白字符,再后跟与第一个单词完全相同的单词,如 “java java”。
3. Java 中 Pattern
和 Matcher
的使用
了解了基本语法后,我们来看看如何在 Java 代码中使用它们。
3.1. 编译正则表达式
首先,您需要使用 Pattern.compile()
方法编译正则表达式字符串,生成一个 Pattern
对象。
“`java
import java.util.regex.Pattern;
import java.util.regex.Matcher;
// 要匹配的正则表达式
String regex = “a.*b”;
// 编译正则表达式
Pattern pattern = Pattern.compile(regex);
“`
编译过程会检查正则表达式的语法是否正确。如果语法错误,会抛出 PatternSyntaxException
。
3.2. 创建匹配器
接下来,使用 Pattern
对象的 matcher()
方法,传入要进行匹配的输入字符串,生成一个 Matcher
对象。
“`java
// 要进行匹配的输入字符串
String input = “acccb”;
// 创建匹配器对象
Matcher matcher = pattern.matcher(input);
“`
Matcher
对象包含了匹配操作所需的所有信息:编译后的模式、输入字符串以及匹配状态。
3.3. 执行匹配操作
Matcher
类提供了多种方法来执行不同类型的匹配操作:
-
boolean matches()
: 尝试将整个输入序列与模式进行匹配。只有当整个输入字符串都符合正则表达式时才返回true
。
“`java
String input1 = “acccb”;
String input2 = “acccbx”;
String input3 = “xacccb”;Pattern p = Pattern.compile(“a.*b”);
Matcher m1 = p.matcher(input1);
Matcher m2 = p.matcher(input2);
Matcher m3 = p.matcher(input3);System.out.println(input1 + ” matches: ” + m1.matches()); // true
System.out.println(input2 + ” matches: ” + m2.matches()); // false (因为后面多了一个x)
System.out.println(input3 + ” matches: ” + m3.matches()); // false (因为前面多了一个x)
* `boolean find()`: 尝试查找输入序列中与模式匹配的**下一个**子序列。这个方法可以重复调用,每次调用都会从上次匹配结束的位置继续查找。
java
String input = “This is a test. The first match is test, the second is Test.”;
Pattern p = Pattern.compile(“[Tt]est”); // 匹配 test 或 Test
Matcher m = p.matcher(input);System.out.println(“Finding matches:”);
while (m.find()) {
// 打印找到的匹配项和它的起始/结束位置
System.out.println(“Found \”” + m.group() + “\” at positions ” +
m.start() + “-” + m.end());
}
// Output:
// Finding matches:
// Found “test” at positions 10-14
// Found “Test” at positions 40-44
* `boolean lookingAt()`: 尝试将输入序列从**开头**与模式进行匹配。与 `matches()` 不同,`lookingAt()` 即使模式只匹配了输入字符串的一部分开头也返回 `true`。
java
String input1 = “HelloWorld”;
String input2 = “Hello Java”;
String input3 = “Java World”;Pattern p = Pattern.compile(“Hello”);
Matcher m1 = p.matcher(input1);
Matcher m2 = p.matcher(input2);
Matcher m3 = p.matcher(input3);System.out.println(input1 + ” lookingAt: ” + m1.lookingAt()); // true
System.out.println(input2 + ” lookingAt: ” + m2.lookingAt()); // true
System.out.println(input3 + ” lookingAt: ” + m3.lookingAt()); // false
“`
3.4. 获取匹配结果
当 find()
、matches()
或 lookingAt()
返回 true
时,表示找到了匹配项。您可以使用 Matcher
的方法来获取匹配到的子字符串或子组的信息:
String group()
或String group(0)
: 返回最近一次匹配到的整个子字符串。String group(int group)
: 返回最近一次匹配中指定捕获组捕获的子字符串。组号从 1 开始。int groupCount()
: 返回此模式中的捕获组数量(不包括组 0)。int start()
: 返回最近一次匹配到的整个子字符串的起始索引(包含)。int start(int group)
: 返回最近一次匹配中指定捕获组的起始索引。int end()
: 返回最近一次匹配到的整个子字符串的结束索引(不包含)。int end(int group)
: 返回最近一次匹配中指定捕获组的结束索引。
“`java
String input = “Order number is 12345, customer ID is 67890.”;
// 匹配 “number is ” 后面跟着5个数字,并捕获这5个数字
Pattern p = Pattern.compile(“number is (\d{5})”);
Matcher m = p.matcher(input);
if (m.find()) {
System.out.println(“Entire match: ” + m.group(0)); // Order number is 12345
System.out.println(“Order number: ” + m.group(1)); // 12345
System.out.println(“Start index of match: ” + m.start()); // 13
System.out.println(“End index of match: ” + m.end()); // 29
System.out.println(“Number of groups: ” + m.groupCount()); // 1
System.out.println(“Start index of group 1: ” + m.start(1)); // 24
System.out.println(“End index of group 1: ” + m.end(1)); // 29
}
“`
3.5. 替换文本
Matcher
类提供了方便的替换方法:
String replaceAll(String replacement)
: 将输入字符串中所有匹配到的子字符串替换为指定的替换字符串。替换字符串可以使用$n
来引用捕获组的内容($1
引用第一个捕获组,$2 引用第二个,等等)。String replaceFirst(String replacement)
: 将输入字符串中第一个匹配到的子字符串替换为指定的替换字符串。
“`java
String input = “Hello World, Hello Java!”;
Pattern p = Pattern.compile(“Hello”);
String output1 = p.matcher(input).replaceAll(“Hi”);
System.out.println(output1); // Hi World, Hi Java!
String output2 = p.matcher(input).replaceFirst(“Greetings”);
System.out.println(output2); // Greetings World, Hello Java!
// 使用捕获组进行替换
String input3 = “Name: John Doe, Phone: 123-456-7890”;
// 找到电话号码,并将其格式化为 (XXX) XXX-XXXX
Pattern p3 = Pattern.compile(“(\d{3})-(\d{3})-(\d{4})”);
String output3 = p3.matcher(input3).replaceAll(“($1) $2-$3”);
System.out.println(output3); // Name: John Doe, Phone: (123) 456-7890
“`
对于更复杂的替换逻辑(例如,替换内容依赖于匹配到的具体文本,或者需要对匹配到的文本进行一些计算后再替换),可以使用 appendReplacement()
和 appendTail()
方法:
“`java
String input = “Price: $10, Discount: $2”;
Pattern p = Pattern.compile(“\$(\d+)”); // 匹配 $ 后面的数字并捕获
Matcher m = p.matcher(input);
StringBuffer sb = new StringBuffer(); // 用于构建新的字符串
while (m.find()) {
int price = Integer.parseInt(m.group(1)); // 获取捕获的数字并转换为整数
int discountedPrice = price – 1; // 简单的计算
// 将匹配之前的内容和计算后的新内容添加到 sb
// m.appendReplacement(sb, “¥” + discountedPrice); // 替换为 ¥ + 折扣价
// 注意:替换字符串中的 $ 或 \ 需要进行转义,使用 Matcher.quoteReplacement() 是一种安全的方法
m.appendReplacement(sb, Matcher.quoteReplacement(“¥” + discountedPrice)); // 安全替换
}
// 将输入字符串中最后一次匹配之后的部分添加到 sb
m.appendTail(sb);
System.out.println(sb.toString()); // Price: ¥9, Discount: ¥1
``
appendReplacement(StringBuffer sb, String replacement)方法的作用是:
appendTail
1. 将当前匹配之前(即从上次或
appendReplacement结束位置到当前匹配开始位置)的输入字符串追加到
StringBuffer中。
replacement
2. 将字符串(其中可以使用
$n引用捕获组)追加到
StringBuffer` 中。
3. 更新匹配器内部的索引,指向当前匹配结束之后的位置。
appendTail(StringBuffer sb)
方法的作用是:将输入字符串中最后一次匹配结束之后的所有剩余部分追加到 StringBuffer
中。
这组方法通常在 while(m.find())
循环中使用,提供了对替换过程更精细的控制。
3.6. 分割字符串
Java 的 String
类有一个方便的 split()
方法,它可以使用正则表达式作为分隔符。
“`java
String input = “apple,banana;orange:grape”;
// 使用逗号、分号或冒号作为分隔符
String[] fruits = input.split(“[,;:]”);
for (String fruit : fruits) {
System.out.println(fruit);
}
// Output:
// apple
// banana
// orange
// grape
“`
4. 正则表达式标志 (Flags)
在编译正则表达式时,可以指定一个或多个标志来改变匹配的行为。这些标志是 Pattern
类中的静态常量。您可以使用按位或 (|
) 操作符来组合多个标志。
Pattern.compile(regex, flags)
常用标志:
Pattern.CASE_INSENSITIVE
(或Pattern.I
): 启用不区分大小写的匹配。Pattern.MULTILINE
(或Pattern.M
): 在多行模式下,^
和$
不仅匹配整个输入字符串的开始和结束,还匹配每一行的开始和结束(紧跟或紧前于换行符\n
或回车符\r
的位置)。Pattern.DOTALL
(或Pattern.S
): 在 dotall 模式下,点号.
匹配包括行终止符(换行符\n
)在内的任意字符。默认情况下,.
不匹配行终止符。Pattern.UNICODE_CHARACTER_CLASS
(或Pattern.U
): 启用 Unicode 版本的预定义字符类(\d
,\w
,\s
等)和 POSIX 字符类(\p{Lower}
,\p{Upper}
等)。这在处理非 ASCII 字符时非常有用。Pattern.COMMENTS
(或Pattern.X
): 在模式中忽略空白和#
后面的注释,直到行尾。这有助于编写更易读的复杂正则表达式。
“`java
String input = “Hello\nworld”;
// 默认模式,. 不匹配 \n
Pattern p1 = Pattern.compile(“.“);
Matcher m1 = p1.matcher(input);
if (m1.find()) System.out.println(“Default .: ” + m1.group()); // Hello
// Dotall 模式,. 匹配 \n
Pattern p2 = Pattern.compile(“.“, Pattern.DOTALL);
Matcher m2 = p2.matcher(input);
if (m2.find()) System.out.println(“DOTALL .: ” + m2.group()); // Hello\nworld
String input3 = “Line 1\nLine 2”;
// 多行模式,^ 匹配每行的开头
Pattern p3 = Pattern.compile(“^Line”, Pattern.MULTILINE);
Matcher m3 = p3.matcher(input3);
while (m3.find()) System.out.println(“MULTILINE ^Line: ” + m3.group());
// Output:
// MULTILINE ^Line: Line
// MULTILINE ^Line: Line
“`
标志也可以嵌入到正则表达式字符串的开头,使用 (?imsx)
这样的语法。例如,(?i)hello
表示不区分大小写匹配 “hello”。(?s).*(?-s).*
表示前半部分启用 dotall 模式,后半部分禁用。
5. Java 字符串中的反斜杠问题
这是 Java 中使用正则表达式最常见的困惑点之一。正则表达式本身使用反斜杠 \
作为转义字符(例如,\.
匹配字面上的点,\d
匹配数字)。然而,Java 字符串字面量也使用反斜杠 \
作为转义字符(例如,"\n"
表示换行,"\\"
表示字面上的反斜杠)。
因此,当您在 Java 字符串中表示一个正则表达式时,任何需要在正则表达式中转义的特殊字符,如果它是通过 \
进行转义的,那么这个 \
本身在 Java 字符串中也需要被转义。也就是说,Java 字符串中的 \\
在正则表达式引擎看来才是一个字面意义上的 \
。
- 要匹配字面上的点
.
,正则表达式是\.
。在 Java 字符串中表示为"\\."
。 - 要匹配字面上的反斜杠
\
,正则表达式是\\
。在 Java 字符串中表示为"\\\\"
。 - 要匹配一个数字
\d
,正则表达式是\d
。在 Java 字符串中表示为"\\d"
。
示例:
“`java
// 匹配格式为 xxx.xxx.xxx.xxx 的 IP 地址 (简化版,不验证数字范围)
String ipRegex = “\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}”;
// 在 Java 字符串中,. 变成了 \.,\d 变成了 \d
String input = “My IP is 192.168.1.100.”;
Pattern ipPattern = Pattern.compile(ipRegex);
Matcher ipMatcher = ipPattern.matcher(input);
if (ipMatcher.find()) {
System.out.println(“Found IP: ” + ipMatcher.group()); // Found IP: 192.168.1.100
}
“`
记住这个“双倍反斜杠”规则是使用 Java 正则表达式的关键。
6. 实际应用示例
示例 1:验证邮箱格式 (简化版)
一个简单的邮箱格式验证:[email protected]
“`java
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class EmailValidator {
// 一个简化版的邮箱正则表达式
// \w+ 匹配用户名部分 (字母、数字、下划线,至少一个)
// @ 匹配 @ 符号
// \w+ 匹配域名部分 (字母、数字、下划线,至少一个)
// . 匹配点号
// \w+ 匹配顶级域名 (字母、数字、下划线,至少一个)
private static final String EMAIL_REGEX = “^\w+@\w+\.\w+$”; // 注意双反斜杠
private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX);
public static boolean isValidEmail(String email) {
if (email == null) {
return false;
}
// 使用 matches() 确保整个字符串都符合模式
return EMAIL_PATTERN.matcher(email).matches();
}
public static void main(String[] args) {
String email1 = "[email protected]";
String email2 = "[email protected]"; // 更复杂的例子,上面的regex不匹配
String email3 = "[email protected]"; // 无效
String email4 = "test@example"; // 无效
System.out.println(email1 + " is valid: " + isValidEmail(email1)); // true
System.out.println(email2 + " is valid (by simple regex): " + isValidEmail(email2)); // false
System.out.println(email3 + " is valid: " + isValidEmail(email3)); // false
System.out.println(email4 + " is valid: " + isValidEmail(email4)); // false
// 更符合实际情况的邮箱正则会复杂得多,例如:
String complexEmailRegex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
System.out.println("\nUsing complex regex:");
System.out.println(email1 + " is valid: " + Pattern.matches(complexEmailRegex, email1)); // true
System.out.println(email2 + " is valid: " + Pattern.matches(complexEmailRegex, email2)); // true
System.out.println(email3 + " is valid: " + Pattern.matches(complexEmailRegex, email3)); // false
System.out.println(email4 + " is valid: " + Pattern.matches(complexEmailRegex, email4)); // false
}
}
``
Pattern.matches(regex, input)
**注意:** 实际生产环境中的邮箱验证需要使用更完善的正则表达式,甚至可能需要结合其他验证方式,因为完整的邮箱规范非常复杂。上面的例子是为了演示基本用法。是
Pattern.compile(regex).matcher(input).matches()` 的一个简写形式,适用于只需要一次性匹配整个字符串的场景。
示例 2:从文本中提取所有数字
“`java
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class ExtractNumbers {
public static void main(String[] args) {
String text = “Invoice amount is $123.45, discount is $10. Tax rate is 0.08.”;
// 匹配一个或多个数字,可能包含一个小数点后跟一个或多个数字
String regex = “\d+(\.\d+)?”;
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
System.out.println("Extracted numbers:");
while (matcher.find()) {
System.out.println(matcher.group()); // 提取整个匹配到的数字字符串
}
// Output:
// Extracted numbers:
// 123.45
// 10
// 0.08
}
}
“`
示例 3:使用 split() 分割 CSV 行 (简化)
假设有一个简单的 CSV 行,字段之间用逗号分隔,但字段内部可能包含空格。
“`java
public class SplitCsv {
public static void main(String[] args) {
String csvLine = “Smith, John, 42, New York”;
// 使用逗号后跟零个或多个空白字符作为分隔符
String[] fields = csvLine.split(“,\s*”);
System.out.println("CSV fields:");
for (String field : fields) {
System.out.println("-" + field.trim()); // 使用 trim() 移除字段两端的空白
}
// Output:
// CSV fields:
// -Smith
// -John
// -42
// -New York
}
}
“`
7. 常见陷阱与最佳实践
- 性能: 编译正则表达式是一个相对耗时的操作。如果同一个正则表达式需要多次使用,应该将其编译成
Pattern
对象并复用,而不是每次都调用静态方法Pattern.matches()
或String.split()
(这些方法内部会重新编译模式)。将Pattern
对象存储为类的常量通常是一个好的做法。 - 可读性: 复杂的正则表达式可能非常难以阅读和理解。考虑使用
Pattern.COMMENTS
标志 (Pattern.X
),在模式中加入注释和空白,或者将大型复杂的模式分解成更小的、命名清晰的部分(尽管这在 Java Regex 中实现起来不像一些其他语言那么直接)。对于特别复杂的解析任务,可能正则表达式不是最佳工具,考虑使用专用的解析库。 - 贪婪与勉强: 理解量词的贪婪性是避免意外匹配的关键。当您发现匹配结果比预期要长时,很可能是贪婪量词导致的,尝试使用勉强量词 (
?
)。 - 反斜杠逃逸: 再次强调,在 Java 字符串中表示正则表达式时,务必正确处理反斜杠。所有正则表达式中的
\
都必须写成 Java 字符串中的\\
。 - 过度使用: 正则表达式功能强大,但并非万能。对于简单的字符串检查(如是否包含某个子串、是否以某个前缀或后缀开始/结束),Java 的
String
类自带的方法(contains
、startsWith
、endsWith
、indexOf
)通常更直观且效率更高。 - 测试: 编写和调试正则表达式是出了名的困难。使用在线的正则表达式测试工具(如 regex101.com, regexr.com)可以极大地帮助您构建和理解模式。在 Java 代码中,使用小的、明确的测试用例来验证您的正则表达式行为是否符合预期。
- 安全性: 如果正则表达式的模式是来自用户输入或不可信源,需要警惕“正则表达式拒绝服务”(ReDoS)攻击。某些结构(如嵌套量词,如
(a+)+
)在匹配特定输入时会导致指数级的回溯时间,从而使程序崩溃或无响应。尽量避免使用易受 ReDoS 攻击的模式,或者对输入长度进行限制。
8. 总结
正则表达式是处理文本模式匹配和操作的利器。Java 的 java.util.regex
包提供了强大且灵活的 API 来利用这一能力。通过理解 Pattern
和 Matcher
这两个核心类,掌握基本的正则表达式语法元素(字符匹配、字符类、量词、锚点、分组、选择),并注意 Java 字符串中反斜杠的特殊处理,您就可以开始在您的 Java 项目中有效地使用正则表达式了。
记住,熟练掌握正则表达式需要时间和实践。从简单的模式开始,逐步构建和测试更复杂的模式。结合在线工具和单元测试,您将能够写出准确、高效的正则表达式,解决各种字符串处理难题。
希望这篇详细教程能帮助您建立坚实的 Java 正则表达式基础!