前言:大家好我将开一个新内容,是正则表达式,将创建一个集合,欢迎来学习。
今天我们来深入聊聊正则表达式的分组(group)原理。很多小伙伴对分组的概念似懂非懂,知道用括号,但到底怎么工作的,为啥能取出内容,可能就不太清楚了。今天,我就带着大家从源码的角度,把分组的原理彻底讲明白。
一、一个简单的例子
我们先来看一个简单的例子。假设我有正则表达式(\\d\\d)(\\d\\d)和字符串"1234"。很明显,这个正则表达式会匹配整个字符串,并且有两个分组:第一个(\\d\\d)匹配"12",第二个(\\d\\d)匹配"34"。
在Java中,我通常会这样写代码:
import java.util.regex.*;
public class Test {
public static void main(String[] args) {
String content = "后面是四个数字1234前面是四个数字";
String regStr = "(\\d\\d)(\\d\\d)";
Pattern pattern = Pattern.***pile(regStr);
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
System.out.println("整个匹配内容: " + matcher.group(0));
System.out.println("分组1内容: " + matcher.group(1));
System.out.println("分组2内容: " + matcher.group(2));
}
}
}
运行结果:
这个结果大家都能猜到。但是,matcher.group(1)为什么就能取出"12"?它底层是怎么实现的?这就是我们讨论的。
二、分组的原理:groups数组
在Matcher类中,有一个非常重要的数组,叫做groups。它是一个整型数组,用来记录每个分组匹配到的字符串在原始字符串中的起始和结束位置。
当我们调用matcher.find()方法时,正则表达式引擎会进行匹配操作。如果匹配成功,引擎会把匹配到的每个分组的位置信息记录在groups数组中。
对于我的例子(\\d\\d)(\\d\\d),一共有两个分组,加上整个表达式(第0组),所以一共有3组。每组需要两个索引:起始索引和结束索引。因此,groups数组的长度为6(3组*2)。
匹配成功后,groups数组的内容如下:
groups = [0, 4, 0, 2, 2, 4]
我来解释一下这个数组:
第0组(整个表达式):起始索引7,结束索引11(Java中结束索引是子字符串结束的下一个索引,所以是11,即"1234")
第1组(第一个括号):起始索引7,结束索引9,对应"12"
第2组(第二个括号):起始索引9,结束索引11,对应"34"
注意:数组索引0和1对应第0组,索引2和3对应第1组,索引4和5对应第2组。也就是说,对于第group组,它的起始索引在groups[group*2],结束索引在groups[group*2+1]。
这和py中的切片很像,前包后不包。
三、group方法源码解析
现在,我们来看group(int group)方法的源码。这个方法就是根据分组号,从groups数组中取出对应的起始和结束索引,然后从原始字符串中截取子串。
下面是group(int group)方法的源码(简化版,重点部分):
public String group(int group) {
if (first < 0) {
throw new IllegalStateException("No match found");
}
if (group < 0 || group > groupCount()) {
throw new IndexOutOfBoundsException("No group " + group);
}
if ((groups[group*2] == -1) || (groups[group*2+1] == -1)) {
return null;
}
return getSubSequence(groups[group*2], groups[group*2+1]).toString();
}
我们来一步步解析:
- 检查是否匹配成功:
first是匹配的起始索引,如果小于0,说明没有匹配,抛出异常。 - 检查分组号是否合法:分组号不能小于0,也不能大于总组数(
groupCount()返回的值)。 - 检查该分组是否有匹配:如果起始索引或结束索引为-1,说明该分组没有匹配到内容,返回null。
- 截取子串:根据
groups[group*2](起始索引)和groups[group*2+1](结束索引)从原始字符串中截取子串。
在我的例子中,当我调用matcher.group(1)时:
group=1,所以group*2=2,group*2+1=3
从groups数组中取出起始索引groups[2]=7,结束索引groups[3]=9
然后从原始字符串"后面是四个数字1234前面是四个数字"中截取索引从7到9(不包括9)的子串,即"12"
同理,matcher.group(2)截取的是从索引9到11的子串,即"34"。
四、总结
通过上面的解析,我可以总结出分组的原理:
- 分组是通过圆括号
()定义的,分组号从1开始,第0组代表整个表达式。 - 当
find()方法匹配成功后,会将每个分组的起始和结束索引记录在groups数组中。 -
group(int group)方法实际上是一个"查表"操作,根据分组号从groups数组中取出位置信息,然后截取子串。
所以,分组并不神秘,它的底层就是一个数组记录位置,然后按位置截取字符串。搞懂了这个原理,你对分组的理解就上了一个台阶。
五、感谢与互动
感谢大家耐心看完这篇技术解析!正则表达式的分组功能在日常开发中非常实用,理解其底层原理不仅能帮助我们更好地使用它,还能在遇到问题时快速定位原因。
如果你觉得这篇文章对你有帮助,欢迎:
- 点赞收藏 - 这是对我最大的鼓励!
- 留言讨论 - 如果有任何疑问或者想分享你的正则表达式使用经验,欢迎在评论区交流
- 关注我 - 我会持续分享更多技术干货和源码解析
正则表达式的世界很精彩,分组只是其中的一个基础但重要的概念。掌握了它,你就能更好地处理字符串匹配和提取的需求。希望这篇解析能成为你学习正则表达式道路上的一个有力助力!