简介

Pattern,正则表达式的编译表示,操作字符序列的利器。

整个Pattern是一个树形结构(对应于表达式中的‘|’),一般为链表结构,树(链表)的基本元素是Node结点,Node有各种各样的子结点,以满足不同的匹配模式。

样例1

以一个最简单的样例,走进源码。

     public static void example() {
String regex = "EXAMPLE";
String text = "HERE IS A SIMPLE EXAMPLE";
Pattern pattern = Pattern.compile(regex, Pattern.LITERAL);
Matcher matcher = pattern.matcher(text);
matcher.find();
}

这个样例实现了查找字串的功能。

Pattern.compile(String regex)

     public static Pattern compile(String regex) {
return new Pattern(regex, 0);
}

这个方法通过调用构造方法返回一个Pattern对象。

构造方法

     private Pattern(String p, int f) {
pattern = p;
flags = f; if ((flags & UNICODE_CHARACTER_CLASS) != 0)
flags |= UNICODE_CASE; capturingGroupCount = 1;
localCount = 0; if (pattern.length() > 0) {
compile();
} else {
root = new Start(lastAccept);
matchRoot = lastAccept;
}
}

构造方法又调用compile()方法。

compile()

     private void compile() {
if (has(CANON_EQ) && !has(LITERAL)) {
normalize(); // 标准化
} else {
normalizedPattern = pattern;
}
patternLength = normalizedPattern.length(); temp = new int[patternLength + 2]; // 将pattern字符的代码点(codePoint)存在int数组中,多出2个槽,标识结束 hasSupplementary = false;
int c, count = 0;
for (int x = 0; x < patternLength; x += Character.charCount(c)) {
c = normalizedPattern.codePointAt(x);
if (isSupplementary(c)) { // 确定指定的代码点是否为辅助字符或未配对的代理
hasSupplementary = true;
}
temp[count++] = c; // 存到数组中
} patternLength = count; // 现在是代码点的个数 if (!has(LITERAL))
RemoveQEQuoting(); // 处理\Q...\E的情况 buffer = new int[32]; // 分配临时对象
groupNodes = new GroupHead[10]; // 组
namedGroups = null; if (has(LITERAL)) { // 纯文本,示例会走这个分支
matchRoot = newSlice(temp, patternLength, hasSupplementary); // Slice结点
matchRoot.next = lastAccept;
} else {
matchRoot = expr(lastAccept); // 递归解析表达式
if (patternLength != cursor) { // 处理异常情况
if (peek() == ')') {
throw error("Unmatched closing ')'");
} else {
throw error("Unexpected internal error");
}
}
} if (matchRoot instanceof Slice) { // 如果是文本模式,则返回BnM结点(Boyer Moore算法,处理子字符串的高效算法)
root = BnM.optimize(matchRoot);
if (root == matchRoot) {
root = hasSupplementary ? new StartS(matchRoot) : new Start(matchRoot); // Start和LastNode(lastAccept)是首尾两个结点,通用处理
}
} else if (matchRoot instanceof Begin || matchRoot instanceof First) { // Begin和End也是结点类型,大概是处理多行模式,不展开讨论
root = matchRoot;
} else {
root = hasSupplementary ? new StartS(matchRoot) : new Start(matchRoot);
}
// 清理工作
temp = null;
buffer = null;
groupNodes = null;
patternLength = 0;
compiled = true;
}
  1. 首先标准化表达式
  2. 将字符代码点暂存int数组中,所谓代码点指的是字符集里每个字符的编号,从0开始,常见的字符集ASCII和Unicode
  3. 返回相应类型的结点
  4. root和matchRoot的关系,root表示可以从给定文本的任意位置开始查找,matchRoot表示全字符匹配(从头到尾)

先看正则表达式是文本的分支,即样例中所示。

newSlice(int[] buf, int count, boolean hasSupplementary)

     private Node newSlice(int[] buf, int count, boolean hasSupplementary) {
int[] tmp = new int[count];
if (has(CASE_INSENSITIVE)) {
if (has(UNICODE_CASE)) {
for (int i = 0; i < count; i++) {
tmp[i] = Character.toLowerCase(Character.toUpperCase(buf[i]));
}
return hasSupplementary ? new SliceUS(tmp) : new SliceU(tmp);
}
for (int i = 0; i < count; i++) {
tmp[i] = ASCII.toLower(buf[i]);
}
return hasSupplementary ? new SliceIS(tmp) : new SliceI(tmp);
}
for (int i = 0; i < count; i++) {
tmp[i] = buf[i];
}
return hasSupplementary ? new SliceS(tmp) : new Slice(tmp);
}

