Java之String,StringBuffer和StringBuilder

String,StringBuffer和StringBuilder

        在java中有关这三个对象的比较一直是一个老生常谈的问题,大部分人了解的也就限于StringBuffer是线程安全的,而StringBuilder是非线程安全的。今天我们一起看下这三者之间的联系和区别。

        我们先从类图中看下这三个类之间的关系:


    可以看到String,StringBuffer和StringBuilder三者都实现了CharSequence接口,这个接口用于表示有序的字符集合。在java这三个对象都可以用来代表字符串,他们的底层也都是用的字符数组来实现。这是他们三者之间的联系。我们接下来看下它们的之间的区别:

1.对象可变性

    在很多地方都有说明String是不可变对象,而StringBuffer和StringBuilder是可变对象,怎么理解这个对象可不可变呢?其实我们可以自己写一个测试类来帮助我们理解这个概念:

package com.ljw.StringStringBuilderStringBuffer;

/**
 * Created by liujiawei on 2018/7/3.
 */
public class Test {
    public static void main(String[] args) {
        String a = "";
        StringBuffer stringBuffer = new StringBuffer();
        StringBuilder stringBuilder = new StringBuilder();

        System.out.print("改变String a的值之后和a进行比较的结果");
        System.out.print(a == a + "b");
        System.out.print("  ");
        System.out.println(a == a.concat("b"));


        System.out.print("改变StringBuffer的值之后和StringBuffer进行比较的结果");
        System.out.println(stringBuffer == stringBuffer.append("ab"));

        System.out.print("改变StringBuilder的值之后和StringBuilder进行比较的结果");
        System.out.println(stringBuilder == stringBuilder.append("ab"));


    }
}

        在这个测试类中,我们分别声明了一个String 对象,一个StringBuffer对象和一个StringBuilder对象,改变他们的字符串对象以后,再与原对象进行比较,看一下运行的结果:


      可以看到,String类型的对象,不管是通过+操作符拼接字符串还是使用concat()方法,只要对象的内容发生改变以后,它都变成了一个对象,StringBuffer和StringBuilder类型的两个对象在改变了内容以后,还是原来的对象。我们这个时候并不知道为什么String是不可变对象,StringBuffer和StringBuilder是可变的,但通过代码的测试,我们已经可以得出这个结论。下面我们分析以下他们的源码帮助我们理解这个可变性。

2.底层源码分析

String:
String提供了很多不同的构造方法,我们用最常见的来进行分析:
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}
private final char value[];
    可以看到,String里面用final定义了一个字符数组变量用来存储数据,并且没有提供对应的getter和setter方法来对这个对象进行修改,当我们进行字符串拼接时,再看下concat()方法(用+操作符拼接字符串其实也是调用的这个方法,调试可以发现都是进的这个):
public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}
    在这个方法里面,我们可以清楚的看到拼接字符串所做的操作,通过数组工具类创建一个新的长度的字符数组,在通过底层的复制,将原有的数组内容复制到新的数组当中:
void getChars(char dst[], int dstBegin) {
    System.arraycopy(value, 0, dst, dstBegin, value.length);
}
void getChars(char dst[], int dstBegin) {
    System.arraycopy(value, 0, dst, dstBegin, value.length);
}
    最后在调用自身的另一个构造方法:
String(char[] value, boolean share) {
    // assert share : "unshared not supported";
    this.value = value;
}
    返回一个新的String对象,这就是为什么String类型的变量在初始化完成以后就不可变的原因。

    返回一个新的String对象,这就是为什么String类型的变量在初始化完成以后就不可变的原因。


StringBuffer:
先看下StringBuffer的构造方法:
public StringBuffer() {
    super(16);
}
public StringBuffer(int capacity) {
    super(capacity);
}
public StringBuffer(String str) {
    super(str.length() + 16);
    append(str);
}
public StringBuffer(CharSequence seq) {
    this(seq.length() + 16);
    append(seq);
}
    四个构造函数,最终调用的都是父类中的方法:
AbstractStringBuilder(int capacity) {
    value = new char[capacity];
}
char[] value;
    后面两个构造方法支持直接将字符串作为参数,其实就是先调用第二个构造函数,再调用append方法进行填充,我们一起看下这个append() 方法:
public synchronized StringBuffer append(String str) {
    super.append(str);
    return this;
}
public AbstractStringBuilder append(String str) {
    if (str == null) str = "null";
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    if (srcBegin < 0) {
        throw new StringIndexOutOfBoundsException(srcBegin);
    }
    if (srcEnd > value.length) {
        throw new StringIndexOutOfBoundsException(srcEnd);
    }
    if (srcBegin > srcEnd) {
        throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
    }
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
    我们可以看到StringBuffer中的append()方法调用的是父类方法,父类中的操作是利用底层的数组复制,将新增的内容放入一个新的数组当中,它没有产生新的StringBuffer对象,只是操作了原有的字符数组,所以这个对象是可变的,而且我们还注意到StringBuffer中的append()方法是被synchronized修饰符给修饰的,这说明这是一个线程安全的方法,除了这个方法,StringBuffer中的其他方法也基本都使用了synchronized修饰符,这说明StringBuffer是一个线程安全的对象


StringBuilder:
    先看一下StringBuilder的构造方法:
public StringBuilder() {
    super(16);
}
public StringBuilder(int capacity) {
    super(capacity);
}
public StringBuilder(String str) {
    super(str.length() + 16);
    append(str);
}
public StringBuilder(CharSequence seq) {
    this(seq.length() + 16);
    append(seq);
}
    可以看到,StringBuilder的构造方法和StringBuffer一摸一样,我们看下StringBuilder的append()方法,和StringBuffer一样,他有多个append()方法,支持插入不同类型的数据,我们这里只看其中一个:
public StringBuilder append(String str) {
    super.append(str);
    return this;
}
public AbstractStringBuilder append(String str) {
    if (str == null) str = "null";
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    if (srcBegin < 0) {
        throw new StringIndexOutOfBoundsException(srcBegin);
    }
    if (srcEnd > value.length) {
        throw new StringIndexOutOfBoundsException(srcEnd);
    }
    if (srcBegin > srcEnd) {
        throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
    }
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
    可以看到,大体上和StringBuffer是一样的,只是StringBuilder中的append方法没有使用synchronized修饰符,这代表它并不是一个线程安全的方法,事实上StringBuilder是在StringBuffer之后才出现的类,它的目的就是为了在单线程的环境下使用。


3.总结

1.String, StringBuffer和StringBuilder都可以用来表示字符串常量,但是String类型的对象不可变,如果需要改变字符串内容,建议使用StringBuffer和StringBuilder;
2.StringBuffer和StringBuilder的用法完全相同,区别在于StringBuffer是线程安全的,建议使用在多线程环境下,StringBuilder是非线程安全的,建议使用在单线程环境下。






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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值