• 沒有找到結果。

Java 正则表达式 API

在文檔中 Ron Hitchens 著 裴小星 译 (頁 157-0)

第五章 正则表达式

5.2 Java 正则表达式 API

java.util.regex 程序包只包含用于实现 Java 正则表达式处理技术的两个类,分别名 为 Pattern 和 Matcher 。 自 然 而 然 你 会 想 到 正 则 表 达 式 由 模 式 匹 配 ( pattern matching)而成.java.lang 还定义了一个新接口,它支持这些新的类。在研究 Patternt 和 Matcher 之前,我们先快速浏览一下 CharSequence 这一新概念。

另外,为方便起见 String 类为运行正则表达式匹配提供了一些新程序作为捷径。这些将 5.3 节中讨论。

5.2.1 CharSequence接口 The CharSequence Interface

正则表达式是根据字符序列进行模式匹配的。虽然 String 对象封装了字符序列,但是它们 并不能够这样做的唯一对象。

JDK 1.4 定义了一个名为 CharSequence 的新接口,可描述特定不变的字符序列。该新接口 Larry Wall 在 1987 年发布了首版 perl。Perl 十分有用,因为它将许多功能强大的部件 集成在一个脚本语言中,而不仅仅是将正则表达式处理技术编入语言句法结构中。首版 Perl 采用的正则表达式代码取自 James Gosling 版的 emacs(editor macros,编辑器宏指 令)。Perl 现在已经发展到了第 5 版,普遍使用已达八年。它已经成了衡量大部分正则表 达式处理器的基准。

正则表达式匹配引擎(regular expression-matching engine)可分成两类:确定性有限自 动机(Deterministic Finite Automaton, DFA)和非确定性有限自动机(Nondeterministic Finite Automaton, NFA)。二者的差异与如何编译表达式及如何匹配目标有关。一般情况下 DFA 速度较快,因为它会建立匹配树(matching tree),根据匹配进程“修剪”无法达到 的“分枝”,预先作更多的工作。NFA 可能较慢,因为它运行的是更详尽的检索,常常需 要回溯(backtrack)。DFA 速度较快,但 NFA 功能更全。例如 DFA 处理器无法捕捉子表 达式(subexpression)而 NFA 可以。java.util.regex 引擎是 NFA 并且它的句法与 Perl 5 相 似。

虽 然 正 则 表 达 式 成 了 带 JDK 1.4 的 Java 平台 的正 式组成部 分,但是 这无 碍于 java.util.regex 是第一个适用于 Java 的正则表达式包这一事实。Apache 组织有两个开放源的

(open source)正则表达式包:Jakarta-ORO 和 Jakarta-Regexp,距离它们发布已经有些时 日了。继承自 OROMatcher 的 Jakarta-ORO 发展了 Apache 并且自身也得到了进一步的增 强 。 它 有 许 多功 能 部 件且 可 高 度 自 定义 ( highly customizable)。Jakarta-Regexp 同样 Apache 有所助益但是范围较小。GNU 的“民间人士”提供了一个名为 gnu.regexp 的正则 表达式包,它有一些像 Perl 的功能。并且 IBM 有个名为 com.ibm.regex 的商业软件包。它 也有许多类似 Perl 功能,可很好地支持 Unicode。

正则表达式历史、正则处理器类型、可用的实现方式及所有你希望知道的有关正则表 达 式 句 法 和 使 用 的 所 有 细 节 , 请 查 阅 Jeffrey E.F. Friedl 的 著 书 : Mastering Regular Expressions (O’Reilly) 。

154 是一个抽象(abstraction),它把字符序列从包含这些字符的具体实现(specific implementation)中 分离出来。JDK 1.4 对“年高德勋”的 String 和 StringBuffer 类进行了改进,用于实现 CharSequence 接口。新的 CharBuffer 类(在第二章中介绍过)也实现了 CharSequence。

CharSequence 接口也在字符集映射中投入了使用(参见第六章)。

CharSequence 定义的 API 十分简单。毕竟它没有花太多“笔墨”描述字符序列。

package java.lang;

public interface CharSequence {

int length( );

char charAt (int index);

public String toString( );

CharSequence subSequence (int start, int end);

}

CharSequence 描述的每个字符序列通过 length( )方法会返回某个长度值。通过调用 charAt( )可以得到序列的各个字符,其中索引是期望的字符位置(desired character position)。

字符位置从零到字符序列的长度之间,与我们熟悉的 String.charAt( )基本一样。

toString( )方法返回的 String 对象包括所描述的字符序列。这可能很有用,如打印字符序 列。正如之前提过的,String 现在实现了 CharSequence。String 和 CharSequence 同为不变的,

