String、StringBuffer、StringBuiler看这一篇就够了

1.概述

string、stringbuffer、stringbuiler一直是java面试中较为热点的问题,一般作为一个开场问题。那么这三者之间有什么区别,如何根据场景来选择使用,本文将基于三者的源码、性能、以及面试常问点来进行分析。

2.String、StringBuffer、StringBuiler底层源码

2.1 String类
首先看一下String类的类图,它实现了CharSequence接口,也就是说String是CharSequence类型,它的底层是被final修饰的字符数组,意味着一旦String被定义之后就不能够改变。在日常我们做字符串拼接时,其实是一个创建新对象和回收旧对象的过程。以下面代码为例:

String str = "a";
	str += "b";

当定义字符串str时并将值“a”赋给它,当执行第二段代码时,即实现str拼接“b”时,此时JAVA虚拟机创建了一个新对象str,将拼接成的字符串“ab”赋给新str,GC同时会回收旧的str对象,所以str并没有被更改,而是直接生成的新对象。若大量字符串拼接,利用上述+=操作将严重影响性能。
在这里插入图片描述

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    //构造函数
     public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    //构造函数
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
    //返回一个新的char[]
    public char[] toCharArray() {
        // Cannot use Arrays.copyOf because of class initialization order issues
        char result[] = new char[value.length];
        System.arraycopy(value, 0, result, 0, value.length);
        return result;
    }
 }

在上述String类的源码声明中,String类由final对象修饰,被final修饰的类不能被继承、方法不能被重写、属性不能被修改。String是一个典型的Immutable(不可变)类,所谓的不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值。与不可变类相对的则是可变类(mutable),可变类创建实例后可以改变其成员变量值,开发中创建的大部分类都属于可变类。跟据上述String源码,分析String保证不可变的原因主要如下:

String类被final修饰,不可被继承; string内部所有成员都设置为私有变量,外部无法访问;
value被final修饰,所以变量的引用不可变,且不提供对外访问value的接口;
在获取value时,不是直接返回value的引用,而是直接通过System.arraycopy()方法来返回一个新的char数组;
char[]为引用类型仍可以通过引用修改实例对象,为此String(char value[])构造函数内部使用的copyOf而不是直接将value[]复制给内部变量。

接下来讲一下这样设计的优势:

1.支持hash映射和缓存,提升效率: 由于字符串不可变性,在其创建时就会缓存其hashCode值,不需要每次重新计算其hash值。这也是HashMap中键值一般会使用字符串的原因,对于字符串的处理速度要快于其它的键对象。

2.保证了线程安全: 由于其不变性,同一个字符串实例可以被多个线程共享。如果是可变对象,很容易被修改而造成不可预估的后果。

3.不可变对象提升了字符串常量池(String Pool)的效率和安全性: 字符串常量池是java堆内存中一个特殊的存储区域,当创建一个String对象,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。在java中内存分为堆内存和栈内存,堆内存存放的是对象,栈内存存储对象的引用,对象是不可变动的,因此拷贝对象时只需要复制它的地址。复制地址(通常一个指针的大小)需要很小的内存,效率也很好。二对于其他引用同一个对象的其他变量也不会造成影响。

2.2 StringBuffer类
在这里插入图片描述
由上述类图可知,StringBuffer类继承自类AbstractStringBuilder,使用 value 和 count 分别表示存储的字符数组和字符串使用的计数,AbstractStringBuilder类封装了大量基础方法,包括数组扩容机制、拼接方法等。首先看一下数组初始容量:

/**
 * Constructs a string buffer with no characters in it and an
 * initial capacity of 16 characters.
 */
public StringBuffer() {
    super(16);
}
public StringBuffer(int capacity) {
    super(capacity);
}

StringBuffer初始化时默认长度大小为16,StringBuffer是线程安全的,可以查看其append方法(如下所示),由synchronized修饰,因此是线程安全的。

	
 @Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

    public synchronized StringBuffer append(StringBuffer sb) {
        toStringCache = null;
        super.append(sb);
        return this;
    }

    @Override
    synchronized StringBuffer append(AbstractStringBuilder asb) {
        toStringCache = null;
        super.append(asb);
        return this;
    }

    @Override
    public synchronized StringBuffer append(CharSequence s) {
        toStringCache = null;
        super.append(s);
        return this;
    }

接下来看一下其扩容部分的源码,具体如下:

private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }
    
    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

