作者:禅楼望月(http://www.cnblogs.com/yaoyinglong/)
1.字符串可以被GC回收了
我们之前在表达式的陷阱中就说到“对于Java程序中的字符直接量,JVM会使用一个字符串池来保护他们:当第一次使用某个字符串直接时,JVM会将它们放入字符串池进行缓存。”在jdk1.7之前HotSpot将该字符串常量池放在永久代中,所以当初我们还说“在一般情况下,字符串缓冲池中字符串对象不会被垃圾回收”,但是jdk1.7以后HotSpot就将字符串常量池从永久代中移出。因此我们看到如下程序,并不会导致内存溢出:
public class StringTest { public static void main(String[] args){ List<String> list=new ArrayList<String>(); int i=0; while(true){ list.add(String.valueOf(i++).intern()); } } }
这段代码在jdk1.7中会一直循环下去,但是在jdk1.6中会报“OutOfMemoryError:PermGen space”错误。这样java字符串常量池就回归到了堆内存中,接受垃圾回收器的垃圾回收。
2.String是不可变的
创建好一个String之后,它就不会再被改变了,查看JDK文档会发现,String类中看似修改了String值的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容,而最初的String对象纹丝未动。
3.String重载了“+”运算符
Java不允许Java程序员重载运算符。
public class StringTest { public static void main(String[] args){ String mango="mango"; String s="abc"+mango+"def"+47; System.out.println(s); } }
通过javap来反编译以上代码:
E:\program\Thinking_in_Java\bin\string>javap -c StringTest.class Compiled from "StringTest.java" public class string.StringTest { public string.StringTest(); Code: 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #16 // String mango 2: astore_1 3: new #18 // class java/lang/StringBuilder 6: dup 7: ldc #20 // String abc 9: invokespecial #22 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 12: aload_1 13: invokevirtual #25 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 16: ldc #29 // String def 18: invokevirtual #25 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: bipush 47 23: invokevirtual #31 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 26: invokevirtual #34 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 29: astore_2 30: getstatic #38 // Field java/lang/System.out:Ljava/io/PrintStream; 33: aload_2 34: invokevirtual #44 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 37: return }
从上面的的反编译代码我们可以清楚的看到,编译器默认使用StringBuilder对我们的字符串做了优化。这样就避免了直接使用String和“+”运算符拼接所产生的中间垃圾。既然,JDK已经为我们做了字符串的优化,那我们是不是肆无忌怛的使用“+”了呢?那么我们再来看看编译器到底为我们优化到什么程度:
public class StringTest { 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(); } public static void main(String[] args){ } }
首先我们查看反编译后的第一个函数:
public java.lang.String implicit(java.lang.String[]); Code: 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
从上面反编译的代码可以看出,从第4到第32行是for循环体,同时我们也看到,StringBuilder是在for循环中创建的,即每循环一次就创建一新的StringBuilder。再看看第二个函数:
public java.lang.String explicit(java.lang.String[]); 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
不仅代码缩短了,而且整个方法中只在for循环的外面生成了一个StringBuilder对象。
总结:循环中处理字符串最好自己创建一个StringBuilder对象,并用它来构造最终的结果,循环之外则可以信赖编译器对String的优化。
4.一个陷阱:
public class StringTest { public static void main(String[] args){ String str1="abc"; StringBuilder sb=new StringBuilder(); sb.append("is true? "); sb.append(str1+str1.length()); System.out.println(sb.toString()); } }
这段代码,编译器会怎么处理呢?
public static void main(java.lang.String[]); Code: 0: ldc #16 // String abc 2: astore_1 3: new #18 // class java/lang/StringBuilder 6: dup 7: invokespecial #20 // Method java/lang/StringBuilder."<init>":()V 10: astore_2 11: aload_2 12: ldc #21 // String is true? 14: invokevirtual #23 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17: pop 18: aload_2 19: new #18 // class java/lang/StringBuilder 22: dup 23: aload_1 24: invokestatic #27 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String; 27: invokespecial #33 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 30: aload_1 31: invokevirtual #36 // Method java/lang/String.length:()I 34: invokevirtual #40 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 37: invokevirtual #43 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 40: invokevirtual #23 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 43: pop 44: getstatic #47 // Field java/lang/System.out:Ljava/io/PrintStream; 47: aload_2 48: invokevirtual #43 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 51: invokevirtual #53 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 54: return
由上面代码我们可以看出,在sb.append(str1+str1.length()); 这句代码中,编译器还是采取了创建一个StringBuilder在拼接括号里面的字符串,然后将这个StringBuilder的toString传递给外围的StringBuilder。
结论:禁止在StringBuilder的append中使用“+”。如果StringBuilder处于循环中就更糟糕了。