你真的了解String和StringBuilder吗

前言

面试的时候可能常常会问这样一个问题:请说一下String、StringBuilder、StringBuffer的区别,可能不少人会说String若是经过+(这是加好,后面同理)来拼接字符串时,会建立不少临时变量,性能比较低(网上不少帖子也是这么写的),可是,真的是这样的吗?java

字符串的拼接

那么String经过+来拼接字符串时,到底有没有建立临时变量呢?其实,这个问题很简单,只须要经过javap反编译生成的class文件,看看class文件中String所作的操做就能够了。下面咱们就以《java编程思想》中字符串章节的例子来说解。面试

首先咱们来看下面这段代码:编程

public class Test {
    public static void main(String[] args){
        String mango = "mango";
        String s = "abc" + mango + "def" + 47;
        System.out.println(s);
    }
}
复制代码

这段代码是比较典型的经过+来拼接字符串的代码,接下来咱们经过javac Test.java来编译这段代码,而后经过javap -c Test.class反编译生成的Test.class文件。剔除掉一些无关的部分,主要展现了main()中代码的字节码,因而有了如下的字节码。你会发现很是有意思的东西发生了。segmentfault

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         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
}
复制代码

这里涉及到汇编语言,读者能够网上搜索字节码指令表会有不少,我这里提供一个,读者能够对着表来理解每个指令的含义,这里我不详细展开。每一个指令的后面可能会有////后面的内容表示指令码操做的对象。细心的读者必定会发现:编译器自动引入了java.lang.StringBuilder(其中java前面的L表示引用类型,想要详细了解的读者能够看一下《深刻理解Java虚拟机》中类文件结构那一章)。虽然咱们在源代码中没有使用StringBuilder,可是编译器却自做主张的使用了它,由于它更高效。数组

看上面的字节码你会发现,编译器建立StringBuilder对象以后,对+号相连的每个字符串使用append()方法来拼接,总计调用了四次,最后调用toString()方法生成结果。(注:读者感兴趣的话能够用StringBuilder来替换上面的代码,经过javac javap从新编译,而后你会发现main()方法中生成的字节码是同样的)。bash

结论

经过上面的例子咱们发现,当咱们经过+来拼接字符串时,编译器会自动替咱们优化成StringBuilder来拼接,并不会形成网上所说的建立临时变量,速度变慢这些缺点。(注:因为StringBuidler是在jdk5.0以后引入的,因此jdk5.0以前是经过StringBuffer来拼接的,感兴趣的读者能够自行验证)。app

延伸

如今,咱们确定会很开心,既然编译器都替咱们优化了,那咱们是否是能够随意使用String了呢(想一想都开心)。哈哈,不要高兴得太早,由于有时候编译器的优化可能并非你想要的结果。让咱们来看下面这段代码:jvm

下面这段程序采用两种方式生成一个String:方法一使用多个String对象;方法二代码中使用了StringBuidler。性能

public class Test {
    public String testString(String[] fields) {
        String result = "";
        for (int i = 0; i < fields.length; i++) {
            result += fields[i];
        }
        return result;
    }

    public String testStringBuilder(String[] fields){
        StringBuilder result = new StringBuilder();
        for (int i = 0; i<fields.length; i++){
            result.append(fields[i]);
        }
        return result.toString();
    }
}
复制代码

上面代码中的两个方法执行相似,都是传入字符串数组,而后经过for循环将数组字符串拼接起来,区别是第一个方法使用String来拼接,第二个方法使用StringBuilder来拼接。而后咱们仍是经过javap来反编译这段代码,剔除无关部分,会看到两个方法的字节码。优化

首先是testString()方法的字节码:

public java.lang.String testString(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=2
         0: ldc           #2 // String
         2: astore_2
         3: iconst_0
         4: istore_3
         5: iload_3
         6: aload_1
         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_2
        19: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: aload_1
        23: iload_3
        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_2
        32: iinc          3, 1
        35: goto          5
        38: aload_2
        39: areturn
复制代码

这里读者看一下第8行中的if_icmpge,对照字节码指令表会发现这个指令就是for循环中比较i值等于某个值时进入循环,后面的38表示在38行跳出循环,这里的循环体是第8行到第35行。第35行的意思是:返回循环体的起始点(第5行)。而后咱们看循环体(第8行到第35行)中第11行,是一个new指令,这个太熟悉了,就是建立对象。可是它竟然是在循环体内部,这就意味着每循环一次,就要建立一个新的StringBuilder对象。这显然不能接受。

那咱们再看一下testStringBuilder()的字节码:

public java.lang.String testStringBuilder(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=2
         0: new           #3 // class java/lang/StringBuilder
         3: dup
         4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
         7: astore_2
         8: iconst_0
         9: istore_3
        10: iload_3
        11: aload_1
        12: arraylength
        13: if_icmpge     30
        16: aload_2
        17: aload_1
        18: iload_3
        19: aaload
        20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        23: pop
        24: iinc          3, 1
        27: goto          10
        30: aload_2
        31: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: areturn
复制代码

咱们能够看到,不只循环体部分的代码更简短、更简单了,并且new只在刚开始调用了一次,说明只生成了一个StringBuilder对象。

延伸结论

因此,当你为一个类编写toString()方法时,若是字符串操做比较简单,那就能够信赖编译器,它会为你合理的构造最终的字符串结果。可是,若是你要在toString()方法中使用循环,那么你就须要本身建立一个StringBuilder对象。固然,当你拿不定主意的时候,那么你随时能够经过javap来分析你的程序。

留一个问题:枚举你们应该都知道,可是你知道它在jvm中究竟是怎么执行的吗?(思路:其实要想知道它的原理很简单,你一样能够写一段枚举代码,而后经过javap反编译这段代码,你会有一种豁然开朗的感受。)

参考文献

-《java编程思想》

相关文章
相关标签/搜索