17、第十八章字符串

+的重载与StringBuilder

用于String++=是Java中仅有的两个重载过的操作符,Java不允许程序员重载任何其他的操作符。编译器自动引入了java.lang.StringBuilder类进行了优化。

StringBuilder是Java SE5引入的,在这之前用的是StringBuffer。后者是线程安全的,因此开销也会大些。使用StringBuilder进行字符串操作更快一点。

可以用JDK自带的javap工具来反编译:

javap -c Concatenation.class

格式化输出

printf()

Java SE5推出了C语言中 printf() 风格的格式化输出这一功能。

System.out.format()

format()方法模仿了C语言的printf()format()printf()是等价的。

Formatter类

在Java中,所有的格式化功能都是由java.util.Formatter类处理的。可以将Formatter看做一个翻译器,它将你的格式化字符串与数据翻译成需要的结果。

Formatter的重载构造器支持输出到多个路径,不过最常用的还是PrintStreamOutputStreamFile

格式化修饰符

语法:

%[argument_index$][flags][width][.precision]conversion

最常见的应用是控制一个字段的最小长度,这可以通过指定width来实现。

width相对的是precision,用于指定最大长度。width可以应用于各种类型的数据转换,并且其行为方式都一样。precision则不然,不是所有的类型都能使用precision,而且,应用于不同类型的数据转换时,precision的意义也不同。在将precision应用于String时,它表示打印string时输出字符的最大数量。而在将precision应用于浮点数时,它表示小数部分要显示出来的位数(默认是6位小数),如果小数位数过多则舍入,太少则在尾部补零。由于整数没有小数部分,所以precision无法应用于整数,如果你对整数应用precision,则会触发异常。

Formatter转换

类型 含义
d 整型(十进制)
c Unicode字符
b Boolean值
s String
f 浮点数(十进制)
e 浮点数(科学计数)
x 整型(十六进制)
h 散列码(十六进制)
% 字面值“%”

String.format()

String.format()是一个static方法,它接受与Formatter.format()方法一样的参数,但返回一个String对象。当你只需使用一次format()方法的时候,String.format()用起来很方便。

String string = String.format("(t%d, q%d) %s", transactionID, queryID, message);

正则表达式

正则表达式是一种强大而灵活的文本处理工具。

基础

在其他语言中,\\表示“我想要在正则表达式中插入一个普通的(字面上的)反斜线,请不要给它任何特殊的意义”。而在Java中,\\的意思是“我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。”例如,如果你想表示一位数字,那么正则表达式因该是\\d。如果你想插入一个普通的反斜线,应该这样写\\\。不过换行符和制表符之类的东西只需要使用单反斜线:\n \t

在正则表达式中,用括号将表达式进行分组,用竖线|表示或操作。

\\W,它的意思是一个非单词字符(如果W小写,\\w,则表示一个单词字符)。

应用正则表达式最简单的途径,就是利用String类内建的功能:

boolean matches = "-1234".matches("-?\\d+");
String[] split = knights.split(regex);
String replaceFirst = s.replaceFirst("f\\w+", "located");
String replaceAll = s.replaceAll("shrubbery|tree|herring", "banana");

创建正则表达式

正则表达式的完整构造子列表,请参考JDK文档java.util.regex包中的Pattern类。

表达式 含义
B 指定字符B
\xhh 十六进制值为0xhh的字符
\uhhhh 十六进制表现为0xhhhh的Unicode字符
\t 制表符Tab
\n 换行符
\r 回车
\f 换页
\e 转义(Escape)

以下是一些创建字符类的典型方式,以及一些预定义的类:

表达式 含义
. 任意字符
[abc] 包含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 空白符(空格、tab、换行、换页、回车)
\S 非空白符([^\s])
\d 数字([0-9])
\D 非数字([^0-9])
\w 词字符([a-zA-Z_0-9])
\W 非词字符([^\w])
逻辑操作符 含义
XY Y跟在X后面
X Y
(X) 捕获组(capturing group)。可以在表达式中用\i引用第i个捕获组
边界匹配符 含义
^ 一行的开始
$ 一行的结束
\b 词的边界
\B 非词的边界
\G 前一个匹配的结束

目的并不是编写最难理解的正则表达式,而是尽量编写能够完成任务的、最简单以及最必要的正则表达式。一旦真正开始使用正则表达式了,你就会发现,在编写新的表达式之前,你通常会参考代码中已经用到的正则表达式。

量词

量词描述了一个模式捕获输入文本的方式:

  • 贪婪型: 量词总是贪婪的,除非有其他的选项被设置。贪婪表达式会为所有可能的模式发现尽可能多的匹配。导致此问题的一个典型理由就是假定我们的模式仅能匹配第一个可能的字符组,如果它是贪婪的,那么它就会继续往下匹配。
  • 勉强型: 用问号来指定,这个量词匹配满足模式所需的最少字符数。因此也被称作懒惰的、最少匹配的、非贪婪的或不贪婪的。
  • 占有型: 目前,这种类型的量词只有在Java语言中才可用(在其他语言中不可用),并且也更高级,因此我们大概不会立刻用到它。当正则表达式被应用于String时,它会产生相当多的状态,以便在匹配失败时可以回溯。而“占有的”量词并不保存这些中间状态,因此它们可以防止回溯。它们常常用于防止正则表达式失控,因此可以使正则表达式执行起来更高效。

