美文网首页Java捡漏
编程思想:字符串1

编程思想:字符串1

作者: shenyoujian | 来源:发表于2018-11-09 15:41 被阅读14次

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

    相关文章

      网友评论

        本文标题:编程思想:字符串1

        本文链接:https://www.haomeiwen.com/subject/dganxqtx.html