聊聊String、StringBuilder以及StringBuffer

特征

String:字符串常量;
StringBuilder:字符串变量(非线程安全);
StringBuffer:字符串变量(线程安全);

聊聊String、StringBuilder以及StringBuffer_第1张图片
UML图

可以看出,三者都实现了CharSequence接口,并且StringBuilder和StringBuffer都继承了AbstractStringBuilder类。通过阅读源码还能得知,三者内部都是通过一个char[ ]数组来实现功能的。

异同点:

  • 都被final声明了,不可被继承;
  • String不可变,每次使用都新建一个对象。而StringBuffer和StringBuilder是可变的;
  • 在字符串不经常变化的背景下使用String,在对频繁对字符串进行运算的背景(如拼接、修改、删除等)下使用StringBuffer或StringBuilder。
聊聊String、StringBuilder以及StringBuffer_第2张图片
StringBuffer内部通过对所有的公共方法加锁来实现线程安全

String

对于String来说,有两种创建对象的方式:

  • 使用字面量形式,例如 String a = "EakonZhao";
  • 使用构造函数的方式,例如String b = new String("EakonZhao");

关于创建String对象的一些知识,例如字面量常量池(也称字符串常量池)等在我这篇博客有提到。
聊聊Java中的 " == "、equals以及hashCode

让我们来看看下面这个程序:

聊聊String、StringBuilder以及StringBuffer_第3张图片
输出结果为EakonZhao

我们马上可以说出打印的结果:EakonZhao。我们也很容易误认为我们是将字符串"Eakon"与“Zhao”进行拼接然后得到“EakonZhao”,并且此时"EakonZhao"将原有的“Eakon”替换掉了。

事实真的如我们想象的那样吗?
如果真的是这样理解,那我们就错了。别忘了String是不可变的,创建之后就不能修改了。
其实看完下面这个示意图就很容易理解了:

聊聊String、StringBuilder以及StringBuffer_第4张图片
内存分配示意图

其实不管是原有的“Eakon”还有“Zhao”或者是后来生成的"EakonZhao",其实都是存放在不同的内存空间的。
也就是说,name最初引用的是值为“Eakon”的字符串对象,至于最终打印出的结果是“EakonZhao”,并不是说对name引用的对象的内容进行了替换,而是将name的引用指向了一个新的对象---值为"EakonZhao"的字符串对象。

那么在使用 “ + ”对字符串进行连接的时候,底层发生了什么事情呢?
下面我将借助jad反编译工具来查看字节码的内容:

聊聊String、StringBuilder以及StringBuffer_第5张图片
使用jad工具反编译之后查看字节码

第32行,将字符串“Eakon”压入栈中
第35行,创建了一个StringBuilder对象
第39行,由于使用了+,所以调用了StringBuilder对象的append方法,参数是后面的"Zhao"
第40行,将字符串“Zhao”压入栈中
第41行,使用append方法连接“Eakon”和"Zhao"
第42行,调用toString方法

所以我们可以知道,当我们使用 “ + ” 对字符串进行操作时,其实底层会转换成调用StringBuilder的append方法来实现。

其实如果用加号对String对象进行连接的话,效率是十分低下的,并且每次都要创建一个新的对象,也会占用大量的系统资源。

下面我将对String、StringBuffer以及StringBuilder的性能进行探究:

分别使用String、StringBuffer、StringBuilder进行150000次的字符串拼接操作,然后获得执行时间,取六次执行时间求得平均值来比较性能。

public class Eakon{
    private int LOOP_TIMES = 150000;
    private final String TEST_STRING = "EakonZhao";
    
    public void testString(){
        String eakon = "";
        long beginTime = System.currentTimeMillis();
        for(int i = 0; i < LOOP_TIMES; i++){
            eakon += TEST_STRING;
        }
        long endTime = System.currentTimeMillis();
        System.out.print(endTime-beginTime+" ");
    }
    
    public void testStringBuffer(){
        StringBuffer eakon = new StringBuffer();
        long beginTime = System.currentTimeMillis();
        for(int i = 0 ; i < LOOP_TIMES; i++){
            eakon.append(TEST_STRING);
        }
        eakon.toString();
        long endTime = System.currentTimeMillis();
        System.out.print(endTime-beginTime+" ");
    }
    
    public void testStringBuilder(){
        StringBuilder eakon = new StringBuilder();
        long beginTime = System.currentTimeMillis();
        for(int i = 0; i < LOOP_TIMES; i++){
            eakon.append(TEST_STRING);
        }
        eakon.toString();
        long endTime = System.currentTimeMillis();
        System.out.print(endTime-beginTime+" ");
    }
    
    public void run(){
        for(int i = 0; i < 5; i++){
            System.out.println("第" +i+ "次:");
            testStringBuilder();
            testStringBuffer();
            testString();
            System.out.println("----------------------------------");
        }
    }
    
    public static void main(String[] args){
        new Eakon().run();
    }
}

测试结果:

聊聊String、StringBuilder以及StringBuffer_第6张图片
测试结果
聊聊String、StringBuilder以及StringBuffer_第7张图片
测试结果

我们可以看出,对String使用“+”操作耗时是最多,性能最差的。StringBuilder比StringBuffer性能略好一些,但我们要注意的是StringBuilder是非线程安全的,也就是StringBuilder牺牲了线程安全性来提升性能。

为什么对String使用“+”操作耗时要那么久呢?因为每使用一次“+”操作,都要新创建一个对象。然而创建的对象马上又会失去引用,从而被垃圾回收机制清理掉。下面我们来看看在使用“+”对字符串进行连接时内存的使用情况:

聊聊String、StringBuilder以及StringBuffer_第8张图片
内存使用情况

我们可以看到占用了极大部分的堆内存-----因为对象是存放在堆上的,如果一直不停地创建对象,将会堆内存使用量将会极大地增加。

以上就是我对String、StringBuilder以及StringBuffer简单的介绍,以后我会另开博客来深入探究它们的源码,以便帮助我们更加合理地使用它们。

你可能感兴趣的:(聊聊String、StringBuilder以及StringBuffer)