本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
你好,我是看山。
正则表达式是Java变成中一把利器,常出现在文本检查、替换等逻辑中。本文中,我们将深入探讨 Java 正则表达式 API,并研究如何在 Java 编程语言中运用正则表达式。
在正则表达式的领域中,存在多种不同的“风格”可供选择,例如 grep、Perl、Python、PHP、awk 等等。这就意味着,在一种编程语言中有效的正则表达式,在另一种语言中可能无法正常工作。Java 中的正则表达式语法与 Perl 中的最为相似。
八、边界匹配器
Java 正则表达式 API 还支持边界匹配,如果我们需要判断匹配的位置,就可以使用边界匹配器。
要仅在文本开头满足所需正则表达式时进行匹配,我们使用“^”。下面的测试将通过,因为“dog”在文本开头:
int matches = runTest("^dog", "dogs are friendly");
assertTrue(matches > 0);
下面示例就会匹配失败:
int matches = runTest("^dog", "are dogs are friendly?");
assertFalse(matches > 0);
要仅在文本结尾满足所需正则表达式时进行匹配,我们使用“$”。在下面的情况下,我们会找到匹配:
int matches = runTest("dog$", "Man's best friend is a dog");
assertTrue(matches > 0);
下面示例就会匹配失败:
int matches = runTest("dog$", "is a dog man's best friend?");
assertFalse(matches > 0);
\\b
表示单词边界,正则表达是中所说的单词是指\w
+空格,即数字、大小写字母、下划线、空格。
int matches = runTest("\\bdog\\b", "a dog is friendly");
assertEquals(matches, 1);
行首的空字符串也是一个单词边界:
int matches = runTest("\\bdog\\b", "dog is man's best friend");
assertEquals(matches, 1);
下面这个示例则会无匹配字符:
int matches = runTest("\\bdog\\b", "snoop dogg is a rapper");
assertEquals(matches, 0);
两个连续的单词字符不标记为单词边界,但我们可以通过更改正则表达式的结尾来查找非单词边界,使其通过测试:
int matches = runTest("\\bdog\\B", "snoop dogg is a rapper");
assertEquals(matches, 1);
这里用到了\B
,表示非边界单词,与\b
正好相反。
九、Pattern 类方法
Pattern
的compile
方法可以传入一组标志,这些标志会影响匹配方式。
让我们在测试类中重载runTest
方法,使其能够接受一个标志:
public static int runTest(String regex, String text, int flags) {
Pattern pattern = Pattern.compile(regex, flags);
Matcher matcher = pattern.matcher(text);
int matches = 0;
while (matcher.find()) {
matches++;
}
return matches;
}
Pattern
提供了标记的常量,我们一一看一下。
Pattern.CANON_EQ
当指定此标志时,两个字符只有在其完整的规范分解匹配时才被视为匹配。主要用于组合码与组成元素之间的匹配。
比如,带重音的 Unicode 字符“é”。它的组合码点是“u00E9”,Unicode 也为其组成字符“e”(“u0065”)和锐音符(“u0301”)提供了单独的码点。如果启用CANON_EQ
模式,组合字符“u00E9”与两个字符序列“u0065 u0301”可以算作匹配的。
默认情况下,下面示例是不会命中:
int matches = runTest("\u00E9", "\u0065\u0301");
assertEquals(matches, 0);
如果我们设置了CANON_EQ
标志,测试将通过:
int matches = runTest("\u00E9", "\u0065\u0301", Pattern.CANON_EQ);
assertTrue(matches > 0);
Pattern.CASE_INSENSITIVE
此标志启用不区分大小写的匹配。
默认情况下,匹配是区分大小写的:
int matches = runTest("dog", "This is a Dog");
assertEquals(matches, 0);
我们可以使用CASE_INSENSITIVE
设置为不区分大小写:
int matches = runTest("dog", "This is a Dog", Pattern.CASE_INSENSITIVE);
assertTrue(matches > 0);
我们可以使用内联修饰符(?i)
,可以使正则表达式的匹配过程忽略大小写,使用(?-i)
关闭。
int matches = runTest("(?i)d(?-i)og", "This is a Dog");
assertTrue(matches > 0);
Pattern.COMMENTS
Java API 允许我们在正则表达式中使用“#”添加注释。这有助于为复杂的正则表达式添加文档说明,使其对其他程序员更易理解。
COMMENTS
标志可以忽略正则表达式中的任何空白字符或注释。
在默认匹配模式下,下面这种会无匹配:
int matches = runTest("dog$ #check for word dog at end of text", "This is a dog");
assertEquals(matches, 0);
这是因为匹配器会在输入文本中查找整个正则表达式,包括空格和“#”字符。但是当我们使用COMMENTS
标志时,它会忽略多余的空格,并且每行中以“#”开头的所有文本都将被视为注释而被忽略:
int matches = runTest("dog$ #check end of text", "This is a dog", Pattern.COMMENTS);
assertTrue(matches > 0);
这里也有一个内联修饰符(?x)
,可以启用扩展模式,在这种模式下:白空格(如空格、制表符、换行符等)在正则表达式中被忽略;可以使用 # 开始的注释,直到行尾。
比如:
int matches = runTest("(?x)dog$ #check end of text", "This is a dog");
assertTrue(matches > 0);
Pattern.DOTALL
默认情况下,当我们在正则表达式中使用点“.”表达式时,它会匹配输入字符串中的每个字符,直到遇到换行符为止。使用DOTALL
标志后,可以匹配换行符。
首先,来看默认行为:
Pattern pattern = Pattern.compile("(.*)");
Matcher matcher = pattern.matcher("this is a text" + System.getProperty("line.separator")
+ " continued on another line");
matcher.find();
assertEquals("this is a text", matcher.group(1));
如我们所见,匹配了换行符之前的部分。如果在DOTALL
模式下,整个文本,包括换行符,都会被匹配:
Pattern pattern = Pattern.compile("(.*)", Pattern.DOTALL);
Matcher matcher = pattern.matcher("this is a text" + System.getProperty("line.separator")
+ " continued on another line");
matcher.find();
assertEquals("this is a text" + System.getProperty("line.separator")
+ " continued on another line", matcher.group(1));
我们也可以使用内联表达式(?s)
开启单行模式,与DOTALL
标志行为相同。
Pattern pattern = Pattern.compile("(?s)(.*)");
Matcher matcher = pattern.matcher("this is a text" + System.getProperty("line.separator")
+ " continued on another line");
matcher.find();
assertEquals("this is a text" + System.getProperty("line.separator")
+ " continued on another line", matcher.group(1));
Pattern.LITERAL
在这种模式下,匹配器不会给任何元字符、转义字符或正则表达式语法赋予特殊含义。
在没有此标志时,匹配器会将以下正则表达式与任何输入字符串进行匹配:
int matches = runTest("(.*)", "text");
assertTrue(matches > 0);
使用LITERAL
标志时,如果输入字符串与正则表达式字面相同才会匹配,否则不会找到匹配,因为匹配器会查找(.*)
而不是对其进行解释:
int matches = runTest("(.*)", "text", Pattern.LITERAL);
assertEquals(matches, 0);
上面示例中,如果待匹配字符串变为text(.*)
,matches
就是1。
Pattern.MULTILINE
默认情况下,“^”和“$”元字符分别绝对匹配整个输入字符串的开头和结尾。匹配器会忽略任何换行符:
int matches = runTest("dog$",
"This is a dog" + System.getProperty("line.separator") + "this is a fox");
assertEquals(matches, 0);
这个匹配会失败,因为匹配器在整个字符串的末尾查找“dog”,但是示例中文本是分两行,会认为并没有结束。
使用MULTILINE
标志后,匹配器会考虑换行符,遇到换行符即认为结束:
int matches = runTest("dog$",
"This is a dog" + System.getProperty("line.separator") + "this is a fox",
Pattern.MULTILINE);
assertTrue(matches > 0);
我们也可以使用内联表达式(?m)
实现相同逻辑:
int matches = runTest("(?m)dog$",
"This is a dog" + System.getProperty("line.separator") + "this is a fox");
assertTrue(matches > 0);
十、Matcher 类方法
(一)索引方法
索引方法提供了有用的索引值,能准确地告诉我们在输入字符串中匹配项的位置。
在以下测试中,我们将确认输入字符串中“dog”的匹配起始和结束索引:
Pattern pattern = Pattern.compile("dog");
Matcher matcher = pattern.matcher("This dog is mine");
matcher.find();
assertEquals(5, matcher.start());
assertEquals(8, matcher.end());
(二)查找方法
查找方法遍历输入字符串,并返回一个布尔值,指示是否找到模式。常用的方法是matches
和lookingAt
。
matches
和lookingAt
方法都尝试将输入序列与模式进行匹配。区别在于matches
要求整个输入序列都匹配,而lookingAt
不需要。
这两个方法都从输入字符串的开头开始:
Pattern pattern = Pattern.compile("dog");
Matcher matcher = pattern.matcher("dogs are friendly");
assertTrue(matcher.lookingAt());
assertFalse(matcher.matches());
在以下情况下,matches
方法将返回 true:
Pattern pattern = Pattern.compile("dog");
Matcher matcher = pattern.matcher("dog");
assertTrue(matcher.matches());
(三)替换方法
替换方法用于替换输入字符串中的文本。常用的方法是replaceFirst
和replaceAll
。
replaceFirst
和replaceAll
方法替换与给定正则表达式匹配的文本。顾名思义,replaceFirst
替换第一次出现的匹配项,replaceAll
替换所有匹配项:
Pattern pattern = Pattern.compile("dog");
Matcher matcher = pattern.matcher("dogs are domestic animals, dogs are friendly");
String newStr = matcher.replaceFirst("cat");
assertEquals("cats are domestic animals, dogs are friendly", newStr);
替换所有匹配项:
Pattern pattern = Pattern.compile("dog");
Matcher matcher = pattern.matcher("dogs are domestic animals, dogs are friendly");
String newStr = matcher.replaceAll("cat");
assertEquals("cats are domestic animals, cats are friendly", newStr);
replaceAll
方法允许我们用相同的替换内容替换所有匹配项。
String
类的replaceFirst
和replaceAll
方法啊,底层也是使用了Matcher
的方法。
文末总结
正则表达式是一个开发利器,用的好的话,会大大提升我们的开发效率。本文介绍了边界匹配、Pattern模式、Matcher方法。
青山不改,绿水长流,我们下次见。
推荐阅读
- 从小工到专家的 Java 进阶之旅
- 并发编程
- MapStruct实用手册
- 一个架构师的素养修炼
- 一文掌握 Java8 Stream 中 Collectors 的 24 个操作
- 一文掌握 Java8 的 Optional 的 6 种操作
- 使用 Lambda 表达式实现超强的排序功能
- Java11 中基于嵌套关系的访问控制优化
- 原来OpenFeign功能这么强大
- 原来OpenFeign功能这么强大(第二弹)
- 使用OpenFeign的5个步骤和7个高级功能
- 由浅入深掌握CompletableFuture的七种用法
- 全新的HttpClient,现代高效的网络通信利器
- Java 中反射、内省的性能差距居然如此
- 比反射更好用的内省
- 异常处理的9条建议
- Java原生支持Lombok了
- MapStruct教程
- 手写重试器
- 等待线程完成的三种常用方式
- 如何在线程完成时执行回调
- 由浅入深掌握Future
- 由浅入深掌握ExecutorService
- Fork/Join框架快速上手指南
- 如何延迟执行一段代码
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
- 个人主页:https://www.howardliu.cn
- 个人博文:正则表达式实用指南(三):边界匹配、Pattern模式、Matcher方法
- CSDN 主页:https://kanshan.blog.csdn.net/
- CSDN 博文:正则表达式实用指南(三):边界匹配、Pattern模式、Matcher方法