你真的了解String和StringBuilder吗

前言

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

字符串的拼接

其实要想知道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()中代码的字节码,于是有了以下的字节码。你会发现非常有意思的东西发生了。

  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()方法中生成的字节码是一样的)。

结论

通过上面的例子我们发现,当我们通过+来拼接字符串时,编译器会自动替我们优化成StringBuilder来拼接,并不会造成网上所说的创建临时变量,速度变慢这些缺点。(注:由于StringBuidler是在jdk5.0之后引入的,所以jdk5.0之前是通过StringBuffer来拼接的,感兴趣的读者话可以自行验证)。

延伸

现在,我们肯定会很开心,既然编译器都替我们优化了,那我们是不是可以随意使用String了呢(想想都开心)。哈哈,不要高兴得太早,因为有时候编译器的优化可能并不是你想要的结果。让我们来看下面这段代码:

下面这段程序采用两种方式生成一个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编程思想》

猜你喜欢

转载自juejin.im/post/5d161dc06fb9a07ec63b28be