【Java字符序列】Pattern
简介
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;
}
- 首先标准化表达式
- 将字符代码点暂存int数组中,所谓代码点指的是字符集里每个字符的编号,从0开始,常见的字符集ASCII和Unicode
- 返回相应类型的结点
- 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的更多相关文章
- Java 之 可变字符序列:字符串缓冲区(StringBuilder 与 StringBuffer)
一.字符串拼接问题 由于 String 类的对象内容不可改变,所以每当进行字符串拼接时,总是会在内存中创建一个新的对象. Demo: public class StringDemo { public ...
- Java实现 蓝桥杯VIP 算法提高 最长字符序列
算法提高 最长字符序列 时间限制:1.0s 内存限制:256.0MB 最长字符序列 问题描述 设x(i), y(i), z(i)表示单个字符,则X={x(1)x(2)--x(m)},Y={y(1)y( ...
- Java 常用类——StringBuffer&StringBuilder【可变字符序列】
一.字符串拼接问题 由于 String 类的对象内容不可改变,所以每当进行字符串拼接时,总是会在内存中创建一个新的对象. Demo: 1 public class StringDemo { 2 pub ...
- JAVA基础 XML生成与解析和String包装类下 .replace方法的使用以及char和字符序列的使用场景
ptLink0.setText(arbu.getPtLink().replace("&","&")); // 如果像 '&','& ...
- java.util.regex.Pattern的应用
java.util.regex.Pattern 正则表达式的一种已编译的实现. 正则表达式通常以字符串的形式出现,它首先必须被编译为Pattern类的一个实例.结果模型可以用来生成一个Matcher, ...
- JAVA正则表达式:Pattern类与Matcher类详解(转)
java.util.regex是一个用正则表达式所订制的模式来对字符串进行匹配工作的类库包.它包括两个类:Pattern和Matcher Pattern 一个Pattern是一个正则表达式经编译后的表 ...
- JAVA正则表达式:Pattern类与Matcher类详解
java.util.regex是一个用正则表达式所订制的模式来对字符串进行匹配工作的类库包.它包括两个类:Pattern和Matcher Pattern 一个Pattern是一个正则表达式经编译后的表 ...
- Java 字符的验证
package net.hlj.common.util; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @ ...
- Java 字符编码归纳总结
String newStr = new String(oldStr.getBytes(), "UTF-8"); java中的String类是按照unicode进行编码的 ...
随机推荐
- matlab 函数句柄@的介绍_什么是函数句柄(转)
http://blog.csdn.net/kevinhg/article/details/8861774 http://www.ilovematlab.cn/thread-30375-1-1.html ...
- 反射工具类.提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class,被AOP过的真实类等工具函数.java
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.ap ...
- Stacks And Queues
栈和队列 大型填坑现场,第一部分的还没写,以上. 栈和队列是很基础的数据结构,前者后进先出,后者先进先出,如下图: 下面开始将客户端和具体实现分开,这样有两个好处:一是客户端不知道实现的细节,但同时也 ...
- 基于easyui开发Web版Activiti流程定制器详解(六)——Draw2d详解(二)
回顾: 上一篇我们介绍了Draw2d整体结构,展示了组件类关系图,其中比较重要的类有Node.Canvas.Command.Port.Connection等,这篇将进一步介绍Draw2d如何使用以及如 ...
- [SDOI2010]Hide and Seek
题目 非常显然就是求一下距离每一个点曼哈顿距离最近的点和最远的点就好了 最远点非常好算,我们建完\(kd-tree\)之后直接暴力就好了 找最近点的时候会有这样一个问题,就是自己找到了自己 所以我们需 ...
- 私有仓库harbor安装包括https
1. 下载离线的 harbor gz包 wget https://github.com/vmware/harbor/releases/download/v1.2.0/harbor-offline-in ...
- Js 运行机制 event loop
Js - 运行机制 (Even Loop) Javascript 的单线程 - 引用思否的说法: JavaScript的一个语言特性(也是这门语言的核心)就是单线程.什么是单线程呢?简单地说就是同一时 ...
- 关于onscroll函数兼容各浏览器的方法分析
关于window.onscroll函数兼容各浏览器的方法分析 1.当前文档的渲染模式是决定onscroll函数兼容性根本原因 目前浏览器的排版引擎有三种模式:怪异模式(Quirks mode).接近标 ...
- active developer path ("/Applications/Xcode.app/Contents/Developer")
-> git xcrun: error: active developer path ("/Applications/Xcode.app/Contents/Developer" ...
- NRF52832 能烧写代码 但是不运行 ,是因为没有烧写协议栈
仿真进入的状态 , MOVES R0.R0 请烧写 协议栈 ,自行百度!