字符串
字符串的不可变
String 对象是不可变的。查看 JDK 文档就会发现,String 类中每一个看起来会修改 String 值的方法,实际上都是创建了一个全新的 String 对象,该对象包含了修改的内容。而原始的 String 则保持不变。
public class Immutable {
public static String upcase(String s) {
return s.toUpperCase();
}
public static void main(String[] args) {
String q = "howdy";
System.out.println(q);// howdy
String qq = upcase(q);
System.out.println(qq);// HOWDY
System.out.println(q);// howdy
}
}当 q 被传递给upcase()时,实际上传递的是 q 对象引用的一个副本。此引用所指向的对象只存在于单一的物理位置中。在传递时被复制的只是引用。
在upcase()中,参数 s 只存活于这个方法的方法体里,当upcase()运行完成后,局部引用 s 就会消失。upcase()返回执行的结果:一个指向新字符串的引用。
对于代码的读者来说,参数一般是给方法提供消息的,而不是要被修改的。这种不变性是一个重要的保证,因为它使代码更容易编写和理解。
+ 的重载与 StringBuilder
String 对象是不可变的,因此可以根据需要给一个 String 对象添加任意多的别名。因为 String 是只读的,指向它的任何引用都不可能修改它的值,所以引用之间不会相互影响。
不可变性会带来一定的效率问题。为 String 对象重载的 + 操作符就是一个例子。重载的意思是,一个操作符在用于特定的类时,被赋予了特殊的意义(用于 String 的 + 与 += 是 Java 中仅有的两个重载过的操作符,Java 不允许程序员重载任何其他的操作符)。
+操作符可以用来拼接字符串:
可以想象一下这里的工作原理。字符串"abc"可以有一个方法append(),它创建了一个新的 String 对象,其中包含"abc"和 mango 拼接后的内容,随后新的 String 对象添加了"def"后,会创建另一个新的 String,以此类推。
这样可以行得通,但是当创建许多 String 对象来组合新的 String 时,这时就有一堆 String 类型的中间对象需要被垃圾收集。这种实现在性能上十分糟糕。
可以使用 JDK 自带的 javap 工具反编译上述代码来查看其是如何实现的:
-c表示生成 JVM 字节码。根据反编译的内容,编译器引入了java.lang.StringBuilder类,虽然代码中没有使用 StringBuilder,但是编译器决定使用它,因为它效率更高。
在这里,编译器创建了一个 StringBuilder 对象,用于构建最终的 String,并对每个字符串调用了一次 append() 方法,共计 4 次。最后调用toString()生成结果,并存为 s。
下面以两种不同方式生成 String 对象:直接使用String,以及手动使用 StringBuilder 编码。
运行javap -c WhitherStringBuilder:
第一段字节码是一个循环,它遍历了一个数组(存储在局部变量3中),对数组中的每个元素执行字符串拼接操作,然后将最终的字符串结果存储在局部变量2中,并在循环结束后返回这个字符串。(这里的输出实际使用了 invokedynamic 指令,该指令在运行时决定如何执行字符串拼接,通常会使用 StringConcatFactory 类来进行优化。)
第二段字节码创建了一个StringBuilder对象,然后遍历一个数组(可能包含字符串),将数组中的字符串逐个追加到StringBuilder中,最后返回StringBuilder对象调用 toString() 后的字符串结果。
直接在循环中进行字符串拼接会导致每次迭代都会创建一个新的StringBuilder对象,这会涉及不断地分配内存空间和复制字符串内容,导致性能开销较大。而StringBuilder通过内部的可变字符序列来构建字符串,避免了不断创建新的String对象,因此在大量字符串拼接的场景下性能更好。(该结论只适用于Java 8 及之前,在 Java 9 及之后:编译器和 JVM 对 + 拼接进行了优化,能够在很多情况下生成与 StringBuilder 等效甚至更优的代码。)
因此,当创建 toString()方法时,如果操作很简单,通常可以依赖编译器,让他以合理的方式自行构建结果。但是如果涉及循环,并且对性能也有一定要求,那就需要在toString()中显式使用 StringBuilder 了。
StringBuilder 提供了全面丰富的方法,包括insert(),replace(),substring()甚至reverse()。通常使用的只有append()和toString()。
string2()使用了Stream,生成的代码更简洁易懂。且Collectors.joining()内部使用StringBuilder实现,因此不会损失性能。
StringBuilder是 Java 5 引入的,在这之前用的是 StringBuffer。后者是线程安全的,因此开销也会大些。使用 StringBuilder 进行字符串操作更快一点。
无意识递归
和其他类一样,Java的标准集合也是从Object继承而来的,所以它们也包含toString()方法。该方法在集合中被重写,这样生成的结果字符串就能表示容器自身,以及该容器持有的所有对象。例如 ArrayList.toString(),它会遍历 ArrayList 的元素,并为每个元素调用 toString() 方法。
如果希望 toString()打印对象的内存地址,使用this会得到很长的异常栈,这是因为字符串的自动类型转换。
由于编译器看到String后面跟着一个+,而不是String的东西,就会试图将这个this转换为一个String。这个转换是通过调用toString()来完成的,而这样就产生一个递归调用。
如果真想打印对象的地址,不应该使用this,而应该使用super.toString()。
字符串操作
构造器
重载版本包括:默认构造器;参数分别为String、StringBuilder、StringBuffer、char数组、byte数组的构造器
创建String对象
length()
——
String中的Unicode代码单元(code units)个数
charAt()
int索引
String中某个位置的char
getChars()、getBytes()
要复制的开始和结束索引,要复制到的目标数组,以及目标数组的起始索引(共四个参数)
将char或byte复制到外部数组中
toCharArray()
——
生成一个char[],包含了String中的所有字符
equals(),equalsIgnoreCase()
要与之比较的String
对两个String的内容进行相等性检查,若内容相等则返回True
compareTo()、compareToIgnoreCase()
要与之比较的String
按字典顺序比较String内的内容,结果可能为负数、零或正数(若调用方法的字符串在字典顺序上在参数字符串之前,则返回负数,若之后,返回正数,相等返回0)。注意大写和小写不相等
contains()
要查找的CharSequence
如果参数包含在String中,则结果为true
contentEquals()
用来比较的CharSequence或StringBuffer
如果该String与参数的内容完全匹配,则结果为true
isEmpty()
——
返回一个boolean值,表明该String的长度是否为0
regionMatches()
该String的索引偏移量,参数String和它的索引偏移量,以及要比较的长度。重载方法添加了"忽略大小写"功能
比较特定区域的子串是否相等,返回boolean
startsWith()
该字符串可能的前缀String,重载方法在参数列表中增加了偏移量
返回一个boolean值,表明该String是否是以该参数字符串开头
endsWith()
该字符串可能的后缀String
返回一个boolean值,表明参数字符串是否为后缀
indexOf()、lastIndexOf()
重载版本包括:char、char和起始索引;String、String和起始索引
如果在此String中找不到该参数,则返回-1;否则返回参数开始的索引,lastIndexOf()则从后向前搜索。
matches()
一个正则表达式
返回一个boolean值,表明此String是否与给定的正则表达式匹配
split()
一个正则表达式,可选的第二个参数是要进行的最大分割数
根据正则表达式拆分String,返回结果数组
join()
分隔符以及要合并的元素。通过将元素与分隔符连接在一起,生成新的String
将片段合并成一个由分隔符分隔的新String
substring()(和subSequence())
重载版本包括:起始索引;起始索引 + 结束索引
返回一个String对象,包含了指定的字符集合
从这个表可以看出,当需要改变字符串的内容时,String 类的方法都会返回一个新的 String 对象。同时,如果内容不改变,String 方法只是返回原始对象的一个引用。这可以节约存储空间以及避免额外的开销。
格式化输出
printf()
C 语言的 printf() 并不像 Java 那样连接字符串,它使用一个简单的格式化字符串,加上要插入其中的值,然后将其格式化输出。 printf() 并不使用重载的 + 操作符(C语言没有重载)来连接引号内的字符串或字符串变量,而是使用特殊的占位符来表示数据将来的位置,要插入到格式化字符串的参数用逗号分隔排列。
System.out.format()
Java 5 引入的format()方法可用于 printStream 或 printWriter 对象,其中也包括 System.out 对象,format() 方法模仿了 C 语言的 printf()。
format() 和 printf() 是等价的。它们都只需要一个格式化字符串,后面跟着参数,其中每个参数都对应一个格式说明符。
String 类也有一个静态的 format() 方法,它会产生一个格式化字符串。
Formatter类
在 Java 中,所有的格式化功能都是由 java.util.Formatter 类处理的。可以将Formatter视为一个转换器,将格式化字符串和数据转换为想要的结果。当创建一个Formatter对象时,可以将信息传递给构造器,来表明希望将结果输出哪里。
所有的 tommy 都将输出到 System.out,而所有的 terry 则都输出到 System.out 的一个别名中。Formatter 的重载构造器支持输出到多个路径,不过最常用的还是 PrintStream()(如上例)、OutputStream 和 File。
在插入数据时控制间距和对齐方式,就需要更详细的格式说明符。通用语法如下:
控制一个字段的最小长度可以通过指定 width 来实现。Formatter 会确保这个字段至少达到一定数量的字符宽度,必要时会使用空格来填充。默认情况下,数据是右对齐的,但这可以通过使用一个-标记来改变。
与 width 相对的是 precision,用于指定最大长度。width 适用于所有进行转换的数据类型,并且对每种类型来说其行为方式都一样,而 precision 对不同的类型有不同的含义。对字符串来说,precision 指定了字符串的最大输出字符数。对浮点数而言,precision 指定了要显示的小数位数(默认为6位),小数位数如果太多就舍入,太少就末尾补零。precision 不能应用于整数,因为其没有小数部分。
下面的示例中使用格式说明符打印购物收据,它使用了生成器模式,你可以创建一个起始对象,然后向其中添加内容,最后使用build()方法生成最终结果。
将StringBuilder传递给Formatter构造器后,它就有了一个构造String的地方。还可以使用构造器参数将其发送到标准输出甚至文件中。
Formatter转换
一些常见的转换字符:
d
整数类型(十进制)
c
Unicode字符
b
Boolean值
s
字符串
f
浮点数(十进制)
e
浮点数(科学计数法)
x
整数类型(十六进制)
h
哈希码(十六进制)
下面的示例显示了这些转换的效果:
注意:转换字符 b 适用于上述所有变量。对于 boolean 基本变量或 Boolean 对象,相应的结果就是 true 或 false。但是对于任何其他参数,只要参数类型不是 null,结果总是 true。即使数值 0,其转换结果也是 true。
Formatter类还有许多转换类型和格式说明符选项,参考 JDK 文档。
String.format()
Java 5借鉴了 C 语言用来创建字符串的 sprintf(),提供了 String.format()方法。该方法是一个静态方法,参数与 Formatter 类的 format() 方法完全相同,但返回一个 String。当只调用一次 format() 时,这个方法就很方便:
String.format()内部所做的就是实例化一个 Formatter,并将传入的参数直接传递给它。和手动做这些相比,使用这个方法更方便,代码也更清晰。
十六进制转储工具
第二个示例是将二进制文件中的字节格式化为十六进制。如下通过使用String.format(),将一个二进制字节数组按可读的十六进制格式打印出来:
为了打开和读取二进制文件,使用了上一章介绍的Files.readAllBytes()方法,它以 byte 数组的形式返回整个文件。
新特性:文本块
JDK 15 添加了从 Python 语言借鉴来的 文本块 特性,使用三引号来表示包括换行符的文本块,可以更轻松地创建多行文本。
旧方式处理多行字符串需要很多换行符 \n 和符号 +。而新方式消除了这些符号,提供了更好的语法。
注意:若想要产生缩进,需要移动最后的"""来产生所需的缩进。
为了支持文本块,String类里添加了一个新的 formatted()方法:
formatted() 是一个成员方法,而不是像String.format()那样的单独的静态函数,所以除了文本块之外,也可以把它用于普通字符串,它用起来更清晰,因为可以直接将它加到字符串的后面。
文本块的结果是一个常规的字符串,所以其他字符串能做的,它也可以做。
正则表达式
基础
正则表达式用通用术语描述字符串,因此可以说:“如果字符串中包含这些内容,那么它就符合我的搜索条件”。例如,要表示一个数前面可能有或没有减号,可以在减号后面加一个问号,即 -?。
要描述一个整数,你可以说它是一个或多个数字。在正则表达式中,数字用\d来描述。在其它语言中,\\表示“我想要在正则表达式中插入一个普通的(字面上的)反斜线,请不要给它任何特殊的意义”。而在Java中,\\表示“我要插入一个正则表达式的反斜线,所以其后的字符具有特殊意义”。所以要表示一位数字,正则表达式应该是\\d,要插入一个普通的反斜线,应该是\\\。但是,换行符和制表符之类的符号只使用一个反斜杠:\n。(很令人费解的设定)
要表示“一个或多个之前的表达式”,要使用+。所以表示“可能有一个负号,后面跟着一位或多位数字”,是:
为了显示普通字符串反斜杠和正则表达式反斜杠之间的区别,这里使用简单的String.matches()函数。matches()的参数是一个正则表达式,它会调用matches()的字符串。下面可以看到在一个普通的字符串中需要使用两个反斜杠来生成一个反斜杠:
而在正则表达式中,需要使用四个反斜杠才能与单个反斜杠匹配。
使用正则表达式最简单的途径,就是直接使用 String 类内置的功能。例如,可以查看一个String是否与上述的正则表达式匹配:
由于 + 字符在正则表达式中有特殊含义,因此需要用 \\ 转义为普通字符,竖线 | 表示“或”操作。
String 类中有一个很有用的正则表达式工具 split(),它可以围绕给定正则表达式的匹配项来拆分字符串。
也可以使用普通字符作为正则表达式,如第一个只使用空格进行了拆分。
后面两次调用的split()使用了\\W,表示非单词字符(小写的\\w表示单词字符)。第三次的正则表达式表示“字母n跟一个或多个非单词字符”。用来拆分的模式,即字符串中与正则表达式匹配的部分,不会出现在结果中。
还可以使用正则表达式进行替换,可以只替换第一个匹配的项,也可以全部替换:
第一个表达式匹配的是字母f后面跟着一个或多个字母,且它只替换找到的第一个匹配项。第二个表达式要匹配的是三个单词中的任意一个,因为它们以竖线分割表示“或”,并且替换所有匹配的部分。
非String的正则表达式还有更强大的替换工具,例如,可以通过方法调用执行替换。
创建正则表达式
首先从正则表达式可能存在的构造集中选取一个很有用的子集,以此开始学习正则表达式。正则表达式的完整构造子列表,请参考JDK文档 java.util.regex 包中的 Pattern 类。
以下是一些定义某种模式字符的典型方式,以及部分预先定义的字符模式:
.
任何字符
[abc]
a,b,c中的任何一个字符(与a|b|c相同)
[^abc]
a,b,c之外的任何字符(否定)
[a-zA-Z]
a-z或A-Z的任何字符(范围)
[abc[hij]]
a,b,c,h,i,j中任何一个字符(求并集)
[a-z&&[hij]]
h,i,j中的任何一个字符(求交集)
\s
一个空白字符(空格、制表符、换行符、换页、回车)
\S
非空白字符([^\s])
\d
数字([0-9])
\D
非数字([^0-9])
\w
一个单词字符([a-zA-Z0-9])
\W
非单词字符([^\w])
下表列出了一些逻辑操作符:
XY
X后紧跟着Y
X|Y
X或Y
(X)
一个捕获组(capturing group),可以在后面的表达式中用\i来引用第i个捕获组
下表是不同的边界匹配器:
^
行首
$
行尾
\b
单词的边界
\B
非单词的边界
\G
前一个匹配的结束
例如,下面的例子中每一个都可以匹配成功:
我们的目的并不是编写最难理解的正则表达式,而是尽量编写能够完成任务的、最简单以及最必要的正则表达式。
量词
量词(quantifier)描述了一个正则表达式模式处理输入文本的方式:
贪婪型:量词默认是贪婪的,除非另有设置。贪婪型表达式会为模式找到尽可能多的匹配项,即找到最大的匹配字符串。
勉强型:用问号指定,这个量词会匹配满足模式所需的最少字符数。(惰性匹配/最小匹配)
占有型:目前这种类型仅适用于 Java,是一个更高级的功能。当正则表达式应用于字符串时,它会生成许多状态,以便在匹配失败时可以回溯。而占有型量词不会保留这些中间状态,因此可以防止回溯。还可以防止正则表达式运行时失控,并使其执行更有效。
X?
X??
X?+
X.一个或一个也没有
X*
X*?
X*+
X.零个或多个
X+
X+?
X++
X.一个或多个
X{n}
X{n}?
X{n}+
X.正好n个
X{n,}
X{n,}?
X{n,}+
X.至少n个
X{n,m}
X{n,m}?
X{n,m}+
X.至少n个但不超过m个
注意:表达式X经常需要用括号括起来,才能按照预期工作。例如:abc+这个表达式,它看起来会匹配一个或多个abc序列,但是它实际上表示“ab后跟一个或多个c”。要匹配一个或多个完整字符串,必须使用(abc)+表示。
CharSequence 接口对 CharBuffer, String, StringBuffer, StringBuilder等类进行了抽象,给出了一个字符序列的通用定义:
上述类实现了这个接口。许多正则表达式操作使用了 CharSequence 参数。
Pattern和Matcher
通常,比起功能相当有限的String,我们更愿意编译正则表达式对象。为此,只需要导入java.util.regex包,然后使用静态方法Pattern.compile()编译正则表达式即可。它会根据传入的字符串参数生成一个Pattern对象,可以通过调用matcher()方法来使用这个Pattern,对传递的String进行搜索。matcher()方法会产生一个Matcher对象,它有一组可供选择的操作(参考java.util.regex.Matcher的JDK文档)。例如,replaceAll()会用其参数替换所有的匹配项。
下面第一个示例,可以针对输入字符串来测试正则表达式。第一个命令行参数是要匹配的输入字符串,然后是一个或多个应用于输入字符串的正则表达式。
Pattern对象表示正则表达式的编译版本,如上示例,可以使用编译后的 Pattern 对象的 matcher() 方法,加上输入的字符串作为参数,来生成一个 Matcher 对象。Pattern 还提供了一个静态方法:
该方法会检查正则表达式 regex 与整个 CharSequence 类型的输入参数 input 是否匹配。编译后的 Pattern 对象还提供了 split() 方法,它从匹配了 regex 的地方分割输入字符串,返回分割后的子字符串数组。
通过调用 Pattern.matcher() 方法,并传入一个字符串参数,来生成一个 Matcher 对象。然后就可以使用 Matcher 对象提供的方法来评估不同类型的匹配,并获得 boolean 类型的匹配结果:
其中的 matches() 方法用来判断整个输入字符串是否匹配正则表达式模式,而lookingAt()则用来判断该字符串(不必是整个字符串)的起始部分是否能够匹配模式。
find()
Matcher.find()可以在应用它的 CharSequence 中查找多个匹配:
模式\\w+会将输入的字符串拆分为单词,find()方法像迭代器那样向前遍历输入字符串。而另一版本的find()可以接收一个整数参数,来表示搜索开始的字符位置。从结果可以看出,后一个版本的 find() 方法能够根据其参数的值,不断重新设定搜索的起始位置。
分组(group)
组是用括号划分的正则表达式,可以根据组的编号来引用某个组。组号为 0 表示整个表达式,组号 1 表示被第一对括号括起来的组,以此类推。因此,下面这个表达式,
分组 0 为ABCD,分组 1 为 BC,分组 2 为C。
Matcher对象提供了一系列方法,可以获取与分组相关的信息:
public int groupCount()返回该模式中的分组数目,分组 0 不包含在此计数中。public String group()返回前一次匹配操作(如find())的第 0 个分组(即整个匹配)。public String group(i)返回前一次匹配操作期间给定的分组号,如果匹配成功,但是指定的组没有匹配输入字符串的任何部分,则将返回 null。public int start(int group)返回在前一次匹配操作中寻找到的分组的起始索引。public int end(int group)返回在前一次匹配操作中寻找到的分组的最后一个字符索引加一的值。
可以看到这个正则表达式模式有几个带括号的分组,由任意数目的非空白符(\\S+)及随后的任意数目的空白符(\\s+)所组成。目标是捕获每行的最后三个单词,行尾由$分隔。但在正常情况下是将$与整个输入序列的结尾进行匹配,因此我们必须明确让正则表达式注意输入中的换行符,这是通过序列开头的模式标记(?m)来完成的。
start()和end()
匹配成功后,start()返回本次匹配结果的起始索引,而把本次匹配结果最后一个字符的索引加一就是end()的返回值。如果匹配失败(或在尝试匹配操作之前),这时调用start()或end()会产生一个 IllegalStateException。下面的示例还同时展示了 matches() 和 lookingAt() 的用法:
注意,find() 可以在输入字符串的任意位置匹配正则表达式,而 lookingAt() 和 matches() 只有在正则表达式与输入的最开始处就开始匹配时才会成功。matches()只有在整个输入都匹配正则表达式时才会成功,而lookingAt()则仅在输入字符串的开始部分匹配时才会成功。
Pattern标记
Pattern类的compile()方法还有一个重载版本,它可以接受一个标记参数,来影响匹配行为:
其中 flag 来自 Pattern 类中的常量:
Pattern.CANON_EQ
当且仅当两个字符的完全正则分解匹配时,才会认为它们匹配。例如,当指定此标记时,表达式\u003f将匹配字符串?。默认情况下,匹配不考虑正则的等价性。
Pattern.CASE_INSENSITIVE(?i)
默认情况下,匹配仅在US-ASCII字符集中进行才不区分大小写。这个标记允许模式匹配时不考虑大小写。可以通过指定UNICODE_CASE标记,并结合这个标记来在Unicode字符集里启用不区分大小写的匹配。
Pattern.COMMENTS(?x)
在这种模式下,空格符将被忽略掉,并且以#开始直到行末的注释也会被忽略掉。UNIX的行模式也可以通过嵌入的标记表达式来启用
Pattern.DOTALL(?s)
在 dotall 模式下,表达式.匹配任何字符,包括换行符。默认情况下,表达式不匹配换行符
Pattern.MULTILINE(?m)
在多行模式下,表达式^和$分别匹配一行的开始和结束。^还匹配输入字符串的开始,而$还匹配输入字符串的结尾。默认情况下,这些表达式仅匹配整个输入字符串的开头和结尾
Pattern.UNICODE_CASE(?u)
当指定了这个标记,并且同时启用了CASE_INSENSITIVE标记时,不区分大小写的匹配将以符合Unicode标准的方式完成。默认情况下,匹配仅在US-ASCII字符集中进行时才不区分大小写
Pattern.UNIX_LINES(?d)
在这种模式下,在.,^和$的行为里,只有换行符被识别。
上表列举的标记中,特别有用的是下面几种:
Pattern.CASE_INSENSITIVE
Pattern.MUTILINE
Pattern.COMMENTS(对声明或文档有用)
注意,你也可以在正则表达式中直接使用上表中的大多数标记,只需要将左栏括号中的字符插入希望模式生效的位置之前即可。
可以通过“或”操作(|)组合这些标记,实现多种效果:
在这个例子中,我们创建了一个模式,它将匹配所有以“java”、“Java”和“JAVA”等开头的行,并且是在设置了多行标记的状态下,对每一行(从字符序列的第一个字符开始,至每一个行终止符)都进行匹配。注意,group()方法只返回已匹配的部分。
split()
split()根据输入的正则表达式拆分字符串,并返回拆分后的字符串数组。另外还提供了一种形式,可以限制拆分的次数:
这是一种在通用边界上拆分输入文本的便捷方法:
替换操作
正则表达式有一些可用于替换文本的方法:
replaceFirst(String replacement)用参数 replacement 替换输入字符串的第一个匹配的部分。replaceAll(String replacement)用参数 replacement 替换输入字符串每个匹配的部分。appendReplacement(StringBuffer sbuf, String replacement)执行逐步替换,并保存到 sbuf 中,而不是像replaceFirst()和replaceAll()那样分别替换第一个匹配和全部匹配。这是一个非常重要的方法,它允许你调用其他方法来生成或处理 replacement(replaceFirst()和replaceAll()只能放入固定的字符串),使你能够以编程的方式将目标分割成组,从而具备更强大的替换功能。在调用了一次或多次
appendReplacement()方法后,可以再调用appendTail(StringBuffer sbuf)方法,将输入字符串的剩余部分复制到 sbuf。
下面的程序演示了如何使用这些替换方法。开头部分注释掉的文本,就是正则表达式要处理的输入字符串:
这里使用上一章介绍的 Files 类来打开和读取文件。Files.line()会生成一个包含多行字符串的 Stream,而Collectors.joining()则将参数附加到每行的末尾,然后将它们合并成一个字符串。
mInput 匹配 /*! 和 !*/ 之间的所有文字(注意分组的括号)。接下来,将存在两个或两个以上空格的地方,缩减为一个空格,并且删除每行开头部分的所有空格(为了使每一行都达到这个效果,而不仅仅是删除文本开头部分的空格,这里特意开启了多行模式)。这两个替换操作所使用的的 replaceAll() 是 String 类自带的方法,它和 Matcher 里的 replaceAll()等效,在这里使用此方法更方便。注意,因为这两个替换操作都只使用了一次,所以并不需要将它与编译成 Pattern,这样开销会小一些。
replaceFirst()只对它找到的第一个匹配进行替换。此外,replaceFirst()和replaceAll()中用来替换的字符串只能是固定的文本,因此如果想在每次替换时进行一些处理,它们是做不到的。此时可以使用appendReplacement()方法,它允许你在执行替换的过程中操作用来替换的字符串。在示例中,我们先获取一个group(),这里表示正则表达式匹配到的元音字母,然后将其设置为大写,最后将结果写入 sbuf。通常我们逐步执行并替换所有的匹配项,然后调用appendTail(),但是若为了模拟replaceAll(),你可以只执行一次替换,然后调用appendTail()将剩余部分放入 sbuf。
同时,appendReplacement()方法还允许你通过\$g直接找到匹配的某个组,这里的 g 就是组号。然而,它只能应付一些简单的处理,无法实现类似前面这个例子中的功能。
reset()
通过 reset() 方法,可以将现有的 Matcher 对象应用于一个新的字符序列:
使用不带参数的 reset() 方法,可以将 Matcher 对象重新设置到当前字符序列的起始位置。
正则表达式和Java I/O
到目前为止,我们看到的例子都是将正则表达式用于静态的字符串。下面的例子将向你演示,如何应用正则表达式在一个文件中进行搜索匹配操作。JGrep.java 的灵感源自于 Unix 上的 grep。它有两个参数:文件名以及要匹配的正则表达式。输出的是每行有匹配的部分以及匹配部分在行中的位置。
虽然可以为每一行创建一个新的 Matcher 对象,但最好还是创建一个空的 Matcher 对象,然后在遍历时使用 reset() 方法来为 Matcher 对象加载对应的行,最后用find()来查找结果。
这里的测试参数是 WhitherStringBuilder.java,将该文件作为输入,然后搜索单词return、for或String。
扫描输入
到目前为止,从文件或标准输入读取数据还是一件相当痛苦的事情。一般的解决办法就是读入一行文本,对其进行分词,然后使用 Integer、Double 等类的各种解析方法来解析数据:
input 字段使用了来自 java.io 的类,StringReader 将字符串转换为可读流对象,用于创建 BufferedReader,因为 BufferedReader 有一个 readLine()方法,这样就可以每次从 input 对象里读取一行,就好像从控制台读入标准输入一样。
readLine()方法将获取的每行输入转为 String 对象。当一行数据只对应一个输入时处理比较简单,但是如果两个输入值在一行上,就必须拆分该行,才能解析每个输入。
Java 5 新增了 Scanner 类,它可以大大减轻扫描输入的工作负担:
Scanner 的构造器可以接收任何类型的输入对象,包括 File 对象、InputStream、String,或者这个例子中的 Readable(Java 5引入的一个接口,用于描述"具有read()方法的东西"),BufferedReader 就属于这一类。
在 Scanner 中,输入、分词和解析这些操作都被包含在各种不同类型的"next"方法中。一个普通的next()返回下一个 String,所有基本类型(除了char)以及 BigDecimal 和 BigInteger 都有对应的"next"方法。所有的"next"方法都是阻塞的,即只有在找到一个完整的分词之后才会返回。还可以根据相应的"hasNext"方法,判断下一个输入分词是否是所需的类型,如果是则返回 true。
在 BetterRead.java 中没有针对 IOException 的 try 块。这是因为 Scanner 会假设 IOException 表示输入结束,因此 Scanner 会把 IOException 隐藏起来。不过最近发生的异常可以通过它的ioException()方法获得,因此可以在必要时检查它。
Scanner 分隔符
默认情况下,Scanner 通过空格分隔输入数据,但也可以用正则表达式的形式来指定自己的分隔符模式:
该示例使用逗号(由任意数量的空白符包围)作为分隔符,来处理读取的给定字符串。同样的技术也可以用来读取逗号分隔的文件。我们可以用 useDelimiter() 来设置分隔符,同时,还有一个 delimiter() 方法,用来返回当前正在作为分隔符使用的 Pattern 对象。
使用正则表达式扫描
除了扫描预定义的基本类型,还可以用自己定义的正则表达式模式来扫描,这在扫描更复杂的数据时非常有用。下面的例子将扫描一个防火墙日志文件中的威胁数据:
next()与特定模式一起使用时,该模式会和下一个输入分词进行匹配,结果由 match() 方法提供,如上面示例,它的工作方式与之前看到的正则表达式匹配相似。
使用正则表达式扫描时,有一点要注意:该模式仅与下一个输入的分词进行匹配。因此,如果模式中包含了分隔符,就永远不会匹配成功。
StringTokenizer
在正则表达式(Java 1.4引入)和 Scanner 类(Java 5引入)之前,对字符串进行拆分的方式是使用 StringTokenizer 对其分词。但是现在有了正则表达式和 Scanner 类,对于同样的功能实现起来更容易和简洁。下面是 StringTokenizer 和其它两种技术的简单比较:
使用正则表达式或 Scanner 对象,还可以使用更复杂的模式来拆分字符串,而对于 StringTokenizer 就比较困难了。所以可以说 StirngTokenizer 已经过时了。
小结
过去,Java 对于字符串操作的技术相当不完善。不过随着近几个版本的升级,我们可以看到,Java 已经从其他语言中吸取了许多成熟的经验。到目前为止,它对字符串操作的支持已经很完善了。不过,有时你还要在细节上注意效率问题,例如恰当地使用 StringBuilder 等。