###本章主题
在Web应用中,字符串算是使用率最高的结构了。我们查看JDK文档会发现,String对象是不变的,String类中每一个看起来会修改String对象的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容。而最初的String对象则丝毫未动。
在字符串领域有3个大头:
- String
- StringBuffer
- StringBuilder
具体参考原来的一篇博文:Java字符串之String、StringBuffer、StringBuilder
###1. String对象不可变
这点在实际应用中特别重要:String对象不可变,查看JDK文档会发现,String类中每一个改变String对象的操作,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容。而最初的String对象则丝毫不动。举个例子一看便知:
package Chapter13;
public class Immutable {
public static void main(String[] args) {
String s = "aaa";
String b = s.toUpperCase();
System.out.println(s);
System.out.println(b);
}
}
/** output:
aaa
AAA
*/
我们以为的是调用s.toUpperCase()之后,b引用指向修改后的s本身。那么,s和b都指向了AAA。但事实是又创建了一个新的String对象来存放修改后的s
学会一招怎么分析,就是通过javap -c 类名
的方法,可以查看Java编译器产生的中间码,上面的例子中产生的中间码为:
Code:
0: ldc #2 // String mango
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String abc
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_1
16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: ldc #7 // String def
21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: bipush 47
26: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
29: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: astore_2
33: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
36: aload_2
37: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
40: return
}
可以很清晰的看到,new了一个StringBuilder,调用了4次append,最后调用toString存储到astore_2中。
还有一个经典的例子必须说明一下:
package Chapter13;
public class WitherStringBuilder {
public String implicit(String[] fields) {
String result = "";
for(int i = 0; i < fields.length; i++) {
result += fields[i];
}
return result;
}
public String explicit(String[] fields) {
StringBuilder result = new StringBuilder();
for(int i = 0; i < fields.length; i++) {
result.append(fields[i]);
}
return result.toString();
}
}
里面是生成2种字符串的方法,一般项目中都会使用前一种。但如果我们用反编译的方法查看它们各自的中间码,就会发现问题:
implicit的反编译结果:
0: ldc #16 // String
2: astore_2
3: iconst_0
4: istore_3
5: goto 32
8: new #18 // class java/lang/StringBuilder
11: dup
12: aload_2
13: invokestatic #20 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #26 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
19: aload_1
20: iload_3
21: aaload
22: invokevirtual #29 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: invokevirtual #33 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
28: astore_2
29: iinc 3, 1
32: iload_3
33: aload_1
34: arraylength
35: if_icmplt 8
38: aload_2
39: areturn
explicit的反编译结果:
Code:
0: new #18 // class java/lang/StringBuilder
3: dup
4: invokespecial #45 // Method java/lang/StringBuilder."<init>":()V
7: astore_2
8: iconst_0
9: istore_3
10: goto 24
13: aload_2
14: aload_1
15: iload_3
16: aaload
17: invokevirtual #29 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: pop
21: iinc 3, 1
24: iload_3
25: aload_1
26: arraylength
27: if_icmplt 13
30: aload_2
31: invokevirtual #33 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: areturn
对比两个反编译结果,就会发现问题了。在implicit中,8-35行是一个循环,在循环的每一次生成一个StringBuilder进行操作,而在explicit中,只生成一个StringBuilder。而且如果你知道最终生成的字符串大概长度,你可以在定义的时候给StringBuilder预先声明大小,这样又可以避免多次重新分配缓冲。
所以,总结一下就是:
当你为一个类编写字符串拼接或者是实现toString()方法时,需要先考虑拼接操作是否简单,如果足够简单,你就可以信赖编译器会为你构造最高效的字符串结果。而如果字符串拼接比较麻烦,比如带有for循环或者迭代之类的,就要考虑创建一个StringBuilder来完成拼接,这样,自始至终只有一个StringBuilder对象,而不是编译器那样创建多个。如果这你都确定不了,还有一个方法就是用```javap -c 类(编译后的class文件)看看生成的中间码过程,根据这个分析是百分百靠谱的。
tips:
不要出现StringBuilder.append(a + ":" + c);
这样的代码,因为这样会让编译器在括号内重新生成一个StringBuilder对象来进行拼接。要确定一个append()中只含有一个字符串。
###2. 无意识的递归
判断下面程序的输出:
package Chapter13;
import java.util.*;
public class InfiniteRecursion {
public String toString() {
return "InfiniteRecursion address: " + this + "\n";
}
public static void main(String[] args) {
List<InfiniteRecursion> v = new ArrayList<InfiniteRecursion>();
for(int i = 0; i < 10; ++i) {
v.add(new InfiniteRecursion());
}
System.out.println(v);
}
}
大眼一看,就知道是打印数组中元素的内存地址。但实际一执行,发现stackoverflow了。很明显,是栈溢出。说明程序中有递归,而且递归没有终止条件。问题就出在toString()上,在遇到this时,String需要把this和前面的字符串合并,但是this不是String类型,于是编译器会尝试将this(InfiniteRecursion类型)转换为String类型,方法就是调用InfiniteRecursion的toString(),于是无限递归产生。
改进方法很简单,调用父类(Object)的toString(),这样就可以了。但是道理我还没想清楚。
###3. Java格式化输出
简单说几点就够了:
System.out.println("%d %f", x, y);
System.out.printf("%d %f", x, y);
- java.util.Formatter类
String.format("%d %f", x, y);
###4. 正则表达式
这个其实算是正则表达式在Java中的一种应用吧,其实正则表达式算是一种语法,推荐这个教程:正则表达式30分钟入门教程
然后还有一个小游戏:东北linux - 正则表达式闯关
###5. Scanner
因为在项目中写的Java程序是处于后台的,不会和用户有交互。所以没有用到输入相关的操作,但是在日常的使用中,扫描输入是一个非常常见的功能,比如ACM中JAVA的输入,记得当时是使用Scanner类。现在看到这一点,大概知道怎么用了。就是扫描标准输入流Scanner scanner = new Scanner(System.in);
,然后取得值的方法和迭代器的next相似,是跳过区域的值。