String、StringBuilder和StringBuffer详解

String、StringBuilder和StringBuffer详解以及区别

String (字符串常量)

String类型是java中最常用的数据类型,String 类代表字符串,Java中的所有字符串字面值(如 “abc” )都作为此类的实例实现,字符串是常量;它们的值在创建之后不能更改.字符串缓冲区支持可变的字符串.因为 String 对象是不可变的,所以可以共享.来看部分源码:


public final class String implements java.io.Serializable, Comparable<String>, CharSequence {

    private final char value[];


    private int hash; // Default to 0

    private static final long serialVersionUID = -6849794470754667710L;


    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];


    public String() {
        this.value = "".value;
    }


    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }


    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
	public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }
	...
}

从源码我们可以看出来,String类是final的,String底层也是使用一个字符数组来维护的,同时value[] 也是final的,所以String类的值也是final类型的,
不能被改变的,所以只要一个值改变就会生成一个新的String类型对象,存储String数据也不一定从数组的第0个元素开始的,而是从offset所指的元素开始的.
在存储方面,String是很值得研究的,其实在String的设计过程中就采用了享元模式去实现的,在储存这块其实还有很多延伸出来的面试题,就不一一举例了,看了
下边的详细分析,自然也就懂了.
创建String对象基本也就两种,比如:

String str = "aaa";//直接赋值
String str = new String("aaa");//new对象实例化的方式

那这两种方式有啥区别呢?这要从内存上去说了,这里不好画图,只能脑子模拟了:
直接赋值(享元模式):
在字符串中,如果采用直接赋值的方式(String str=“aaa”)进行对象的实例化,则会将匿名对象“aaa”放入对象池,每当下一次对
不同的对象进行直接赋值的时候会直接利用池中原有的匿名对象,从而起到共享的效果.而这种方式也只会开辟一块堆内存空间,并且会
自动入池,不会产生对象垃圾.
对象实例化:
如果采用实例化的方式,则会创建两个对象出来,为什么说两个呢?因为在String被调用构造方法时会产生一个新的aaa对象在堆内存中,
而这个时候的对象是没有引用的,当new这个关键字被执行时又会产生一个新的对象aaa,并且把aaa的引用传递给str,但是构造方法产生
需要通过intern()这个方法进行手工入池才不会被认为是垃圾,当然在开发中并不推荐这种方式.
在这里需要了解的一个问题,那就是在Object对象中,equals()这个方法是用来比较内存地址的,但是String重写了这个方法,变成了比较内容,
所以String的值,只要内容一样,那怕地址不同,返回的也一直是true.

StringBuilder (非线程安全的字符串变量)

StringBuilder 是一个可变的字符序列.它继承于AbstractStringBuilder,实现了CharSequence接口.
StringBuffer 也是继承于AbstractStringBuilder的子类;但是,StringBuilder和StringBuffer不同,前者是非线程安全的,后者是线程安全的.
其实StringBuilder也是通过建造者模式去实现的,底层也是一个字符数组而已,他的默认长度是16,可以通过append方法去追加内容,与String不同,
它不是final的,可以修改.另外,与String不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量count用来表示数组中已经使用的字
符个数,count的默认值是0.
来看append的代码:

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

append会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展,实际使用的长度用count体现.具体来说,
ensureCapacityInternal(count+len)会确保数组的长度足以容纳新添加的字符,str.getChars会拷贝新添加的字符到字符数组中,
count+=len会增加实际使用的长度.
ensureCapacityInternal的代码如下:

private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    if (minimumCapacity - value.length > 0)
        expandCapacity(minimumCapacity);
}

如果字符数组的长度小于需要的长度,则调用expandCapacity进行扩展,expandCapacity的代码是:

void expandCapacity(int minimumCapacity) {
    int newCapacity = value.length * 2 + 2;
    if (newCapacity - minimumCapacity < 0)
        newCapacity = minimumCapacity;
    if (newCapacity < 0) {
        if (minimumCapacity < 0) // overflow
            throw new OutOfMemoryError();
        newCapacity = Integer.MAX_VALUE;
    }
    value = Arrays.copyOf(value, newCapacity);
}

