Java 正则

本文以及示例源码已归档在 javacore

简介

正则表达式是什么?有什么用?

**正则表达式(Regular Expression)是一种文本规则,可以用来校验查找替换与规则匹配的文本。

又爱又恨的正则

正则表达式是一个强大的文本匹配工具,但是它的规则实在很繁琐,而且理解起来也颇为蛋疼,容易让人望而生畏。

如何学习正则

刚接触正则时,我看了一堆正则的语义说明,但是仍然不明所以。后来,我多接触一些正则的应用实例,渐渐有了感觉,再结合语义说明,终有领悟。我觉得正则表达式和武侠修练武功差不多,应该先练招式,再练心法。如果一开始就直接看正则的规则,保证你会懵逼。当你熟悉基本招式(正则基本使用案例)后,也该修炼修炼心法(正则语法)了。真正的高手不能只靠死记硬背那么几招把式。就像张三丰教张无忌太极拳一样,领悟心法,融会贯通,少侠你就可以无招胜有招,成为传说中的绝世高手。

以上闲话可归纳为一句:学习正则应该从实例去理解规则。

招式篇

JDK 中的java.util.regex包提供了对正则表达式的支持。

java.util.regex有三个核心类:

  • Pattern 类:Pattern是一个正则表达式的编译表示。
  • Matcher 类:Matcher是对输入字符串进行解释和匹配操作的引擎。
  • PatternSyntaxException:PatternSyntaxException是一个非强制异常类,它表示一个正则表达式模式中的语法错误。

***注:***需要格外注意一点,在 Java 中使用反斜杠"\“时必须写成 "\\"。所以本文的代码出现形如String regex = "\\$\\{.*?\\}" 其实就是”\$\{.*?\}",不要以为是画风不对哦。

Pattern 类

Pattern类没有公共构造方法。要创建一个Pattern对象,你必须首先调用其静态方法compile,加载正则规则字符串,然后返回一个 Pattern 对象。

Pattern类一样,Matcher类也没有公共构造方法。你需要调用Pattern对象的matcher方法来获得一个Matcher对象。

案例:Pattern 和 Matcher 的初始化

Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(content);

Matcher 类

Matcher类可以说是java.util.regex核心类中的必杀技!

Matcher类有三板斧(三类功能):

  • 校验
  • 查找
  • 替换

下面我们来领略一下这三块的功能。

校验文本是否与正则规则匹配

为了检查文本是否与正则规则匹配,Matcher 提供了以下几个返回值为boolean的方法。

序号 方法及说明
1 **public boolean lookingAt() ** 尝试将从区域开头开始的输入序列与该模式匹配。
2 **public boolean find() **尝试查找与该模式匹配的输入序列的下一个子序列。
3 **public boolean find(int start)**重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列。
4 **public boolean matches() **尝试将整个区域与模式匹配。

如果你傻傻分不清上面的查找方法有什么区别,那么下面一个例子就可以让你秒懂。

案例:lookingAt vs find vs matches
public static void main(String[] args) {
	checkLookingAt("hello", "helloworld");
	checkLookingAt("world", "helloworld");

	checkFind("hello", "helloworld");
	checkFind("world", "helloworld");

	checkMatches("hello", "helloworld");
	checkMatches("world", "helloworld");
	checkMatches("helloworld", "helloworld");
}

private static void checkLookingAt(String regex, String content) {
	Pattern p = Pattern.compile(regex);
	Matcher m = p.matcher(content);
	if (m.lookingAt()) {
		System.out.println(content + "\tlookingAt: " + regex);
	} else {
		System.out.println(content + "\tnot lookingAt: " + regex);
	}
}

private static void checkFind(String regex, String content) {
	Pattern p = Pattern.compile(regex);
	Matcher m = p.matcher(content);
	if (m.find()) {
		System.out.println(content + "\tfind: " + regex);
	} else {
		System.out.println(content + "\tnot find: " + regex);
	}
}

private static void checkMatches(String regex, String content) {
	Pattern p = Pattern.compile(regex);
	Matcher m = p.matcher(content);
	if (m.matches()) {
		System.out.println(content + "\tmatches: " + regex);
	} else {
		System.out.println(content + "\tnot matches: " + regex);
	}
}

