String对象是immutable
1、String对象是不可变的,String的每个看起来都会修改对象值的方法都是创建一个新的对象。
2、uppcase传递的参数s是s1这个引用的拷贝,是值传递,而不是直接传引用s1。并且s在uppcase运行结束就消失了,跟原本的对象没有关联,方法 返回的是一个新对象的引用。
3、在代码中,可以创建多个某一个String对象的别名。但是这些别名都是的引用是相同的。 比如s1和s3都是”ljs”对象的别名,别名保存着到真实对象的引用。所以s1 = s3
public static String upcase(String s){
return s.toUpperCase();
}
public static void main(String[] args) {
String s1 = "ljs ai csy";
System.out.println(s1);
String s2 = upcase(s1);
System.out.println(s2);
System.out.println(s1);
String s3 = s1;
System.out.println(s3==s1);
}
# ljs ai csy
# LJS AI CSY
# ljs ai csy
# true
重载+遇到的问题
1、既然String对象是immutable,那么String对象的重载+就会出现一个问题。(java不允许程序员重载操作符,c++可以),两个以上的String对象拼接必定会产生多余的中间String对象。
2、例如下面要得到ljslovecsy1314,就先生成ljslove这个临时String对象,然后在生成ljslovecsy这个临时String对象,最后才能生成我们想要的对象。这其中的两个临时对象没有主动回收,肯定会占一定的空间,假如有上百个,代价不就很大,性能也就下降。
public static void main(String[] args) {
String ljs = "ljs";
String s = ljs + "love" + "csy" + 1314;
System.out.println(s);
}
编译器优化
1、一个Java程序如果想运行起来,需要经过两个时期,编译时和运行时。在编译时,Java 编译器(Compiler)将java文件转换成字节码。在运行时,Java虚拟机(JVM)运行编译时生成的字节码。通过这样两个时期,Java做到了所谓的一处编译,处处运行。
2、java设计者肯定不会这样设计,所以java中仅有的+,+=重载都不是真正的重载, 而是当编译器在遇到+重载的时候会创建一个StringBuilder对象,而后面的拼接会调用StringBuilder的append的方法。这个优化进行在编译器编译.java到bytecode时。
3、我们对上面的javac Demo02.java编译出class文件,然后再javap -c Demo02反编译,其中,ldc,astore等为java字节码的指令,类似汇编指令。后面的注释使用了Java相关的内容进行了说明。可以看到new StringBuilder,并且拼接使用了append方法。
E:\CodeSpace\JavaProject\think_java\out\production\think_java\com\ljs\string>javap -c Demo02
警告: 二进制文件Demo02包含com.ljs.string.Demo02
Compiled from "Demo02.java"
public class com.ljs.string.Demo02 {
public com.ljs.string.Demo02();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String ljs
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String lovecsy
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: sipush 1314
22: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
25: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
28: astore_2
29: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
32: aload_2
33: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
只靠编译器优化就够了吗
1、既然编译器为我们做了优化,是不是仅仅依靠编译器的优化就够了呢,当然不是。 下面我们看一段未优化性能较低的代码
public static String implicit(String[] values) {
String result = "";
for (int i = 0; i < values.length; i++) {
result += values[i];
}
return result;
}
2、反编译一下,其中从第8行到第35行是循环体,8: if_icmpge 38的意思是如果JVM操作数栈的整数对比大于等于(i < values.length的相反结果)成立,则跳到第38行(System.out)。35: goto 5则表示直接跳到第5行。可以看到这里11行的new在循环体里,也就是说每一次循环都会创建一个新的StringBuilder对象。这样也是不好的。
public static java.lang.String implicit(java.lang.String[]);
Code:
0: ldc #2 // String
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: aload_0
7: arraylength
8: if_icmpge 38
11: new #3 // class java/lang/StringBuilder
14: dup
15: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
18: aload_1
19: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: aload_0
23: iload_2
24: aaload
25: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore_1
32: iinc 2, 1
35: goto 5
38: aload_1
39: areturn
3、这就得自己优化一下了,记住String才有+=重载,StringBuilder只能调用append方法。现在new是在循环体之外所以不会创建多个StringBuilder对象。这才是真正的代码啊。
public static String explicit(String[] values) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < values.length; i++) {
result.append(values[i]);
}
return result.toString();
}
public static java.lang.String explicit(java.lang.String[]);
Code:
0: new #3 // class java/lang/StringBuilder
3: dup
4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
7: astore_1
8: iconst_0
9: istore_2
10: iload_2
11: aload_0
12: arraylength
13: if_icmpge 30
16: aload_1
17: aload_0
18: iload_2
19: aaload
20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: pop
24: iinc 2, 1
27: goto 10
30: aload_1
31: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: areturn
4、使用StringBuilder的好处,循环代码更短,只生成一个StringBuilder对象,并且使用StringBuilder还可以指定大小,当我传入1时,调试发现当append b的时候需要扩容,然后是调用str.getChars(0, len, value, count)把字符串“b”复制到目标上(0,len是"b"的开始到结尾,value是目标数组,count是从目标数组的什么位置开始复制)。而getchars只是判断这些参数是否合理,最后还是调用System的arraycopy方法,arraycopy是一个native方法。native方法只有签名没有实现。
无意识的递归
1、当String对象后面一个"+",然后再跟着一个非String对象,这时候编译器就会把调用这个非String对象的toString方法把该非String对象自动类型转换为String,如果这发生在自定义的类的重写的toString()方法体内,就有可能发生无限递归,运行时抛出java.lang.StackOverflowError栈溢出异常。
public static void main(String[] args) {
ArrayList<Demo04> al = new ArrayList<Demo04>();
for (int i = 0; i < 10; i++) {
al.add(new Demo04());
System.out.println(al);
}
}
@Override
public String toString() {
return "Demo04 address" + this + "\n";
}
2、这时候就不能调用this了,而是得调用super.toString。
toString
1、每个非基本类型对象有一个toString方法,当编译器需要String而你却只有一个对象时,该方法就会被调用。
"source" + source;
小结
1、我们应该避免在循环中隐式或显式的创建StringBuilder对象。特别是当你需要重写toString()方法时,最好自己创建一个StringBuilder。
2、如果不确定哪种方式更好,要多使用javap -c反编译
3、StringBuilder常用的方法,append,toString
4、StringBuilder是java5引进的,之前是StringBuffer,它是线程安全的,比较慢。
5、不要在toString方法里调用this,有可能会造成递归。
网友评论