JDK 1.8 下的 StringBuilder 和 StringBuffer 区别与源码分析

本文详细分析了JDK 1.8中StringBuilder和StringBuffer的区别,主要关注它们的父类AbstractStringBuilder。讨论了value成员变量、append方法以及扩容机制,揭示了线程安全和性能之间的权衡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 - 引言

在 Java 中,String 被设置为不可变类,JDK 开发人员很小心地保证 String 的底层存储结构 private final char[] value 不被修改。所有对字符串的直接赋值操作,实际上都将字符串变量指向了新的 String 对象。

对于需要对字符串进行大量修改的程序来说,例如对字符串进行拼接,会产生很多无用的 String 实例,所以我们常常会使用 StringBuffer 或者 StringBuilder 来取代 String。


2 - StringBuilder 和 StringBuffer

这两个类其实在实现上是大同小异的,主要的区别就是 StringBuffer 在绝大部分必要的方法上添加了 synchronized 关键字,所以 StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的。在单线程应用中,使用哪一个问题都不大,但 StringBuffer 使用同步锁会损失一部分性能,这一点也是需要知道的。(有点像 HashMap 和 HashTable)

先观察这两个类的声明,我们发现它们继承相同的父类和实现相同的接口,同时两个类几乎没有自己定义的成员变量。

public final class StringBuffer extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {
	/**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache; // 这个cache在本文不讨论
    
    static final long serialVersionUID = 3388685877147921107L;
	...
	@Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }
    ...
}

public final class StringBuilder extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {
    
  	static final long serialVersionUID = 4383685877147921099L;
	...
	@Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
    ...
}

观察两个类的其他方法,我们发现大部分的方法都是直接调用父类已经定义好的方法,所以StringBuilder 和 StringBuffer 的研究我们先放下,将注意力放在它们的父类 AbstractStringBuilder 上。


3 - AbstractStringBuilder 的值修改

首先我们比较关心 AbstractStringBuilder 的成员变量,或者说关心他底层实际的存储结构,可以猜想,肯定是一个 char 数组,情况也确实如此,如下:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;
	...
}

我们看到只有两个成员变量(还有一些常量没有列出来),其中 value 就是存储字符串的底层数据结构,count 是记录字符数的。

3.1 value 成员变量

value 没有使用 final 修饰,意味着我们可以改变 value 的指向,指向一个新的数组。但是如果只是单纯替换 char 数组的某个字符我们自然不需要改变整个指向,比如下面的代码。

    public void setCharAt(int index, char ch) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        value[index] = ch;  // 直接修改value[i]的值
    }

在 String 中为了保持不变性,是不允许直接这样修改 value 的某个字符,所以除了构造方法外,我们看不到 value[i] = ch 这种赋值语句。

3.2 append()

看看 append() 中是怎么使用这个 value 的:

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

我们在上面的代码中,似乎没有看到将 str 的值保存到 value,怎么回事呢?不急,我们留意 getChars() 这个方法,打开源码看到:

    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);
        }
        // 第一个参数 value 是 String 的 char 数组
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }

一切恍然大悟,原来调用了 System 下的数组拷贝方法,这个方法的函数原型长下面这样:

public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

这个是一个 native 方法,不是 Java 的实现。我们翻译一下注释就知道这个方法会把 src 的 srcPos ~ srcPos + (length - 1) 之间的元素复制到 dest 的 destPos ~ destPos + (length - 1) 上,简单来说就是将 src 的某一段元素全部复制到 dest 上。

append() 直接将 str 的字符数组拷贝到 value,本质上也是将 char 一个一个赋值,并没有改变 value 的指向。那么什么时候 value 可以改变指向呢?

3.3 ensureCapacity()

其实很容易想到,当 value 的空间放不下所有字符的时候,value 应该重新申请一块更大内存,我们观察一下 AbstractStringBuilder 中关于容量的方法即可。最终修改容量的方法是如下:

    private void ensureCapacityInternal(int minimumCapacity) {
        if (minimumCapacity - value.length > 0) { // 当前需求的容量大于数组实际容量
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));  // 确认新数组的容量
        }
    }

终于我们在 Arrays.copyOf() 中找到新数组分配的代码:

    public static char[] copyOf(char[] original, int newLength) {
        char[] copy = new char[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

在寻找 value 的使用过程中,我们已经基本了解了 AbstractStringBuilder 的一些基本方法,通过类比我们也能猜测到其他诸如 insert、replace等修改 value 的方法是怎样实现的了,无非就是确认容量再进行拷贝。


4 - AbstractStringBuilder 的扩容

上一节已经给出了判断是否扩容的方法 ensureCapacityInternal() ,这一节我们研究扩容后的新容量是怎么确定的。

下面将这一系列方法截取并翻译:

    /**
     * 确保当前容量至少和指定容量相等。
     * 如果当前容量比 {@code minimumCapacity} 指定的容量小,那么就分配一个
     * 更大的容量,新容量是下面两者中的较大者:
     * <ul>
     * <li>The {@code minimumCapacity} argument.
     * <li>Twice the old capacity, plus {@code 2}.
     * </ul>
     * 如果参数是非正数,这个方法直接返回。
     * Note that subsequent operations on this object can reduce the
     * actual capacity below that requested here.
     *
     * @param   minimumCapacity   the minimum desired capacity.
     */
	public void ensureCapacity(int minimumCapacity) {
        if (minimumCapacity > 0)
            ensureCapacityInternal(minimumCapacity);
    }
    