因此如果 CharSequence 描述一个完整的 String,那么 CharSequence 的 toString( )方法返回的是 潜在的 String 对象而不是副本。如果备份对象是 StringBuffer 或 CharBuffer,系统将创建一个 新的 String 保存字符序列的副本。

最 后 通 过 调 用 subSequence( ) 方 法 会 创 建 一 个 新 的 CharSequence 描 述 子 范 围

(subrange)。start 和 end 的指定方式与 String.substring( )的方式相同:start 必须是序列的有效 索引(valid index);end 必须比 start 大,标志的是最末字符的索引加一。换句话说,start 是 起始索引(计算在内),end 是结束索引(不计算在内)。

CharSequence 接口因为没有赋值方法(mutator method)看上去似乎是不变的,但是基本 的实现对象可能不是不变的。CharSequence 方法反映了基本对象的现状。如果状态改变,

CharSequence 方法返回的信息同样会发生变化(见例 5-1)。如何你依赖 CharSequence 保持 稳定且不确认基础的实现,你可以调用 toString( )方法,对字符序列拍个真实不变的快照。

例 5-1 CharSequence 接口实例

package com.ronsoft.books.nio.regex;

import java.nio.CharBuffer;

/**

* Demonstrate behavior of java.lang.CharSequence as implemented * by String, StringBuffer and CharBuffer.

*

155 * @author Ron Hitchens ([email protected])

*/

