另类分析String,StringBuffer和StringBuilder

开始

String,StringBuffer 和 StringBuilder 直间的区别这个问题已经是一个老问题了,尤其是在面试的时候会被经常提及的问题。首先 String 类型并不是 Java 语言的基本类型,它是 Java 中非常重要的一个引用类型(对象)。String 字符串一旦被创建出来就不会被修改,当你想修改 StringBuffer 或者是 StringBuilder,出于效率的考量,虽然 String 可以通过 + 来创建多个对象达到字符串拼接的效果,但是这种拼接的效率相比 StringBuffer 和 StringBuilder,效率上就差了点。

String

上面说到,String 字符串一旦被创建出来就不会被修改,其实很好理解。我们查看 String 类的源码即可得知(代码片段):

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
	......
	......
	......

上面代码其中 String 类已经被 final 修饰,用于存放字符的数组也被声明为 final,因此只能赋值一次,不可再更改。使用 final 修饰的类不能被继承、方法不能被重写、属性不能被修改。阅读了源码之后就能知道,String 类它其中的方法也是由 final 修饰的,所以,String 类就是一个典型的 Immutable 类。也由于 String 的不可变性,类似字符串拼接、字符串截取等操作都会产生新的 String 对象。所以我们在研究 String 的时候就会引入一个概念叫做“常量池”,如果再深入的话就会碰到堆与栈的问题了。研究那么深其实并没有太大的实际意义,毕竟我们都是以实用为主,所以我们只做基本性质的讨论。

请看以下示例:

package com.wlee.test;

public class StringTest {

    public static void main(String[] args) {

        String str1 = "aaa";
        String str2 = "bbb" + "ccc";
        String str3 = str1 + "bbb";
        String str4 = new String("aaa");

    }
}

这里定义了4个 String 类型的变量,我们逐个的分析一下,这里我们可以用 JDK 自带的反编译工具对生成的 class 文件进行反汇编代码即可。

javap 是 JDK 自带的反汇编工具。它的作用就是根据 class 字节码文件,反汇编出当前类对应的 code 区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
javap -c 就是对代码进行反汇编操作。
命令格式:javap -c xxxx.class

下面就是我反编译之后的信息:

D:\workspace_idea\string_demo\target\classes\com\wlee\test>javap -c StringTest.class

Compiled from "StringTest.java"
public class com.wlee.test.StringTest {
  public com.wlee.test.StringTest();
    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 aaa
       2: astore_1
       3: ldc           #3                  // String bbbccc
       5: astore_2
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_1
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      17: ldc           #7                  // String bbb
      19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      25: astore_3
      26: new           #9                  // class java/lang/String
      29: dup
      30: ldc           #2                  // String aaa
      32: invokespecial #10                 // Method java/lang/String."<init>":(Ljava/lang/String;)V
      35: astore        4
      37: return
}

解释一下(默认情况下,常量池为空时):

String str1 = “aaa”,str1 创建了几个对象?字符串在创建对象时,会在常量池中看有没有 aaa 这个字符串。如果没有此时还会在常量池中创建一个,如果有则不创建。默认情况下所以会创建一个对象。

String str2 = “bbb” + “ccc”,那么这个 str2 会创建几个对象?其实我们可以通过上述的反编译信息可以得知,str2 会直接创建一个 bbbccc 的对象。

String str3 = str1 + “bbb”,这个 str3 又是如何工作的?再看一下反编译信息:

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String aaa
       2: astore_1
       3: ldc           #3                  // String bbbccc
       5: astore_2
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_1
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      17: ldc           #7                  // String bbb
      19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
}

很明显,str3 先创建了一个 StringBuilder 对象并执行了初始化,然后在 str3 中执行 + 号其实是执行了 StringBuilder 的 append() 操作,最后还调用了一个 StringBuilder 的 toString 方法。说白了,String str3 = str1 + “bbb” 就等价于 String str3 = new StringBuilder().append(str1).append(“bbb”).toString(),所以 str3 执行结束后,相当于创建了3个对象。

String str4 = new String(“aaa”),最后就是 str4 了,这个就很好理解了,在创建这个对象时因为使用了 new 关键字,所以肯定会在堆中创建一个对象。然后会在常量池中看有没有 aaa 这个字符串,如果没有此时还会在常量池中创建一个,如果有则不创建。所以可能是创建一个或者两个对象,但是一定存在两个对象。

String 就讲到这里吧,看完上面的分析发现 String 对象竟然和 StringBuilder 有关联,String 对象底层是使用了 StringBuilder 对象的 append 方法进行字符串进行拼接的。我们就不再深入讨论“堆与栈”的问题了,因为它已经是老生常谈的问题了。

StringBuffer