    /**
     * 如果参数是正数,这个方法的效果就如 {@code ensureCapacity} 所说的那样,
     * 但这个方法不是一个同步方法。
     * 如果由于数值溢出导致 {@code minimumCapacity} 是非正数,这个方法会抛出
     * {@code OutOfMemoryError}.
     */
    private void ensureCapacityInternal(int minimumCapacity) {
        if (minimumCapacity - value.length > 0) { // 当前需求的容量大于数组实际容量
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));  // 确认新数组的容量
        }
    }

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // (2^31-1)-8

    /**
     * 返回一个至少和 {@code minCapacity} 相等的容量。
     * 如果可以,会返回一个当前容量加倍后 +2 的容量。
     * 不会返回一个容量大于 {@code MAX_ARRAY_SIZE}, 除非指定的最小容量更大。
     *
     * @param  minCapacity the desired minimum capacity
     * @throws OutOfMemoryError if minCapacity is less than zero or
     *         greater than Integer.MAX_VALUE
     */
    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;  
    }

    private int hugeCapacity(int minCapacity) {
        if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
            throw new OutOfMemoryError(); // 实际请求的 size 超过 2^31-1
        }
        return (minCapacity > MAX_ARRAY_SIZE)
            ? minCapacity : MAX_ARRAY_SIZE;
    }

简单介绍一下 newCapacity() 方法流程:

  1. 首先尝试将 newCapacity 设置为现有容量的 2 倍加 2。
  2. 但是扩充后有几种可能,第一种是不满足申请的空间,此时 0 < newCapacity < minCapacity;第二种是扩充后有可能超过 int 类型正数的最大值(231−1)(2^{31}-1)(2311),造成溢出,此时 newCapacity < 0 。 这意味着这种扩充方法不适用,我们尝试将 newCapacity 设置为 minCapacity,也就是要多少给多少。
  3. 我们理想中的 newCapacity 应该是 0 < newCapacity <= MAX_ARRAY_SIZE 。但现实很骨感,假如 newCapacity 超过了 MAX_ARRAY_SIZE 怎么处理?我们会调用 hugeCapacity() ,申请最多不超过 (231−1)(2^{31}-1)(2311) 的容量。

考虑一个问题:假如 minCapacity 是负数怎么办?负数什么时候会出现呢?
很简单,负数会在hugeCapacity() 中抛出 Error。minCapacity 溢出就是负数,这个负数其实也代表我们申请的空间已经超过 Integer.MAX_VALUE 了,所以 JDK 开发者也在注释中用 overflow 表示这种情况 。

关于源码的部分还有很多没有讲解到的地方,也有可能讲的不对的地方,欢迎大家留言指出。


正文结束,欢迎留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值