2.孪生兄弟StringBuffer和StringBuilder

403 阅读3分钟

这两个类在我看来就是一个'孪生兄弟',所以这次我放到一起.

1. 概览

先看下两个类的定义信息:

public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence{

public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence{

从表面上看,这两个类除了类名不相同,其他都是一样的.

  • final修饰,表示String类不能被继承.

  • CharSequence提供了对不同类型的字符数组进行只读访问的方法.

  • 实现Serializable接口,表示可序列化.

以上两点和String没太大的区别,那看下他们所继承的AbstractStringBuilder:

abstract class AbstractStringBuilder implements Appendable, CharSequence {

Appndable接口定义了一些可以向其追加char序列和值的对象的方法,而CharSequence上面讲过了,不在重复赘述.

虽然该类是抽象类,但是抽象方法也只有一个,就一个toString()方法,显而易见,这是一个通用的工具方法类.不同特性的子类只需要继承,重写需要的方法便行.

AbstractStringBuilder的类注释看出,该抽象类实现的是一个可变的字符序列,可以调某些方法进行改变.和String一样,也是有一个char类型的数组.

既然是通用的工具方法,那具体的差异应该就是在子类中体现的.

2. 方法

这里我以Stringbuilder为主进行讲解,如没有特别说明,则默认实现一样,只是在方法上StringBuffer加了Synchronized.(因为单线程情况下,StringBuilder速度更快,而且StringBuffer有时候并不是'同步的').

构造函数就不讲了,都是默认的16长度,或者是传参字符串长度加上16.

主要的是append()insert()一个是追加,一个是插入.在抽象类中,有很多append()insert()的重载方法,这里只挑一些关键的,其他都是大同小异.

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;
}
  1. 如果为则会添加n,u,l,l四个字符,这个方法点进去就能看到.
  2. 主要的是ensureCapacityInternal(count + len)扩容方法.
private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    if (minimumCapacity - value.length > 0) {
        value = Arrays.copyOf(value,newCapacity(minimumCapacity));
    }
}

判断是否有足够的容量容下需要添加的内容.没有则需要扩容newCapacity():

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

如果需要的容量比原来的两倍加二还要大,则新容量以需要的为准,否则就是两倍加二.最后一步也就是校验长度,可能会存在内存溢出.

扩容完了,接下类就是将值添加进去了:

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

使用的是System.arraycopy()的方法进行复制.

insert()方法大致步骤也是如此,只不过在最后System.arraycopy()时,参数传递的差别便是了.

还有个比较有意思的地方:

@Override
public synchronized String toString() {
    if (toStringCache == null) {
        toStringCache = Arrays.copyOfRange(value, 0, count);
    }
    return new String(toStringCache, true);
}

StringBuffertoString中使用了一个缓存数组,可以提高获取String的速度,原因在于他所使用的String构造函数不涉及数组的复制.

3.杂谈

这两个类在append()的方法中,都有构造者模式的特点,以至于可以采用链式写法.

关于上面讲的StringBuffer有时候并不是真正的'同步',也主要是体现在多次调用StringBuffer的不同方法的情况下,这一系列操作,并不能保证原子性.

在实际情况下,StringBuffer用得还是比较少的,而另一个官方也在单线程下推荐.主要也是用在大量字符串拼接的场景,类似循环拼接之类的.如果使用String则会导致频繁的创建String实例,进而对内存造成影响.

不难发现,在这些源码中,方法写得非常好,考虑的比较周到(重载用得好),包括一些抽象父类的设计.