输出

helloworld	lookingAt: hello
helloworld	not lookingAt: world
helloworld	find: hello
helloworld	find: world
helloworld	not matches: hello
helloworld	not matches: world
helloworld	matches: helloworld

说明

regex = “world” 表示的正则规则是以 world 开头的字符串,regex = “hello” 和 regex = “helloworld” 也是同理。

  • lookingAt方法从头部开始,检查 content 字符串是否有子字符串于正则规则匹配。
  • find方法检查 content 字符串是否有子字符串于正则规则匹配,不管字符串所在位置。
  • matches方法检查 content 字符串整体是否与正则规则匹配。

查找匹配正则规则的文本位置

为了查找文本匹配正则规则的位置,Matcher提供了以下方法:

序号 方法及说明
1 **public int start() **返回以前匹配的初始索引。
2 public int start(int group) 返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引
3 **public int end()**返回最后匹配字符之后的偏移量。
4 **public int end(int group)**返回在以前的匹配操作期间,由给定组所捕获子序列的最后字符之后的偏移量。
5 **public String group()**返回前一个符合匹配条件的子序列。
6 **public String group(int group)**返回指定的符合匹配条件的子序列。
案例:使用 start()、end()、group() 查找所有匹配正则条件的子序列
public static void main(String[] args) {
	final String regex = "world";
	final String content = "helloworld helloworld";
	Pattern p = Pattern.compile(regex);
	Matcher m = p.matcher(content);
	System.out.println("content: " + content);

	int i = 0;
	while (m.find()) {
		i++;
		System.out.println("[" + i + "th] found");
		System.out.print("start: " + m.start() + ", ");
		System.out.print("end: " + m.end() + ", ");
		System.out.print("group: " + m.group() + "\n");
	}
}

输出

content: helloworld helloworld
[1th] found
start: 5, end: 10, group: world
[2th] found
start: 16, end: 21, group: world

说明

例子很直白,不言自明了吧。

替换匹配正则规则的文本

替换方法是替换输入字符串里文本的方法:

序号 方法及说明
1 **public Matcher appendReplacement(StringBuffer sb, String replacement)**实现非终端添加和替换步骤。
2 **public StringBuffer appendTail(StringBuffer sb)**实现终端添加和替换步骤。
3 **public String replaceAll(String replacement) ** 替换模式与给定替换字符串相匹配的输入序列的每个子序列。
4 public String replaceFirst(String replacement) 替换模式与给定替换字符串匹配的输入序列的第一个子序列。
5 **public static String quoteReplacement(String s)**返回指定字符串的字面替换字符串。这个方法返回一个字符串,就像传递给 Matcher 类的 appendReplacement 方法一个字面字符串一样工作。
案例:replaceFirst vs replaceAll
public static void main(String[] args) {
	String regex = "can";
	String replace = "can not";
	String content = "I can because I think I can.";

	Pattern p = Pattern.compile(regex);
	Matcher m = p.matcher(content);

	System.out.println("content: " + content);
	System.out.println("replaceFirst: " + m.replaceFirst(replace));
	System.out.println("replaceAll: " + m.replaceAll(replace));
}

输出

content: I can because I think I can.
replaceFirst: I can not because I think I can.
replaceAll: I can not because I think I can not.

说明

replaceFirst:替换第一个匹配正则规则的子序列。

replaceAll:替换所有匹配正则规则的子序列。

案例:appendReplacement、appendTail 和 replaceAll
public static void main(String[] args) {
	String regex = "can";
	String replace = "can not";
	String content = "I can because I think I can.";
	StringBuffer sb = new StringBuffer();
	StringBuffer sb2 = new StringBuffer();

	System.out.println("content: " + content);
	Pattern p = Pattern.compile(regex);
	Matcher m = p.matcher(content);
	while (m.find()) {
		m.appendReplacement(sb, replace);
	}
	System.out.println("appendReplacement: " + sb);
	m.appendTail(sb);
	System.out.println("appendTail: " + sb);
}