StringBuffer扩容方法继承了类AbstractStringBuilder,上述ensureCapacityInternal方法就是扩容方法,minimumCapacity 就是字符长度 + 要拼接的字符串长度,如果拼接后的字符串要比当前字符长度大的话,会进行数据的复制,真正扩容的方法是在 newCapacity 中,扩容后的字符串长度会是原字符串长度增加一倍 + 2,如果扩容后的长度还比拼接后的字符串长度小的话,那就直接扩容到它需要的长度 newCapacity = minCapacity,然后再进行数组的拷贝。因此,如果我们拼接的字符串长度是可以预计的,那么最好指定合适的capacity,避免多次扩容的开销。

2.3 StringBuiler类
在这里插入图片描述
由上述StringBuilder类图可知,StringBuilder与StringBuffer类似,均继承于AbstractStringBuilder,所以其初始空间和扩容机制均相同。区别在于它是被final修饰的,因此是不可被继承的;而且其append方法不是由synchronized修饰,因此不是线程安全的。同时它实现了Serializable 序列化接口,表示对象可以被序列化;还实现了CharSequence 字符序列接口,提供了几个对字符序列进行只读访问的方法,例如 length()、charAt()、subSequence()、toString() 方法等。

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {
    
	public StringBuilder() {
        super(16);
    }
	public StringBuilder(int capacity) {
        super(capacity);
    }
}
abstract class AbstractStringBuilder implements Appendable, CharSequence {
   
    char[] value;

    int count;

    AbstractStringBuilder() {
    }
    
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
 }

3.String、StringBuffer、StringBuilder性能对比

3.1 测试代码如下:

public class Test {

    private static final Integer time = 100000;

    public static void main(String[] args) {
        testString();
        testStringBuffer();
        testStringBuilder();
    }

    public static void testString() {
        String str = "";
        long start = System.currentTimeMillis();
        for (int i = 0;i < time; i++) {
            str += "a";
        }
        long end = System.currentTimeMillis();
        System.out.println("string:"+(end - start));
    }

    public static void testStringBuffer() {
        StringBuffer stringBuffer = new StringBuffer();
        long start = System.currentTimeMillis();
        for (int i = 0;i < time; i++) {
            stringBuffer.append("a");
        }
        long end = System.currentTimeMillis();
        System.out.println("stringBuffer:"+(end - start));
    }

    public static void testStringBuilder() {
        StringBuilder stringBuilder = new StringBuilder();
        long start = System.currentTimeMillis();
        for (int i = 0;i < time; i++) {
            stringBuilder.append("a");
        }
        long end = System.currentTimeMillis();
        System.out.println("stringBuilder:"+(end - start));
    }
}

3.2 测试结果如下:
上述代码分别测试了在拼接次数为1000次、10000次、100000次和1000000次下,String、StringBuffer、StringBuilder的耗时,具体结果如下表所示:

单位:msStringStringBufferStringBuilder
次数:100030.10.1
次数:1000072.30.70.7
次数:1000003534.24.43.8
次数:1000000130400.413.211.2

结论:由上表可知,String在进行字符串拼接时,随着拼接次数的增多,耗时也呈指数式增加。相比较而言,StringBuffer和StringBuilder性能相对接近。
注意:上述测试环境基于win10操作系统,jdk 1.8,处理器:Intel® Core™ i7-9700 CPU @ 3.00GHz 3.00 GHz,RAM:16.0 GB。

4.深入理解String案例

1.判断下面程序输出结果

 		String str1 = "helloworld";
        String str2 = "hello" + "world";
        System.out.println(str1 == str2); //true

通过反汇编上述代码,得到结果如下:

public class com.eckey.lab.test.StringTest {
  public com.eckey.lab.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 helloworld(str1)
       2: astore_1
       3: ldc           #2                  // String helloworld(str2)
       5: astore_2
       6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: aload_1
      10: aload_2
      11: if_acmpne     18
      14: iconst_1
      15: goto          19
      18: iconst_0
      19: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V
      22: return
}

通过反汇编代码可以看出,编译器对 String str2 = “hello” + “world"做了优化,str2被直接创建成了"helloworld”,而str1在创建时字符串helloworld被写入了常量池。str2在创建时会先从常量池中获取对应字符串,而常量池已经包含了该字符,因此结果为true。

2.判断下面程序输出结果

 		String str1 = "helloworld";
        String str2 = "hello";
        String str3 = str2 + "world";
        System.out.println(str1 == str3); //false

反汇编上述代码得到结果:

public class com.eckey.lab.test.StringTest {
  public com.eckey.lab.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 helloworld
       2: astore_1
       3: ldc           #3                  // String hello
       5: astore_2
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_2
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      17: ldc           #7                  // String world
      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: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      29: aload_1
      30: aload_3
      31: if_acmpne     38
      34: iconst_1
      35: goto          39
      38: iconst_0
      39: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
      42: return
}