该方法主要处理了一些情况,比如是否关心大小写等,直接看最后一句,根据hasSupplementary的值决定初始化SliceS还是Slice,在此只关心Slice的情况。

数据结构Slice

     static final class Slice extends SliceNode {
Slice(int[] buf) {
super(buf);
} boolean match(Matcher matcher, int i, CharSequence seq) {
int[] buf = buffer;
int len = buf.length;
for (int j = 0; j < len; j++) { // 从第一个字符开始比较,如果长度不等,或遇到不等的字符,返回false,否则调用next结点的match方法
if ((i + j) >= matcher.to) {
matcher.hitEnd = true;
return false;
}
if (buf[j] != seq.charAt(i + j))
return false;
}
return next.match(matcher, i + len, seq);
}
}

该类继承了SliceNode,主要实现了match方法,该方法查看给定文本是否与给定表达式相等,从头开始一个字符一个字符地比较。

SliceNode

     static class SliceNode extends Node {
int[] buffer;
SliceNode(int[] buf) {
buffer = buf;
}
boolean study(TreeInfo info) {
info.minLength += buffer.length;
info.maxLength += buffer.length;
return next.study(info);
}
}

所有Slice结点的基类,实现了Node结点,主要的study方法,累加TreeInfo的最小长度和最大长度。

Node

     static class Node extends Object {
Node next; Node() {
next = Pattern.accept;
} boolean match(Matcher matcher, int i, CharSequence seq) {
matcher.last = i;
matcher.groups[0] = matcher.first; // 默认是一组(组[0-1])
matcher.groups[1] = matcher.last;
return true;
} boolean study(TreeInfo info) { // 零长度断言
if (next != null) {
return next.study(info);
} else {
return info.deterministic;
}
}
}

顶级结点,match方法总是返回true,子类应重写此方法,

group, 调用链如下:getSubSequence(groups[group * 2], groups[group * 2 + 1]) ---> CharSequence#subSequence(int start, int end).

每2个相邻的元素表示一个组的首尾索引。

再回到compile方法,下一步调用BnM.optimize(matchRoot).

BnM

继承Node结点

     static class BnM extends Node {}

属性

         int[] buffer; // 表达式数组(里面元素是代码点)
int[] lastOcc; // 坏字符,表达式里的每个字符按顺序(从表达式数组索引0开始)存到lastOcc数组中,存的位置是表达式元素的值对128取模,因为它的长度是128,存的值是patternLength - 移动步长
int[] optoSft; // 好后缀,长度等于表达式数组的长度,里面的元素也表示patternLength - 移动步长

构造方法

         BnM(int[] src, int[] lastOcc, int[] optoSft, Node next) {
this.buffer = src;
this.lastOcc = lastOcc;
this.optoSft = optoSft;
this.next = next;
}

optimize(Node node)

         static Node optimize(Node node) {
if (!(node instanceof Slice)) {
return node;
} int[] src = ((Slice) node).buffer;
int patternLength = src.length;
if (patternLength < 4) {
return node;
}
int i, j, k; // k无用
int[] lastOcc = new int[128];
int[] optoSft = new int[patternLength];
for (i = 0; i < patternLength; i++) { // 构造坏字符数组
lastOcc[src[i] & 0x7F] = i + 1; // 如果不同的字符存在了同一个索引上,则上一个字符沿用后一个字符的【被减步数】,比原来的大了,所以总的步长小了,便不会错过,而坏字符数组的规模则控制在了前128位,拿时间换空间是值得的,毕竟涵盖了整个ASCII字符集
}
NEXT: for (i = patternLength; i > 0; i--) { // 构造好后缀数组
for (j = patternLength - 1; j >= i; j--) { // 从后往前,处理所有子字符串的情况,出现的子字符串同时也在头部出现才算有效
if (src[j] == src[j - i]) {
optoSft[j - 1] = i;
} else {
continue NEXT;
}
}
while (j > 0) { // 填充剩余的槽位
optoSft[--j] = i;
}
}
optoSft[patternLength - 1] = 1;
if (node instanceof SliceS)
return new BnMS(src, lastOcc, optoSft, node.next);
return new BnM(src, lastOcc, optoSft, node.next);
}