输出

content: I can because I think I can.
appendReplacement: I can not because I think I can not
appendTail: I can not because I think I can not.

说明

从输出结果可以看出,appendReplacementappendTail方法组合起来用,功能和replaceAll是一样的。

如果你查看replaceAll的源码,会发现其内部就是使用appendReplacementappendTail方法组合来实现的。

案例:quoteReplacement 和 replaceAll,解决特殊字符替换问题
public static void main(String[] args) {
	String regex = "\\$\\{.*?\\}";
	String replace = "${product}";
	String content = "product is ${productName}.";

	Pattern p = Pattern.compile(regex);
	Matcher m = p.matcher(content);
	String replaceAll = m.replaceAll(replace);

	System.out.println("content: " + content);
	System.out.println("replaceAll: " + replaceAll);
}

输出

Exception in thread "main" java.lang.IllegalArgumentException: No group with name {product}
	at java.util.regex.Matcher.appendReplacement(Matcher.java:849)
	at java.util.regex.Matcher.replaceAll(Matcher.java:955)
	at org.zp.notes.javase.regex.RegexDemo.wrongMethod(RegexDemo.java:42)
	at org.zp.notes.javase.regex.RegexDemo.main(RegexDemo.java:18)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

说明

String regex = "\\$\\{.*?\\}";表示匹配类似${name}这样的字符串。由于${}都是特殊字符,需要用反义字符\来修饰才能被当做一个字符串字符来处理。

上面的例子是想将 ${productName} 替换为 ${product} ,然而replaceAll方法却将传入的字符串中的$当做特殊字符来处理了。结果产生异常。

如何解决这个问题?

JDK1.5 引入了quoteReplacement方法。它可以用来转换特殊字符。其实源码非常简单,就是判断字符串中如果有\$,就为它加一个转义字符\

我们对上面的代码略作调整:

m.replaceAll(replace)改为m.replaceAll(Matcher.quoteReplacement(replace)),新代码如下:

public static void main(String[] args) {
	String regex = "\\$\\{.*?\\}";
	String replace = "${product}";
	String content = "product is ${productName}.";

	Pattern p = Pattern.compile(regex);
	Matcher m = p.matcher(content);
	String replaceAll = m.replaceAll(Matcher.quoteReplacement(replace));

	System.out.println("content: " + content);
	System.out.println("replaceAll: " + replaceAll);
}

输出

content: product is ${productName}.
replaceAll: product is ${product}.

说明

字符串中如果有\$,不能被正常解析的问题解决。

心法篇

为了理解下面章节的内容,你需要先了解一些基本概念。

正则表达式

正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。

元字符

元字符(metacharacters)就是正则表达式中具有特殊意义的专用字符。

普通字符

普通字符包括没有显式指定为元字符的所有可打印和不可打印字符。这包括所有大写和小写字母、所有数字、所有标点符号和一些其他符号。

元字符

基本元字符

正则表达式的元字符难以记忆,很大程度上是因为有很多为了简化表达而出现的等价字符。

而实际上最基本的元字符,并没有那么多。对于大部分的场景,基本元字符都可以搞定。

让我们从一个个实例出发,由浅入深的去体会正则的奥妙。

多选 - |

例 匹配一个确定的字符串

checkMatches("abc", "abc");

如果要匹配一个确定的字符串,非常简单,如例 1 所示。

如果你不确定要匹配的字符串,希望有多个选择,怎么办?

答案是:使用元字符| ,它的含义是或。

例 匹配多个可选的字符串

// 测试正则表达式字符:|
Assert.assertTrue(checkMatches("yes|no", "yes"));
Assert.assertTrue(checkMatches("yes|no", "no"));
Assert.assertFalse(checkMatches("yes|no", "right"));

输出

yes	matches: yes|no
no	matches: yes|no
right	not matches: yes|no
分组 - ()

如果你希望表达式由多个子表达式组成,你可以使用 ()

例 匹配组合字符串

Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "ended"));
Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "ending"));
Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "playing"));
Assert.assertTrue(checkMatches("(play|end)(ing|ed)", "played"));

输出