通过上述反汇编代码可知,当执行String str3 = str2 + “world”;操作时,首先生成了一个StringBuilder,其次将其进行了初始化操作,然后调用了append方法,拼接了字符串“world”,最后调用了toString方法,将拼接后的字符串转成了一个新的String,这个过程生成了新的对象,因此str1和str3肯定不是一个对象,结果自然为false。

3.判断下面程序输出结果

 		String str1 = "helloworld";
        final String str2 = "hello";
        String str3 = str2 + "world";
        System.out.println(str1 == str3); //true

这个结果为true是不是感觉很意外?那我们直接上反汇编代码,一探究竟便知:

public class com.eckey.lab.test.StringTest {
  public com.eckey.lab.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 helloworld
       2: astore_1
       3: ldc           #3                  // String hello
       5: astore_2
       6: ldc           #2                  // String helloworld
       8: astore_3
       9: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: aload_1
      13: aload_3
      14: if_acmpne     21
      17: iconst_1
      18: goto          22
      21: iconst_0
      22: invokevirtual #5                  // Method java/io/PrintStream.println:(Z)V
      25: return
}

这个案例与上述2中案例唯一的区别在于变量str2声明前加了一个关键字final,导致在编译时,编译器直接将str3创建成了"helloworld",是不是有很多问号?由于被final修饰,在编译阶段会存入调用类的常量池中,因此str2一开始就存在于常量池中,String str3 = str2 + "world"实际就变成了String str3 = “hello” + “world”,因为String str1 = “helloworld”;这条语句已经将字符串"helloworld"写入常量池,str3首先会获取常量池中的值,所以结果为true。

4.判断下面程序输出结果

public static void main(String[] args) {
        String str1 = "helloworld";
        final String str2 = returnStr();
        String str3 = str2 + "world";
        System.out.println(str1 == str3); //false
    }
    
    public static String returnStr() {
        return "hello";
    }

反编译代码如下:

public class com.eckey.lab.test.StringTest {
  public com.eckey.lab.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 helloworld
       2: astore_1
       3: invokestatic  #3                  // Method returnStr:()Ljava/lang/String;
       6: astore_2
       7: new           #4                  // class java/lang/StringBuilder
      10: dup
      11: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      14: aload_2
      15: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      18: ldc           #7                  // String world
      20: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      23: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      26: astore_3
      27: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      30: aload_1
      31: aload_3
      32: if_acmpne     39
      35: iconst_1
      36: goto          40
      39: iconst_0
      40: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
      43: return

  public static java.lang.String returnStr();
    Code:
       0: ldc           #11                 // String hello
       2: areturn
}

通过上述反汇编代码可知,当执行String str3 = str2 + “world”;操作时,首先生成了一个StringBuilder,其次将其进行了初始化操作,然后调用了append方法,拼接了字符串“world”,最后调用了toString方法,将拼接后的字符串转成了一个新的String,因此结果为false。

5.判断下面程序输出结果

  		String str1 = "hello";
        String str2 = new String("hello");
        System.out.println(str1 == str2); //false

在创建这个str2对象时因为使用了 new 关键字,所以肯定会在堆中创建一个对象。然后会在常量池中看有没有 hello这个字符串;如果没有此时还会在常量池中创建一个;如果有则不创建。所以可能是创建一个或者两个对象,但是一定存在两个对象,因此结果为false。

6.判断下面程序输出结果

  		String str1 = "hello";
        String str2 = new String("hello");
        System.out.println(str1 == str2.intern()); //true

String.intern()是一个Native方法,它的作用是:如果字符常量池中已经包含一个等于此String对象的字符串,则返回常量池中字符串的引用,否则,将新的字符串放入常量池,并返回新字符串的引用。在上述代码中,在创建str2对象时使用了new,因此肯定会在堆中创建一个对象。由于str1在创建时将字符串hello写入了常量池,因此str2会返回其引用,因此当str2调用intern()方法时,与str1指向的是常量池中同一个地址,则结果为true。

5.小结

1.String适用于字符串拼接较少、字符串变化较小的场合。
2.StringBuffer适用于字符串连接操作比较频繁,且要求线程安全(多线程环境下操作)。
3.StringBuilder适用于字符串连接操作比较频繁,且是单线程的情况。

6.参考文章

1.https://juejin.im/entry/59082ab5a0bb9f006510683a
2.https://juejin.im/entry/580467cfda2f60004fea4673
3.《深入理解JAVA虚拟机》

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值