预处理,构造出坏字符数组和好后缀数组。

         boolean match(Matcher matcher, int i, CharSequence seq) {
int[] src = buffer;
int patternLength = src.length;
int last = matcher.to - patternLength; NEXT: while (i <= last) {
for (int j = patternLength - 1; j >= 0; j--) { // 从后往前比较字符
int ch = seq.charAt(i + j);
if (ch != src[j]) {
i += Math.max(j + 1 - lastOcc[ch & 0x7F], optoSft[j]); // 每次移动步长,取坏字符和好后缀中较大者
continue NEXT;
}
}
matcher.first = i;
boolean ret = next.match(matcher, i + patternLength, seq);
if (ret) {
matcher.first = i;
matcher.groups[0] = matcher.first; // 默认一组(两个索引确定一个片段,所以只需2个元素)
matcher.groups[1] = matcher.last;
return true;
}
i++;
}
matcher.hitEnd = true;
return false;
}

根据Boyer Moore算法比较子字符串。

study

         boolean study(TreeInfo info) {
info.minLength += buffer.length;
info.maxValid = false;
return next.study(info);
}

Boyer Moore算法

可参考这个

该算法最主要的特征是,从右往左匹配,这样每次可以移动不止一个字符,有两个依据,坏字符和好后缀,取较大值。

坏字符

从表达式最右边的字符开始与文本中同索引字符比较,若相同则继续往左,直至比较结束,即匹配;或遇到不等的字符,即称该不等字符(文本中的字符)为坏字符,根据表达式中是否包含坏字符和坏字符的位置来确定移动步长,公式如下:

后移位数 = 坏字符的位置 - 搜索词中的上一次出现位置

如果"坏字符"不包含在搜索词之中,则上一次出现位置为 -1。

好后缀

从右往左比较过程中,相等的部分字符序列称为好后缀,最长好后缀的子序列也是好后缀,同时在表达式头部出现的好后缀才有效。公式如下:

后移位数 = 好后缀的位置 - 搜索词中的上一次出现位置

"好后缀"的位置以最后一个字符为准。

分析

其实,不管是坏字符还是好后缀,它的目的是移动最大步长,以实现快速匹配字符串的,还得不影响正确性。

坏字符很好理解,如果表达式中不包含坏字符,这个时候移动的步长是表达式的长度,也是能移动的最大长度;假如这种情况下,移动的长度小于表达式的长度,那么上次的坏字符总能再次出现,结果还是不匹配,所以直接移动到坏字符的后面,即表达式长度。

若是表达式中包含坏字符呢,肯定是的表达式中的那个字符和坏字符对齐才行,若是不对齐,与别的字符比较,还是不等,那如果表达式中包含不只一个呢,为了不往回(左)移动,应该使得表达式中靠后的字符与坏字符对齐,这样如果不匹配的话,可以接着右移,避免回溯。

好后缀也好理解,如果头部不包含好后缀,那么完全可以移动表达式的长度,若是包含,只需将好后缀部分对齐即可。

Node链

matches()

matchRoot -> Slice -> LastNode -> Node

Slice和Node结点,前面已经介绍过了。Slice结点,从第一个字符开始比较,如果长度不等,或遇到不等的字符,返回false,否则调用next结点的match方法,这里的next结点是LastNode.

Node结点的match方法总会返回true.

LastNode

     static class LastNode extends Node {
boolean match(Matcher matcher, int i, CharSequence seq) {
if (matcher.acceptMode == Matcher.ENDANCHOR && i != matcher.to) // 当acceptMode是ENDANCHOR时,此时是全匹配,所以需要检查i是否是最后一个字符的下标
return false;
matcher.last = i;
matcher.groups[0] = matcher.first;
matcher.groups[1] = matcher.last;
return true;
}
}

此结点是通用结点,用来最后检测结果的,注意accetMode参数,用以区分是全匹配还是部分匹配。

find()

root -> BnM -> LastNode -> Node

由BnM结点可知,匹配可从任意有效位置开始,其实就是查找子字符串,且acceptMode不是ENDANCHOR,所以在LastNode中,无需检查i是否指向最后一个字符。

以上结点均已在上文中给出。

样例2

     public static void example() {
String regex = "\\d+";
String text = "0123456789";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
matcher.find();
}

这个样例是匹配数字。

跟踪其调用过程,跟样例1差不多,最后是到compile方法里面,调用expr(Node end) 方法。

expr(Node end)