ended	matches: (play|end)(ing|ed)
ending	matches: (play|end)(ing|ed)
playing	matches: (play|end)(ing|ed)
played	matches: (play|end)(ing|ed)
指定单字符有效范围 - []

前面展示了如何匹配字符串,但是很多时候你需要精确的匹配一个字符,这时可以使用[]

例 字符在指定范围

// 测试正则表达式字符:[]
Assert.assertTrue(checkMatches("[abc]", "b"));  // 字符只能是a、b、c
Assert.assertTrue(checkMatches("[a-z]", "m")); // 字符只能是a - z
Assert.assertTrue(checkMatches("[A-Z]", "O")); // 字符只能是A - Z
Assert.assertTrue(checkMatches("[a-zA-Z]", "K")); // 字符只能是a - z和A - Z
Assert.assertTrue(checkMatches("[a-zA-Z]", "k"));
Assert.assertTrue(checkMatches("[0-9]", "5")); // 字符只能是0 - 9

输出

b	matches: [abc]
m	matches: [a-z]
O	matches: [A-Z]
K	matches: [a-zA-Z]
k	matches: [a-zA-Z]
5	matches: [0-9]
指定单字符无效范围 - [^]

例 字符不能在指定范围

如果需要匹配一个字符的逆操作,即字符不能在指定范围,可以使用[^]

// 测试正则表达式字符:[^]
Assert.assertFalse(checkMatches("[^abc]", "b")); // 字符不能是a、b、c
Assert.assertFalse(checkMatches("[^a-z]", "m")); // 字符不能是a - z
Assert.assertFalse(checkMatches("[^A-Z]", "O")); // 字符不能是A - Z
Assert.assertFalse(checkMatches("[^a-zA-Z]", "K")); // 字符不能是a - z和A - Z
Assert.assertFalse(checkMatches("[^a-zA-Z]", "k"));
Assert.assertFalse(checkMatches("[^0-9]", "5")); // 字符不能是0 - 9

输出

b	not matches: [^abc]
m	not matches: [^a-z]
O	not matches: [^A-Z]
K	not matches: [^a-zA-Z]
k	not matches: [^a-zA-Z]
5	not matches: [^0-9]
限制字符数量 - {}

如果想要控制字符出现的次数,可以使用{}

字符 描述
{n} n 是一个非负整数。匹配确定的 n 次。
{n,} n 是一个非负整数。至少匹配 n 次。
{n,m} m 和 n 均为非负整数,其中 n <= m。最少匹配 n 次且最多匹配 m 次。

例 限制字符出现次数

// {n}: n 是一个非负整数。匹配确定的 n 次。
checkMatches("ap{1}", "a");
checkMatches("ap{1}", "ap");
checkMatches("ap{1}", "app");
checkMatches("ap{1}", "apppppppppp");

// {n,}: n 是一个非负整数。至少匹配 n 次。
checkMatches("ap{1,}", "a");
checkMatches("ap{1,}", "ap");
checkMatches("ap{1,}", "app");
checkMatches("ap{1,}", "apppppppppp");

// {n,m}: m 和 n 均为非负整数,其中 n <= m。最少匹配 n 次且最多匹配 m 次。
checkMatches("ap{2,5}", "a");
checkMatches("ap{2,5}", "ap");
checkMatches("ap{2,5}", "app");
checkMatches("ap{2,5}", "apppppppppp");

输出

a	not matches: ap{1}
ap	matches: ap{1}
app	not matches: ap{1}
apppppppppp	not matches: ap{1}
a	not matches: ap{1,}
ap	matches: ap{1,}
app	matches: ap{1,}
apppppppppp	matches: ap{1,}
a	not matches: ap{2,5}
ap	not matches: ap{2,5}
app	matches: ap{2,5}
apppppppppp	not matches: ap{2,5}
转义字符 - /

如果想要查找元字符本身,你需要使用转义符,使得正则引擎将其视作一个普通字符,而不是一个元字符去处理。

* 的转义字符:\*
+ 的转义字符:\+
? 的转义字符:\?
^ 的转义字符:\^
$ 的转义字符:\$
. 的转义字符:\.

