13.1
String对象是不可变的。String类中每一个看起来会修改String值的方法实际上都是创建了一个全新的String对象来包含修改后的字符串内容,而最初的String对象则丝毫未动。
13.2
用于String的+与+=是Java中仅有的两个重载过的操作符,并不允许程序员重载任何操作符。
作者在解释String不可变性对效率的影响时使用了javap工具,它是Java Class文件分解器,可以反编译。javap -c命令用来分解代码,从输出上看就是按函数将字节码拆分显示。
通过查看字节码,知道了由于String的不可变性,为了提高运行效率例如下面的语句:
String s = "abc" + mango + "def" + 47;
编译时编译器自动引入了java.lang.StringBuilder类,用它的append()方法完成字符串拼接,最后用它的toString()方法返回结果字符串。
作者然后演示了一个在循环中使用拼接的例子,如果让编译器自动优化,那么在每个循环中就会创建一个StringBuilder对象;在这时我们应该在代码中自己定义StringBuilder来提高效率。
StringBuilder是Java SE5引入的,在此之前使用StringBuffer,StringBuffer是线程安全的,因此开销也会大一些。
练习1分析reusing/SprinklerSystem.java的SprinklerSystem.toString()方法(方法中使用了+重载形式),如果明确的使用StringBuilder对象是否产生过多的StringBuilder对象。
首先,使用javap -c分解原SpringklerSystem代码,发现编译器自动为我们创建了一个StringBuilder对象,然后我修改程序自己创建一个StringBuilder对象,然后将代码中的每一行加入到StringBuilder.append()方法(每一行中也使用了+重载):
StringBuilder sb = new StringBuilder();
sb.append("valve1 = " + valve1 + " ");
sb.append("valve2 = " + valve2 + " ");
sb.append("valve3 = " + valve3 + " ");
sb.append("valve4 = " + valve4 + " ");
sb.append("i = " + i + " " + "f = " + f + " ");
sb.append("source = " + source);
return sb.toString();
这种情况下使用javap -c查看,会创建了很多的StringBuilder,每一行一个。这种情况表面上使用了StringBuilder,而且代码看起来也很整洁,但是效率却没有以下形式高:
"valve1 = " + valve1 + " " +
"valve2 = " + valve2 + " " +
"valve3 = " + valve3 + " " +
"valve4 = " + valve4 + " " +
"i = " + i + " " + "f = " + f + " " +
"source = " + source;
因为编译器自动替我们优化,仅仅使用了一个StringBuilder。
13.4
String类的大多数方法,当需要改变字符串的内容时,方法都返回一个新的String对象;如果内容没有改变方法仅仅返回指向原字符串对象的引用。
13.5
从Java 1.5开始加入了c中printf()函数的功能,它可以用于 PrintStream类和PrintWriter类,因为System.out对象是一个PrintStream对象,故它拥有format()方法和printf()方法(两者功能一样,等同于c中的printf()方法用于格式化输出)。
Java5中新添加的格式化输出功能都由java.util.Formatter类处理。可以将Formatter看作一个翻译器,它将格式化字符串和数据翻译成想要的结果。创建Formatter对象时需要向构造函数传递一些信息,告诉Formatter将翻译的结果输出到哪里。
Formatter构造函数经过重载可以接受多种输出目的地,不过最常用的还是PrintStream、OutputStream和FIle。
格式化说明符
常规类型、字符类型和数值类型的格式说明符语法如下:
%[argument_index$][flags][width][.precision]conversion
argument_index是一个十进制整数,用于表面参数在参数列表中的位置。第一个参数由“1$”引用,第二个参数由“2$”引用,以此类推;
flags是修改输出格式的字符集;
width指定输出的最小宽度,必要时通过添加空格来维持最小宽度,默认是右对齐,可通过添加-改变对齐方向。
precision,应用于String表示打印出字符的最大长度,应用于浮点数表示小数点后要显示出的位数,超出的做舍入运算,不够的添加0,默认是6位。
conversion是表明应该如果格式化参数的字符。
用于表示日期和时间类型的格式说明符语法如下:
%[argument_index$][flags][width]conversion
需要特殊注意的是 %b 转换成boolean类型,对所有引用只要不会Null都会输出true,经过编码测试
int i = 0;
f.format("%b\n", i);
输出的依旧是true。
Java 1.5在String类中添加了静态方法format(),它接受与Formatter.format一样的参数,但返回一个String对象。其实在String.format()内部也使用Formatter完成相应的任务。
13.6
使用String对象的matches()方法可以判断该对象是否匹配方法参数表示的正则表达式;
String对象的replaceFirst()或者replaceAll()能将对象中成功匹配正则表达式的部分替代为参数中的字符串。
正则表达式量词的三种类型:
贪婪型:尽可能多的匹配;
勉强型或非贪婪型:尽可能少的匹配;例如(例子摘自http://www.jb51.net/article/31491.htm):
源字符串:aa
test1
bb
test2
cc
正则表达式一:
.*
匹配结果一:
test1
bb
test2
正则表达式二:
.*?
匹配结果二:
test1
(这里指的是一次匹配结果,所以没包括
test2
)
第一种正则表达式是贪婪型,第二种是勉强型。
占有型,它是Java正则表达式独有的,支配就是对整个字符串进行一次匹配,匹配之后返回,并不回溯。这个不大好理解,有一个例子摘自http://bbs.youkuaiyun.com/topics/390269371比较形象:
字符串为bbb,正则表达式为[b]*+,这是一个贪婪的匹配,直接返回bbb。但是如果是占有型的,正则为[b]*+b,返回结果是false,是空。
如果a*a,*是匹配优先的,也就是说先匹配,如果正则的后续部分不能再匹配,就回溯,在这个例子中,匹配字符串aaa的时候,首先a*匹配到最后一个,然后发现正则后面还有一个a没法匹配,就会将a*回溯到字符串的中间一个a,这时候正则中的最后一个a与字符串的最后一个a正好匹配,匹配结束。
如果正则是a*+a,*+是占有优先,也就是说*+前面的字符会尽可能匹配,匹配了的就不会再回溯,不会让回去了,即所谓占有。如果字符串是aaa,那么这个例子中匹配过程就是a*+匹配了字符串的三个a,正则中的最后一个a不会再被匹配,因为a*+不会回溯。
接口java.lang.CharSequence从CharBuffer、String、StringBuilder和StringBuffer类中抽象出了字符序列的一般定义:
interface CharSequence {
charAt(int i);
length();
subSequence(int start, int end);
toString();
}
多数正则表达式的操作都接受CharSequence类型的参数。
使用java.util.regex.Pattern类的静态函数complie()方法来编译正则表达式。它会根据String类型的正则表达式生成一个Pattern对象。接下来把想要检索的字符串传入Pattern对象的matcher()方法。matcher()方法会生成一个Matcher对象。matches()方法用来判断整个输入字符串是否匹配正则表达式模式,而lookingAt()用来判断该字符串(不必是整个字符串)的始部分是否能够匹配模式。find()方法尝试查找与该模式匹配的输入序列的下一个子序列。group()方法匹配的输入子序列。find(int i)的重载版本,输入的整数参数是字符串中字符的位置,以其作为搜索的起点。注意如果使用
while(m.find(i)) {
}
这种形式,一定记得在循环里改变i的值或者加入break条件,否则每次m.find(i)都从固定的位置开始查找匹配,会陷入死循环。
类Pattern类提供了静态方法
static boolean matches(String regex, CharSequence input)
该方法用以检查regex是否匹配了整个input参数。编译后的Pattern对象还提供了split()方法,它从匹配了regex的地方分隔输入字符串,返回分隔后的子字符串String数组。
(?i)在Java的正则表达式中表示忽略大小写。
组是用括号划分的正则表达式,可以用组号来引用组,组号为0表示整个正则表达式,组号为1表示第一个括号包含的正则表达式,以此类推。例如:
A(B(C))D
group0表示ABCD,group1表示BC,group2表示C。
Matcher类的对象有一系列获取组相关信息的方法:
public int groupCount()返回该匹配器模式中的分组数目,第0组不包括在内。
public String group()返回前一次匹配模式操作(例如find())的第0组。
public String group(int i )返回前一次匹配模式操作指定组号的组,如果匹配成功,但是指定的组没有匹配输入字符串的任何部分,则会返回Null;
public in start(int group)返回在前一次匹配操作中寻找到的组的起始索引;
public int end(int group)返回在前一次匹配操作中寻找到的组的最后一个字符索引加一的值。
例子中的正则表达式使用了(?m),正常情况下将$与整个输入序列的末端相匹配,使用模式标记(?m)显示的告诉正则表达式注意输入序列中的换行符。
在匹配操作成功之后,start()返回先前匹配的起始位置的索引,end()返回匹配的最后字符的索引加1的值。匹配操作失败之后(或先与一个正在进行的匹配操作去尝试)调用start()或end()将会产生IllegalStateException。
find()可以在输入的任意位置定位正则表达式,而lookingAt()和matches()只有在正则表达式与输入的最开始处就开始匹配时才会成功。matches()只有在整个输入都匹配正则表达式时才会成功,而lookingAt()只要输入的第一部分匹配就会成功。
Pattern类的compile()方法有一个重载版本,它接受一个flag参数,以调整匹配的行为:
Pattern Pattern.compile(String regex, int flag);
Pattern.Case_INSENSITIVE、Pattern.NULTILINE以及Pattern.COMMENTS比较常用。另外,可以直接在正则表达式中使用其中大多数标记,只要将上表括号括起的字符插入到正则表达式中希望起到作用的位置即可。
Pattern对象的
String[] split(CharSequence input)
String[] split(CharSequence input, int limit)
将输入字符串根据正则表达式断开成字符串对象数组。第二种形式的split()方法可以限制输入分割成字符串的数量。
Matcher类的对象有一个强大的appendReplacement(StringBuffer, String)方法,它执行以下操作:
(1)从添加位置开始在输入序列读取字符并将其添加到StringBuffer中,在匹配之前的那一字符停止;
(2)将给定的字符串添加到StringBuffer;
(3)将此匹配器的添加位置设置为最后匹配位置的索引加1,即end();
在执行一次或多次appendReplacement()之后,调用appendTail(StringBuffer)方法将输入字符串剩余的部分复制到sbuf中。替换字符串还可以包含匹配的组引用,$g将被group(g)的计算结果替换。
Matcher对象的reset()方法可以重新设定字符序列。
练习17、18、19让我们解析java源代码文件,使用正则表达式时总有想不全的情况,写出正确全面的正则表达式非常有挑战性,作者推荐了一个java代码解析器叫javacc。
13.7
Java 1.5新增了java.util.Scanner类,它的构造器可以接受任何类型的输入对象,包括File对象、InputStream、String或者Readable对象。Readable是1.5新增的接口,表示具有read()方法的某种类,它的实现类主要包括**Reader。用Scanner,所有的输入、粉刺以及翻译的操作 都隐藏在不同类型的next方法中。普通的next()方法返回下一个String。所有基本类型(除char之外)都有对应的next方法,包括BigDecimal和BigInteger。所有的next方法只有在找到一个完整的分词之后才会返回。还有相应的hasNext方法,用来判断下一个输入分词是否是所需的类型。
Scanner的操作不会抛出IOException,而是将它们吞掉,可以使用ioException()方法返回最近底层产生的IOException。
默认情况下Scanner使用空白字符对输入进行分词,可以使用正则表达式指定自己需要的定界符:
useDelimiter(String regex);
delimiter()方法用来返回该Scanner对象的定界符使用的正则表达式的Pattern对象。
next(Pattern)和hasNext(Pattern)两个函数的重载版本使用正则表达式匹配。需要注意的是,它仅仅针对下一个分词进行匹配,如果正则表达式中含有定界符,匹配永远不会成功。
13.8
作者提到StringTokenizer类,在Java引入正则表达式(1.4开始)和Scanner类后(1.5开始),StringTokenizer处于废弃状态,官方api推荐使用Stirng.split()或java.util.regex包中的功能替代它。