【Java字符序列】Pattern的更多相关文章

  1. Java 之 可变字符序列:字符串缓冲区(StringBuilder 与 StringBuffer)

    一.字符串拼接问题 由于 String 类的对象内容不可改变,所以每当进行字符串拼接时,总是会在内存中创建一个新的对象. Demo: public class StringDemo { public ...

  2. Java实现 蓝桥杯VIP 算法提高 最长字符序列

    算法提高 最长字符序列 时间限制:1.0s 内存限制:256.0MB 最长字符序列 问题描述 设x(i), y(i), z(i)表示单个字符,则X={x(1)x(2)--x(m)},Y={y(1)y( ...

  3. Java 常用类——StringBuffer&StringBuilder【可变字符序列】

    一.字符串拼接问题 由于 String 类的对象内容不可改变,所以每当进行字符串拼接时,总是会在内存中创建一个新的对象. Demo: 1 public class StringDemo { 2 pub ...

  4. JAVA基础 XML生成与解析和String包装类下 .replace方法的使用以及char和字符序列的使用场景

    ptLink0.setText(arbu.getPtLink().replace("&","&")); // 如果像 '&','& ...

  5. java.util.regex.Pattern的应用

    java.util.regex.Pattern 正则表达式的一种已编译的实现. 正则表达式通常以字符串的形式出现,它首先必须被编译为Pattern类的一个实例.结果模型可以用来生成一个Matcher, ...

  6. JAVA正则表达式:Pattern类与Matcher类详解(转)

    java.util.regex是一个用正则表达式所订制的模式来对字符串进行匹配工作的类库包.它包括两个类:Pattern和Matcher Pattern 一个Pattern是一个正则表达式经编译后的表 ...

  7. JAVA正则表达式:Pattern类与Matcher类详解

    java.util.regex是一个用正则表达式所订制的模式来对字符串进行匹配工作的类库包.它包括两个类:Pattern和Matcher Pattern 一个Pattern是一个正则表达式经编译后的表 ...

  8. Java 字符的验证

    package net.hlj.common.util; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @ ...

  9. Java 字符编码归纳总结

    String newStr = new String(oldStr.getBytes(), "UTF-8");       java中的String类是按照unicode进行编码的 ...

随机推荐

  1. 限定pan手势只能在圆内移动view

    限定pan手势只能在圆内移动view 效果: 虽然看起来很简单,但实现原理还是稍微有点复杂-_-!! 核心的地方,就是需要计算pan手势的点与指定点的距离,不能超过这个距离,超过了就让动画还原,很容易 ...

  2. Squid安装配置和使用

    文:铁乐与猫 环境 centos 6.5 x64 安装 最简单的一种就是yum安装. yum install squid 版本 rpm -qa | grep squid squid-3.1.23-16 ...

  3. 分析 org.hibernate.HibernateException: No Session found for current thread

    /**      *      * org.hibernate.HibernateException: No Session found for current thread      * 分析:ge ...

  4. SIM900A模块HTTP相关调试笔记

    SIM900A模块使用笔记 更新2018-12-8 正常工作状态: 接线方法: 首先将 AT 写入字符串输入框,然后点击 发送.因为模块波特率默认是 9600,所以两条指令的显示都是没有问题的:如果将 ...

  5. 快速搭建一个Express工程骨架

    下载express-generator 通过应用生成器,可以帮我们快速搭建项目需要的骨架.这就需要npm在全局下载express-generator(-g就是在全局安装) npm install ex ...

  6. laravel 资料

    1.http://maxoffsky.com/maxoffsky-blog/building-a-shop-with-laravel-tutorial-series-announcement/  一篇 ...

  7. BZOJ 2763 飞行路线 BFS分层

    题目链接: https://www.lydsy.com/JudgeOnline/problem.php?id=2763 题目大意: Alice和Bob现在要乘飞机旅行,他们选择了一家相对便宜的航空公司 ...

  8. Rabbitmq.md

    RabbitMQ介绍 什么是RabbitMQ RabbitMQ是实现AMQP(高级消息队列协议)的消息中间件的一种,最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性.扩展性.高可用性等方面 ...

  9. swift直接赋值与引用赋值都会触发willSet

    class baseGoo{ var isScannerRunning = false { willSet{ print(newValue) } } var desp:String = "& ...

  10. 【node.js】Stream(流)

    Stream 有四种流类型: Readable - 可读操作. Writable - 可写操作. Duplex - 可读可写操作. Transform - 操作被写入数据,然后读出结果. 所有的 St ...