如果是转义符\本身,你也需要使用\\

指定表达式字符串的开始和结尾 - ^、$

如果希望匹配的字符串必须以特定字符串开头,可以使用^

注:请特别留意,这里的^ 一定要和 [^] 中的 “^” 区分。

例 限制字符串头部

Assert.assertTrue(checkMatches("^app[a-z]{0,}", "apple")); // 字符串必须以app开头
Assert.assertFalse(checkMatches("^app[a-z]{0,}", "aplause"));

输出

apple	matches: ^app[a-z]{0,}
aplause	not matches: ^app[a-z]{0,}

如果希望匹配的字符串必须以特定字符串开头,可以使用$

例 限制字符串尾部

Assert.assertTrue(checkMatches("[a-z]{0,}ing$", "playing")); // 字符串必须以ing结尾
Assert.assertFalse(checkMatches("[a-z]{0,}ing$", "long"));

输出

playing	matches: [a-z]{0,}ing$
long	not matches: [a-z]{0,}ing$

等价字符

等价字符,顾名思义,就是对于基本元字符表达的一种简化(等价字符的功能都可以通过基本元字符来实现)。

在没有掌握基本元字符之前,可以先不用理会,因为很容易把人绕晕。

等价字符的好处在于简化了基本元字符的写法。

表示某一类型字符的等价字符

下表中的等价字符都表示某一类型的字符。

字符 描述
. 匹配除“\n”之外的任何单个字符。
\d 匹配一个数字字符。等价于[0-9]。
\D 匹配一个非数字字符。等价于[^0-9]。
\w 匹配包括下划线的任何单词字符。类似但不等价于“[A-Za-z0-9_]”,这里的单词字符指的是 Unicode 字符集。
\W 匹配任何非单词字符。
\s 匹配任何不可见字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。
\S 匹配任何可见字符。等价于[ \f\n\r\t\v]。

案例 基本等价字符的用法