扩展的逻辑是,分配一个足够长度的新数组,然后将原内容拷贝到这个新数组中,最后让内部的字符数组指向这个新数组,这个逻辑主要靠下面这句代码实现:

value = Arrays.copyOf(value, newCapacity);

我们主要看下newCapacity是怎么算出来的.

参数minimumCapacity表示需要的最小长度,需要多少分配多少不就行了吗?不行,因为那就跟String一样了,每append一次,都会进行一次内存分配,
效率低下.这里的扩展策略,是跟当前长度相关的,当前长度乘以2,再加上2,如果这个长度不够最小需要的长度,才用minimumCapacity.
比如说,默认长度为16,长度不够时,会先扩展到16X2+2即34,然后扩展到34X2+2即70,然后是70X2+2即142,这是一种指数扩展策略.为什么要加2?
大概是因为在原长度为0时也可以一样工作吧.
为什么要这么扩展呢?这是一种折中策略,一方面要减少内存分配的次数,另一方面也要避免空间浪费.在不知道最终需要多长的情况下,指数扩展
是一种常见的策略,广泛应用于各种内存分配相关的计算机程序中.
字符串构建完后它是通过toString方法去完成的,我们来看toString代码:

public String toString() {
	// Create a copy, don't share the array
	return new String(value, 0, count);
}

基于内部数组新建了一个String,注意,这个String构造方法不会直接用value数组,而会新建一个,以保证String的不可变性.当然还有很多其他的
构建方式比如说在指定位置插入字符串什么的,这里就不一一赘述了,有兴趣的去看看StringBuilder的源码就明白了,其实逻辑很简单.类似于链表
的插入,还有一些拷贝的内容.还有就是+ 和+=这些运算符最终会被jvm解析成append等等.

StringBuffer (线程安全的字符串变量)

Java.lang.StringBuffer线程安全的可变字符序列.一个类似于 String 的字符串缓冲区,但不能修改.虽然在任意时间点上它都包含某种特定的
字符序列,但通过某些方法调用可以改变该序列的长度和内容.可将字符串缓冲区安全地用于多个线程.可以在必要时对这些方法进行同步,因此任意
特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致.StringBuffer 上的主要操作是 append
和 insert 方法,可重载这些方法,以接受任意类型的数据.每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字
符串缓冲区中.append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符.
例如,如果 z 引用一个当前内容是“start”的字符串缓冲区对象,则此方法调用 z.append(“le”) 会使字符串缓冲区包含“startle”,而
z.insert(4, “le”) 将更改字符串缓冲区,使之包含“starlet”.
其实Strngbuffer和Stringbuilder非常像,类的结构也是一样的,唯一不同的地方就在于,在Stringbuffer的append方法上都加上了synchronized
关键字来实现多线程下的线程安全。其他的和StringBuilder一致。同时它也比Stringbuilder多了一个参数toStringCache,
这个参数在进行toString的时候会起作用,作用就是如果StringBuffer对象此时存在toStringCache,在多次调用其toString方法时,
其new出来的String对象是会共享同一个char[] 内存的,达到共享的目的。但是StringBuffer只要做了修改,其toStringCache属性
值都会置null处理。这也是StringBuffer和StringBuilder的一个区别点。

总结

String

类不可变,内部维护的char[] 数组长度不可变,为final修饰,String类也是final修饰,不存在扩容。字符串拼接,截取,都会生成
一个新的对象。频繁操作字符串效率低下,因为每次都会生成新的对象.

StringBuilder

类内部维护可变长度char[] , 初始化数组容量为16,存在扩容, 其append拼接字符串方法内部调用System的native方法,进行数组的
拷贝,不会重新生成新的StringBuilder对象。非线程安全的字符串操作类, 其每次调用 toString方法而重新生成的String对象,不会
共享StringBuilder对象内部的char[],会进行一次char[]的copy操作.

StringBuffer

类内部维护可变长度char[], 基本上与StringBuilder一致,但其为线程安全的字符串操作类,大部分方法都采用了Synchronized关键字
修改,以此来实现在多线程下的操作字符串的安全性。其toString方法而重新生成的String对象,会共享StringBuffer对象中的
toStringCache属性(char[]),但是每次的StringBuffer对象修改,都会置null该属性值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值