贪婪型| 勉强型 | 占有型 | 如何匹配
---|---
X?| X?? | X?+ | 一个或零个X
X* | X*? | X*+ | 零个或多个X
X+| X+? | X++ | 一个或多个X
X{n} | X{n}? | X{n}+ | 恰好n次X
X{n,} | X{n,}? | X{n,}+ | 至少n次X
X{n,m} | X{n,m}? | X{n,m}+ | X至少n次,但不超过m次

应该非常清楚地意识到,表达式X通常必须要用圆括号括起来,以便它能够按照我们期望的效果去执行。

在使用正则表达式时很容易混淆,因为它是一种在Java之上的新语言。

CharSequence

接口CharSequenceCharBufferStringStringBufferStringBuilder类中抽象出了字符序列的一般化定义。多数正则表达式操作都接受CharSequence类型参数。

Pattern和Matcher

导入java.util.regex包,然后用static Pattern.compile()方法来编译你的正则表达式。它会根据你的String类型的正则表达式生成一个Pattern对象。接下来,把你想要检索的字符串传入Pattern对象的matcher()方法。matcher()方法会生成一个Matcher对象,它有很多功能可用。

Pattern类还提供了一个static方法:

static boolean matches(String regex, CharSequence input)

编译后的Pattern对象还提供了split()方法,它从匹配了regex的地方分割输入字符串,返回分割后的子字符串String数组。

通过调用Pattern.matcher()方法,并传入一个字符串参数,我们得到了一个Matcher对象。使用Matcher上的方法,我们将能够判断各种不同类型的匹配是否成功:

boolean matches() 
boolean lookingAt() 
boolean find() 
boolean find(int start)

find()

Matcher.find()方法可用来在CharSequence中查找多个匹配。

find()方法像迭代器那样向前遍历输入字符串。重载的find()接收一个整型参数,该整数表示字符串中字符的位置,并以其作为搜索的起点,能够根据其参数的值,不断重新设定搜索的起始位置。

组(Groups)

组是用括号划分的正则表达式,可以根据组的编号来引用某个组。组号为0表示整个表达式,组号1表示被第一对括号括起来的组,以此类推。

Matcher对象提供了一系列方法,用以获取与组相关的信息:
public int groupCount() 返回该匹配器的模式中的分组数目,组0不包括在内。
public String group() 返回前一次匹配操作(例如find())的第0组(整个匹配)。
public String group(int i) 返回前一次匹配操作期间指定的组号,如果匹配成功,但是指定的组没有匹配输入字符串的任何部分,则将返回null
public int start(int group) 返回在前一次匹配操作中寻找到的组的起始索引。
public int end(int group) 返回在前一次匹配操作中寻找到的组的最后一个字符索引加一的值。

start()和end()

注意,find()可以在输入的任意位置定位正则表达式,而lookingAt()matches()只有在正则表达式与输入的最开始处就开始匹配时才会成功。matches()只有在整个输入都匹配正则表达式时才会成功,而lookingAt()只要输入的第一部分匹配就会成功。

Pattern标记

Pattern类的compile()方法还有另一个版本,它接受一个标记参数,以调整匹配行为:

Pattern Pattern.compile(String regex, int flag)

其中的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) 在这种模式下,在.、^和$的行为中,只识别行终止符\n

在这些标记中,Pattern.CASE_INSENSITIVEPattern.MULTILINE以及Pattern.COMMENTS(对声明或文档有用)特别有用。请注意,你可以直接在正则表达式中使用其中的大多数标记,只需要将上表中括号括起来的字符插入到正则表达式中,你希望它起作用的位置即可。还可以通过“或”(|)操作符组合多个标记的功能。

Pattern p = Pattern.compile("^java", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);

reset()

通过reset()方法,可以将现有的Matcher对象应用于一个新的字符序列。

扫描输入

Scanner的构造器可以接收任意类型的输入对象,包括File对象、InputStreamString或者Readable实现类。Readable是Java SE5中新加入的一个接口,表示“具有read()方法的某种东西”。

所有的基本类型(除char之外)都有对应的next方法,包括BigDecimal和BigInteger。所有的next方法,只有在找到一个完整的分词之后才会返回。Scanner还有相应的hasNext方法,用以判断下一个输入分词是否是所需的类型,如果是则返回true。

默认情况下,Scanner根据空白字符对输入进行分词,可以用正则表达式指定自己所需的分隔符。可以用useDelimiter()来设置分隔符,同时,还有一个delimiter()方法,用来返回当前正在作为分隔符使用的Pattern对象。

除了能够扫描基本类型之外,还可以使用自定义的正则表达式进行扫描。当next()方法配合指定的正则表达式使用时,将找到下一个匹配该模式的输入部分,调用match()方法就可以获得匹配的结果。在配合正则表达式使用扫描时,有一点需要注意:它仅仅针对下一个输入分词进行匹配,如果你的正则表达式中含有分隔符,那永远不可能匹配成功。

StringTokenizer类

在Java引入正则表达式(J2SE1.4)和Scanner类(Java SE5)之前,分割字符串的唯一方法是使用StringTokenizer来分词。不过,现在有了正则表达式和Scanner,我们可以使用更加简单、更加简洁的方式来完成同样的工作了。

使用正则表达式或Scanner对象,我们能够以更加复杂的模式来分割一个字符串,而这对于StringTokenizer来说就很困难了。基本上,我们可以放心地说,StringTokenizer已经可以废弃不用了。