// 匹配除“\n”之外的任何单个字符
Assert.assertTrue(checkMatches(".{1,}", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"));
Assert.assertTrue(checkMatches(".{1,}", "~!@#$%^&*()+`-=[]{};:<>,./?|\\"));
Assert.assertFalse(checkMatches(".", "\n"));
Assert.assertFalse(checkMatches("[^\n]", "\n"));

// 匹配一个数字字符。等价于[0-9]
Assert.assertTrue(checkMatches("\\d{1,}", "0123456789"));
// 匹配一个非数字字符。等价于[^0-9]
Assert.assertFalse(checkMatches("\\D{1,}", "0123456789"));

// 匹配包括下划线的任何单词字符。类似但不等价于“[A-Za-z0-9_]”,这里的单词字符指的是Unicode字符集
Assert.assertTrue(checkMatches("\\w{1,}", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"));
Assert.assertFalse(checkMatches("\\w{1,}", "~!@#$%^&*()+`-=[]{};:<>,./?|\\"));
// 匹配任何非单词字符
Assert.assertFalse(checkMatches("\\W{1,}", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"));
Assert.assertTrue(checkMatches("\\W{1,}", "~!@#$%^&*()+`-=[]{};:<>,./?|\\"));

// 匹配任何不可见字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]
Assert.assertTrue(checkMatches("\\s{1,}", " \f\r\n\t"));
// 匹配任何可见字符。等价于[^ \f\n\r\t\v]
Assert.assertFalse(checkMatches("\\S{1,}", " \f\r\n\t"));

输出

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_	matches: .{1,}
~!@#$%^&*()+`-=[]{};:<>,./?|\\	matches: .{1,}
\n	not matches: .
\n	not matches: [^\n]
0123456789	matches: \\d{1,}
0123456789	not matches: \\D{1,}
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_	matches: \\w{1,}
~!@#$%^&*()+`-=[]{};:<>,./?|\\	not matches: \\w{1,}
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_	not matches: \\W{1,}
~!@#$%^&*()+`-=[]{};:<>,./?|\\	matches: \\W{1,}
 \f\r\n\t	matches: \\s{1,}
 \f\r\n\t	not matches: \\S{1,}
限制字符数量的等价字符

在基本元字符章节中,已经介绍了限制字符数量的基本元字符 - {}

此外,还有 *+? 这个三个为了简化写法而出现的等价字符,我们来认识一下。

字符 描述
* 匹配前面的子表达式零次或多次。等价于{0,}。
+ 匹配前面的子表达式一次或多次。等价于{1,}。
? 匹配前面的子表达式零次或一次。等价于 {0,1}。

案例 限制字符数量的等价字符

// *: 匹配前面的子表达式零次或多次。* 等价于{0,}。
checkMatches("ap*", "a");
checkMatches("ap*", "ap");
checkMatches("ap*", "app");
checkMatches("ap*", "apppppppppp");

// +: 匹配前面的子表达式一次或多次。+ 等价于 {1,}。
checkMatches("ap+", "a");
checkMatches("ap+", "ap");
checkMatches("ap+", "app");
checkMatches("ap+", "apppppppppp");

// ?: 匹配前面的子表达式零次或一次。? 等价于 {0,1}。
checkMatches("ap?", "a");
checkMatches("ap?", "ap");
checkMatches("ap?", "app");
checkMatches("ap?", "apppppppppp");

输出

a	matches: ap*
ap	matches: ap*
app	matches: ap*
apppppppppp	matches: ap*
a	not matches: ap+
ap	matches: ap+
app	matches: ap+
apppppppppp	matches: ap+
a	matches: ap?
ap	matches: ap?
app	not matches: ap?
apppppppppp	not matches: ap?

元字符优先级顺序

正则表达式从左到右进行计算,并遵循优先级顺序,这与算术表达式非常类似。

下表从最高到最低说明了各种正则表达式运算符的优先级顺序:

运算符 说明
\ 转义符
(), (?, (?=), [] 括号和中括号
*, +, ?, {n}, {n,}, {n,m} 限定符
^, $, *任何元字符、任何字符* 定位点和序列
| 替换

字符具有高于替换运算符的优先级,使得“m|food”匹配“m”或“food”。若要匹配“mood”或“food”,请使用括号创建子表达式,从而产生“(m|f)ood”。

分组构造

在基本元字符章节,提到了 () 字符可以用来对表达式分组。实际上分组还有更多复杂的用法。

所谓分组构造,是用来描述正则表达式的子表达式,用于捕获字符串中的子字符串。

捕获与非捕获

下表为分组构造中的捕获和非捕获分类。

表达式 描述 捕获或非捕获
(exp) 匹配的子表达式 捕获
(?exp) 命名的反向引用 捕获
(?:exp) 非捕获组 非捕获
(?=exp) 零宽度正预测先行断言 非捕获
(?!exp) 零宽度负预测先行断言 非捕获
(?<=exp) 零宽度正回顾后发断言 非捕获
(? 零宽度负回顾后发断言 非捕获

注:Java 正则引擎不支持平衡组。

反向引用

带编号的反向引用

带编号的反向引用使用以下语法:\number

其中number 是正则表达式中捕获组的序号位置。 例如,\4 匹配第四个捕获组的内容。 如果正则表达式模式中未定义number,则将发生分析错误

例 匹配重复的单词和紧随每个重复的单词的单词(不命名子表达式)

// (\w+)\s\1\W(\w+) 匹配重复的单词和紧随每个重复的单词的单词
Assert.assertTrue(findAll("(\\w+)\\s\\1\\W(\\w+)",
		"He said that that was the the correct answer.") > 0);

输出

regex = (\w+)\s\1\W(\w+), content: He said that that was the the correct answer.
[1th] start: 8, end: 21, group: that that was
[2th] start: 22, end: 37, group: the the correct

说明

(\w+): 匹配一个或多个单词字符。
\s: 与空白字符匹配。
\1: 匹配第一个组,即(\w+)。
\W: 匹配包括空格和标点符号的一个非单词字符。 这样可以防止正则表达式模式匹配从第一个捕获组的单词开头的单词。

命名的反向引用

命名后

你可能感兴趣的:(Java,扫盲,正则表达式)