StringBuffer 代表一个可变的字符串序列,当一个 StringBuffer 被创建以后,通过 StringBuffer 的一系列方法可以实现字符串的拼接、截取等操作。一旦通过 StringBuffer 生成了最终想要的字符串后,就可以调用其 toString 方法来生成一个新的字符串。请看如下代码:

package com.wlee.test;

public class StringTest {

    public static void main(String[] args) {
        StringBuffer strBuffer = new StringBuffer("123");
        strBuffer.append("abc");
        System.out.println(strBuffer);
    }
}

我们将这个类的 class 文件反编译一下看到如下信息:

D:\workspace_idea\string_demo\target\classes\com\wlee\test>javap -c StringTest.class

Compiled from "StringTest.java"
public class com.wlee.test.StringTest {
  public com.wlee.test.StringTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/StringBuffer
       3: dup
       4: ldc           #3                  // String 123
       6: invokespecial #4                  // Method java/lang/StringBuffer."<init>":(Ljava/lang/String;)V
       9: astore_1
      10: aload_1
      11: ldc           #5                  // String abc
      13: invokevirtual #6                  // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
      16: pop
      17: return
}

从上述反编译信息可以看出 StringBuffer 在 append 的时候,并不会自动调用 toString 方法,这和之前 String 对象 在用 + 拼接的时候会自动调用 toString 方法和截然不同。其实很好理解,这里是 StringBuffer 对象,如果你想从 StringBuffer 对象获取 String 对象,你必须手动的进行调用 toString 方法。

我们也可以通过源码看到 StringBuffer 是线程安全的,它在字符串拼接上面直接使用 synchronized 关键字加锁,从而保证了线程安全性。

StringBuilder

StringBuilder 是在 Java5 中被提出,它其实是和 StringBuffer 几乎一样,它和 StringBuffer 之间的最大不同在于 StringBuilder 是非线程安全的(不能同步访问),所以在常规情况下 StringBuilder 相较于 StringBuffer 有速度优势,并且为什么 + 号操作符使用 StringBuilder 作为拼接条件而不是使用 StringBuffer 呢?我觉得就是这个所谓加锁耗时会影响性能,所以 String 底层使用 StringBuilder 作为字符串拼接,所以多数情况下建议使用 StringBuilder 类。

我们再用一个段代码来说明 StringBuilder 的效率:

package com.wlee.test;

public class StringTest {

    public static void main(String[] args) {
        String s = "123";
        for (int i = 0; i < 100000; i++) {
            s += "ab";
        }
    }
}

这段代码根据我们之前分析的,String 在使用 + 拼接时,JVM 会隐式创建 StringBuilder 对象,在大部分情况下不会造成效率的损失,不过在进行大量循环拼接字符串时就不不行了,按照刚才反编译的信息看,上述代码其实就是成了:

String s = "123";
for(int i = 0; i < 10000; i++) {
     s = (new StringBuilder()).append(s).append("ab").toString();    
}

在每次进行循环时,都会创建一个 StringBuilder 对象,每次都会把一个新的字符串元素 ab 拼接到 123 的后面,就成了如下结果:

new StringBuilder()		String s = 123ab;
new StringBuilder()		String s = 123abab;
new StringBuilder()		String s = 123ababab;
new StringBuilder()		String s = 123abababab;
new StringBuilder()		String s = 123ababababab;
new StringBuilder()		String s = 123abababababab;
new StringBuilder()		String s = 123ababababababab;
......
......
......

每次都会创建一个 StringBuilder ,并把引用赋给 StringBuilder 对象,这样在创建完毕后,内存中就会多了很多 StringBuilder 的无用对象。这样由于大量 StringBuilder 创建在内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个 StringBuilder 对象调用 append() 方法手动拼接。如下代码:

package com.wlee.test;

public class StringTest {

    public static void main(String[] args) {
        StringBuilder strBuilder = new StringBuilder("123");
        for (int i = 0; i < 10000; i++) {
            strBuilder.append("ab");
        }
        strBuilder.toString();
    }
}

这段代码中,只会创建一个 StringBuilder 对象,每次循环都会使用这个 StringBuilder 对象进行拼接,因此提高了拼接效率。

结语

以上对 String,StringBuffer 和 StringBuilder 的分析,并没有像其他网友那样对它们进行用法以及功能上的比对,只是相互之间进行了分析而已,正常没有特别要求的情况下,StringBuffer 和 StringBuilder 用哪个都可以,StringBuffer 与 StringBuilder 最大的区别就是 StringBuffer 可以在多线程场景下使用,StringBuffer 内部有大部分方法都加了 synchronized 锁。在单线程场景下效率比较低,因为有锁的开销。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WorkLee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值