Java捡漏

编程思想:字符串1

2018-11-09  本文已影响14人  shenyoujian

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,有可能会造成递归。

上一篇 下一篇

猜你喜欢

热点阅读