字符串三兄弟:String、StringBuilder、StringBuffer

在Java世界中,除了基本数据类型和包装数据类型,最常用的要数字符串类型,也就是String类型,其他还有StringBuilder、StringBuffer,他们堪称字符串三兄弟。

1、String

不可变对象

首先重要的一点:String是不可变对象,通过源码我们发现在String类的定义中加入了final关键字,使得String变成了不可变对象,具体如下面源码所示:

public final class String   //final关键字用在类上,则该类不能被继承
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];  //字符串的底层数据结构是字符数组,同样的字符数组被final修饰,一旦初始化完成,则字符数组的内存地址指向不能改变

所以,每次对String对象重新赋值时,都是指向了一块新的内存空间,这也就是String是不可变对象。

字符串常量池

Java为了避免在一个系统中产生大量的String对象,引入了字符串常量池。

创建一个字符串时,首先会检查池中是否有值相同的字符串对象,如果有就直接返回引用,不会创建字符串对象;如果没有则新建字符串对象,返回对象引用,并且将新创建的对象放入池中。但是,通过new方法创建的String对象是不检查字符串常量池的,而是直接在堆中创建新对象,也不会把对象放入池中。上述原则只适用于直接给String对象引用赋值的情况。

String还提供了intern()方法。调用该方法时,如果字符串常量池中包括了一个等于此String对象的字符串(由equals方法确定),则返回池中的字符串的引用。否则,将此String对象添加到池中,并且返回此池中对象的引用。如下面一段代码的运行结果,可以用上面的描述进行解释。

public class TestString {
    public static void main(String[] args) {
        String str1 = "hello";//检查字符串常量池的
        String str2 = new String("hello");//不检查字符串常量池的,直接在堆中生成对象
        String str3 = "he" + "llo";//字符串的拼接,通过反编译,会发现,JVM对此进行了优化:
        //通过StringBuilder进行拼接的,即StringBuilder的append的方法进行拼接,最后结果转化成String
        //所以结果和Str1一样,也是先检查字符串常量池
        System.out.println(str1 == str2);//结果是false,一个在字符串常量池,一个在堆中,地址不同
        System.out.println(str2.intern() == str1);//调用intern(),结果是true
        System.out.println(str1.equals(str2));//结果是true

        System.out.println(str1 == str3);//结果是true
        System.out.println(str2 == str3);//结果是false
        System.out.println(str2.intern() == str3);//结果是true
        
    }
}

在jdk 8以后,常量池从JVM的永久代(方法区的一种实现)转移到了jdk 8的堆中,避免了永久代被挤满。同时,在jdk 8 中永久代改成了元空间(方法区的另一种实现),并且元空间直接放到内存,而不是JVM内存中了。

 

2、StringBuilder和StringBuffer

通过源码,可以发现,二者都继承了AbstractStringBuilder类,如下面代码:

public final class StringBuilder  //同样有final关键字,表明该类不能被继承
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{


 public final class StringBuffer  //与StringBuilder一样,都继承了AbstractStringBuilder类
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{


//这是AbstractStringBuilder类的定义
abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;//看到底层也是通过字符数组进行存储,但与String不同,这里没有final修饰

与String进行对比,可以理解为,StringBuilder和StringBuffer是可变的字符串,通过拼接等操作,可以在初始地址上扩展字符串,而不是像String一样,开辟新的空间,重新指向。如下面的代码:

public class TestString {
    public static void main(String[] args) {
        StringBuilder stringBuilder = new StringBuilder();

        stringBuilder.append("he");
        System.out.println(stringBuilder);//输出he
        stringBuilder.append("llo");
        System.out.println(stringBuilder);//输出hello
    }
}

这里,我们知道StringBuilder和StringBuffer是可变长度的,而底层是通过字符数组实现的,那么这个就是一个可以扩容的数据结构,而且会有一个初始容量或者设置初始容量的大小,通过阅读源码,可以发现就是这样的:

1、默认初始容量

/**
     * Constructs a string builder with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuilder() {  //默认构造函数,初始容量是16
        super(16);
    }



/**
     * Constructs a string builder with no characters in it and an
     * initial capacity specified by the {@code capacity} argument.
     *
     * @param      capacity  the initial capacity.
     * @throws     NegativeArraySizeException  if the {@code capacity}
     *               argument is less than {@code 0}.
     */
    public StringBuilder(int capacity) {  //也可以设置初始容量大小,一般,如果能预知容量大小时,//最好设置一个固定的容量,在满足使用需要的同时,避免自动扩容对性能的影响
        super(capacity);
    }


//二者都调用了父类的构造方法
/**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

2、扩容机制

上面通过源码,了解了默认容量大小,还有用户可以在构造函数中设置初始容量,在容量不足时,会触发扩容,一般通过append方法添加字符串时,会触发扩容,如下面源码:

  public StringBuilder append(String str) {
        super.append(str); //调用父类的append方法
        return this;
    }


//父类的append方法
public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);//在这里继续查看扩容的代码,传入的参数新加入字符串的长度+已有字符串的长度,即插入后的总长度
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }


private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {//如果插入后的总长度大于已有空间(数组的总长度),则先扩容,再把已有的字符串通过Arrays.copyOf函数复制到扩容后的地址空间,让value指针重新指向扩容后的地址空间
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }


private int newCapacity(int minCapacity) {//扩容,返回申请新的空间的长度
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;//长度是原来的2倍加2
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

上面主要针对StringBuilder进行讲解,StringBuffer基本和其一致,主要区别如下面源码所示:

    @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;
    }


//通过这两个方法的片段我们可以发现,StringBuffer方法上多了synchronized关键字的修饰,使得StringBuffer方法变成了同步方法,所以在高并发场景下,StringBuffer是线程安全的。

总结:

1.从是否可变的角度

String类中使用字符数组保存字符串,因为有“final”修饰符,所以String对象是不可变的。StringBuffer和StringBuilder都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,但没有“final”修饰符,所以两种对象都是可变的。

2.是否多线程安全

String中的对象是不可变的,也就可以理解为常量,所以是线程安全的。

AbstractStringBuilder是StringBuffer和StringBuilder的公共父类,定义了一些字符串的基本操作,如append、、indexOf等公共方法。

StringBuffer对方法加了同步锁(synchronized) ,所以是线程安全的。

StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。

 

参考文档:http://blog.itpub.net/31543790/viewspace-2220506/

                  https://baijiahao.baidu.com/s?id=1629804867201303563&wfr=spider&for=pc

                  https://www.cnblogs.com/peke/p/8044635.html

                  https://blog.csdn.net/u011702479/article/details/82262823

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值