public class CharSeq {

public static void main (String [] argv) {

StringBuffer stringBuffer = new StringBuffer ("Hello World");

CharBuffer charBuffer = CharBuffer.allocate (20);

CharSequence charSequence = "Hello World";

//直接来源于String

printCharSequence (charSequence);

//来源于StringBuffer

charSequence = stringBuffer;

printCharSequence (charSequence);

//更改StringBuffer

stringBuffer.setLength (0);

stringBuffer.append ("Goodbye cruel world");

//相同、“不变的”CharSequence产生了不同的结果 printCharSequence (charSequence);

//从CharBuffer中导出CharSequence charSequence = charBuffer;

charBuffer.put ("xxxxxxxxxxxxxxxxxxxx");

charBuffer.clear( );

charBuffer.put ("Hello World");

charBuffer.flip( );

printCharSequence (charSequence);

charBuffer.mark( );

charBuffer.put ("Seeya");

charBuffer.reset( );

printCharSequence (charSequence);

charBuffer.clear( );

printCharSequence (charSequence);

//更改基础CharBuffer会反映在只读的CharSequence接口上 }

private static void printCharSequence (CharSequence cs) {

System.out.println ("length=" + cs.length( ) + ", content='" + cs.toString( ) + "'");

} }

以下是执行 CharSequence 的结果:

length=11, content='Hello World' length=11, content='Hello World'

length=19, content='Goodbye cruel world' length=11, content='Hello World'

length=11, content='Seeya World'

length=20, content='Seeya Worldxxxxxxxxx'

5.2.2 Pattern类 The Pattern Class

Pattern 类封装了正则表达式,它是你希望在目标字符序列中检索的模式。匹配正则表 达式的代价可能非常高昂,因为可能排列数量巨大,尤其是模式反复应用的情况。大部分正则

156 表达式处理器(包括 Perl 在内,在封装中)首先会编译表达式,然后利用编译好的表达式在 输入中进行模式检测。

在这一点上 Java 正则表达式程序包别无两样。Pattern 类的实例是将一个编译好的正 则表达式封装起来。让我们看看完整的 Pattern API,看看它是如何使用的。记住,这并不 是一个句法完整的类文件,它只中去掉了类主体的方法签名。

package java.util.regex;

public final class Pattern implements java.io.Serializable {

public static final int UNIX_LINES public static final int CASE_INSENSITIVE public static final int COMMENTS

public static final int MULTILINE public static final int DOTALL public static final int UNICODE_CASE public static final int CANON_EQ

public static boolean matches (String regex, CharSequence input) public static Pattern compile (String regex)

public static Pattern compile (String regex, int flags) public String pattern( )

public int flags( )

public String[] split (CharSequence input, int limit) public String[] split (CharSequence input)

public Matcher matcher (CharSequence input) }

上面所列的第一个方法 matches( )是个公用程序。它可以进行完整的匹配操作,并根据正 则表达式是否匹配整个的(entire)输入序列返回一个布尔值。这种方法很容易上手,因为 你无须追踪任何对象;你要做的仅是调用一个简单的静态方法并测试结果。

public boolean goodAnswer (String answer) {

return (Pattern.matches ("[Yy]es|[Yy]|[Tt]rue", answer));

}

这种方法适用于默认设置尚可接受并且只需进行一次测试的情况。假如你要重复检查同一 模式,假如你要找的模式是输入的子序列,又假如你要设置非默认选项,那么你应当创建一个 新的 Pattern 对象并使用新对象的 API 方法。

需要注意的是 Pattern 类并没有公用建构函数。只有通过调用静态工厂方法才可以创建 新的实例。compile( )的两个形式采用的都是正则表达式的 String 参数。返回的 Pattern 对 象包含被转换成已编译内部形式的正则表达式。如果你提供的正则表达式形态异常,那么 compile( )工厂方法会抛出 java.util.regex.PatternSyntaxException(模式句法异 常)。这是未经检查的异常,因此如果你对自己使用的正则表达式是否可行存在疑虑(例如它 传递给你是一个变量),那么你可以把对 compile( )的调用放到 try/catch 块中进行检测。

157 compile( )的第二种形式接受标志有一个位掩码,这影响了正则表达式的默认编译。这些标 志启用了可选的编译模式行为,例如如何处理边界或不区分大小写等。(除 CANOB_EQ 外)

这些标志(flag)同样可由嵌入表达式内的子表达式启用。标志可以与布尔或(OR)表达式 结合使用,如下所示:

Pattern pattern = Pattern.compile ("[A-Z][a-zA-Z]*", Pattern.CASE_INSENSITIVE | Pattern.UNIX_LINES);

所有标志默认为关闭状态。表 5-1 总结了各个编译时间选项的意义。

表5-1. 影响正则表达式编译的标志值 标志名 嵌入的表达式 描述

UNIX 命令行 UNIX lines

(?d) 启用 Unix 命令行模式。 CASE INSENSITIVE

(?i) 启用不区分大小写的模式匹配,这可能会使性能有点打折。

使用这个标志的前提是设置匹配的字符只来自 US-ASCII 字符 集。如果你正在处理的是其它语言的字符集,你同样可以规定 UNICODE_CASE 标志,从而启用 Unicode 识别大小写折叠

(Unicode-aware case folding)。

UNICODE 大小写 UNICODE CASE

(?iu) Unicode 识别、大小写折叠模式

158 选项相当于 Perl 的单行模式,因而有(?s)这一嵌入式标志 名。

规范等式 CANON EQ

无 启用规范等价模式。

当规定了这一标志,当且仅当字符的标准分解匹配时认为它们 匹配。

例如,当该标志启用时,双字符序列 a\u030A(Unicode 符号

“拉丁文小写字母 a”后面接一个“组合上圆圈 ̊”,即 a˚)与 单字符\u00E5(拉丁文小写字母 a“头上”带圆,即 å)是匹配 的。

默认状态下,并不考虑规范等价性。关于规范等价性,详情请 见 http://www.unicode.org/中字符映射的定义。该标志可能会导 致性能显著下降。没有嵌入式标志表达式可以启用规范等式模 式。

Pattern 类的实例是不变的,各个实例与对应的正则表达式绑定,无法修改。Pattern 对象也 是线程安全的,可被多个线程同时使用。

因此,一旦你拿到一个 Pattern,你能对它做什么呢?

package java.util.regex;

public final class Pattern implements java.io.Serializable {

// 这是部分的API列表 public String pattern( ) public int flags( )

public String[] split (CharSequence input, int limit) public String[] split (CharSequence input) public Matcher matcher (CharSequence input) }

下面是两个方法,作用是返回 Pattern 类 API 关于封装表达式的信息。pattern( )返回的 String 被用于初建 Pattern 实例(建立对象时字符串被传递给 compile( ))。另一个是 flags( ),

返回的是在编译模式时提供的标志位掩码。如果 Pattern 对象由无参数的 compile( )创建,则 flags( )返回的值是 0。返回的值反映的只是提供给 compile( )的明确标志值;它不包括由正则表 达式模式中的嵌入表达式设置的任何等效标志,等效标志如表 5-1 第二列所示。

实例方法 split( )是公用程序,它使用模式作为定界符(delimiter)来标记字符序列。这会 令人联想到 StringTokenizer,但是它的功能更强,因为定界符可以是匹配正则表达式的多字符 序列。另外,split( )方法是不监控状态的,它返回的是字符串标志的阵列而不是要求多个调用 程序并循环执行它们:

159 Pattern spacePat = Pattern.compile ("\\s+");

String [] tokens = spacePat.split (input);

调用仅有一个参数的 split( )与调用两个参数但第二参数为零的该方法是等效的。split( )第 二个参数指示了输入序列被正则表达式拆分的限制次数。限制参数的意义在于防止超负荷。非

调用仅有一个参数的 split( )与调用两个参数但第二参数为零的该方法是等效的。split( )第 二个参数指示了输入序列被正则表达式拆分的限制次数。限制参数的意义在于防止超负荷。非

在文檔中 Ron Hitchens 著 裴小星 译 (頁 157-0)

相關文件