StringBuffer 和 StringBuilder

基于 JDK 1.8.0_151

StringBuilder 和 StringBuffer 均继承自 AbstractStringBuilder,而 StringBuilder 在 StringBuffer 之后出现。按照顺序逐个进行分析 AbstractStringBuilder,StringBuffer,StringBuilder 。

AbstractStringBuilder

AbstractStringBuilder 构造器

AbstractStringBuilder 的无参构造器的作用是为了让子类能够序列化和反序列化。另一个有参构造函数传入的 capacity,实例化存储字符序列的字符数组 value

    AbstractStringBuilder() {
    }

    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

append() 方法

append(String str) 的操作如下:

  • 判断 str 是否为空,若为空,则直接调用 appendNull() 并返回;
  • 计算(count + len)追加 str 之后的长度,并确保存储字符序列的字符数组足够长;
  • str.getChars() 方法将 str 复制到字符数组 value(存储了 StringBuffer 字符序列);
  • 返回当前对象。

ensureCapacityInternal() 方法会检查字符数组 value 的容量是否足以存储追加之后的字符序列,不足则会进行扩容。count 表示 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;
    }

appendNull()

appendNull() 是一个私有方法,它同样先确保 value 容量足够,然后追加 'n','u','l','l' 四个字符到 value 数组中。

AbstractStringBuilder.java 代码片段

    private AbstractStringBuilder appendNull() {
        int c = count;
        ensureCapacityInternal(c + 4);
        final char[] value = this.value;
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }

其它 append 的重载方法追加内容流程类似:

  • 判断参数是否为空;
  • 确保 value 容量足够;
  • 执行追加操作;
  • 返回当前对象。

ensureCapacity(int minimumCapacity) 和 ensureCapacityInternal(int minimumCapacity)

ensureCapacity(int minimumCapacity) 由 public 修饰,而 ensureCapacityInternal(int minimumCapacity) 由 private 修饰,前者调用了后者。前者参数只有输入的 minimumCapacity 为正数时才有效。

    public void ensureCapacity(int minimumCapacity) {
        if (minimumCapacity > 0)
            ensureCapacityInternal(minimumCapacity);
    }

ensureCapacityInternal(int minimumCapacity) 先判断 `minnumCapacity 是否大于字符数组 value 的长度,若超过,则调用 newCapacity(int minCapacity) 计算新的长度,再 Arrays.copyOf() 方法对数组进行了复制。Arrays.copyOf(char[] original, int newLength) 会将 original 复制到一个长度为 newLength 的新数组中并返回。

newCapacity(int minCapacity) 与 hugeCapacity(int minCapacity)

newCapacity(int minCapacity) 返回了一个大于等于 minCapacity 的整数。扩容时先将原来的容量乘以 2 再加 2,如果容量未达到要求,则将 newCopacity 的值设置为传入的 minCapacity。
如果计算的 newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0(MAX_ARRAY_SIZE 是一个常量,值为 Integer.MAX_VALUE - 8),则通过 hugeCapacity(int minCapacity) 来决定扩容后大小。

    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2; // 将 value.length 乘以 2 再加 2
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

hugeCapacity(int minCapacity) 先判断是否产生了整数溢出(传入的 minCapacity 为负数时溢出),溢出了则抛出异常,否则将容量设置为一个最大不超过 MAX_ARRAY_SIZE 的值。

    private int hugeCapacity(int minCapacity) {
        if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
            throw new OutOfMemoryError();
        }
        return (minCapacity > MAX_ARRAY_SIZE)
            ? minCapacity : MAX_ARRAY_SIZE;
    }

总而言之,若 value 的容量较小,则每次容量至少扩容为原来的 2 倍再加 2;若大小还不满足要求,则直接扩容为 minCapacity,但是最大容量不会超过 MAX_ARRAY_SIZE,也就是说 StringBuffer 表示的字符序列的最大长度为 Integer.MAX_VALUE - 8,继续 append 则会抛出 OutofMemoryError() 异常。

trimToSize() 方法

trimtoSize() 方法将 value 的容量压缩到和字符序列的长度一致,在确定不增加字符序列长度时可以调用此方法释放一部分内存。可以释放 (sbf.capacity() - sbf.length())*2 字节的内存。

    public void trimToSize() {
        if (count < value.length) {
            value = Arrays.copyOf(value, count);
        }
    }

setLength(int newLength) 方法

设置序列的新长度,若 newLength > sbf.length(),则超出部分填充 '\0';若 newLength < sbf.length(),则直接将 count 移动到 newLength 位置,下次直接从 newLength 位置开始追加内容。

    public void setLength(int newLength) {
        if (newLength < 0)
            throw new StringIndexOutOfBoundsException(newLength);
        ensureCapacityInternal(newLength);

        if (count < newLength) {
            Arrays.fill(value, count, newLength, '\0');
        }

        count = newLength;
    }

charAt(int index)

这个方法很简单,取出下标为 index 的字符,若 index 不在字符索引范围之内则抛出 StringIndexOutofBoundsException。

    public char charAt(int index) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        return value[index];
    }

codePointAt(int index)

此方法返回索引为 index 的字符的 16 位 Unicode 编码系统的码点。编码系统将字符映射到一个数字,这个数字就是码点,例如 'a' 映射的码点就是 97。需要注意的是,char类型是 2 个字节,但码点的的范围却可以超过 65535 ,因为有些字符需要用 4 个字节表示。(参考:Java中码点和代码单元
如:

StringBuilder sb =new StringBuilder("𝕆"); // 这是现实世界中的一个字符,但 Java 中不能用 char 接收,而要用 String。
System.out.println("len: " + sb.length() + ", codePoint:" + sb.codePointAt(0)); // 输出:len: 2, codePoint:120134
index 0 1 2 3 4 5 MAX_ARRAY_SIZE ...... a 𝕆 b code point 0 1 2 count capacity

codePointBefor(int index)

返回前一个码点值,例如:

代码 返回
"a𝕆b".codePointBefore(1) 97, 即 'a' 的码点
"a𝕆b".codePointBefore(2) 55349,即 "𝕆" 前半个部分两个字节对应的 char 字符的码点
"a𝕆b".codePointBefore(3) 120134,即字符 "𝕆" 的码点

index 恰好落在了某个2字节码点的后半部分,则返回的是前半部分对应的 char 字符的码点。

codePointCount(int beginIndex, int endIndex)

此方法返回 [beginIndex, endIndex) 字符序列之间码点的数量,可以通过 sb.codePointCount(0, sb.length()) 来统计当前字符序列中总共有多少个 Unicode 字符。

StringBuilder sb =new StringBuilder("a𝕆b");
System.out.println(sb.codePointCount(0,1)); // 1
System.out.println(sb.codePointCount(0,2)); // 2
System.out.println(sb.codePointCount(0,3)); // 2
System.out.println(sb.codePointCount(0,4)); // 3

offsetByCodePoints(int index, int codePointOffset)

此方法表示从 index 开始,向右偏移 codePointOffset 个码点之后的索引。

例如:

StringBuilder sb =new StringBuilder("a𝕆b");
System.out.println(sb.offsetByCodePoints(0,0));// 0
System.out.println(sb.offsetByCodePoints(0,1));// 1
System.out.println(sb.offsetByCodePoints(0,2));// 3
System.out.println(sb.offsetByCodePoints(0,3));// 4
System.out.println(sb.offsetByCodePoints(1,0));// 1
System.out.println(sb.offsetByCodePoints(1,1));// 3
System.out.println(sb.offsetByCodePoints(1,2));// 4 index 落在𝕆的前半部分
System.out.println(sb.offsetByCodePoints(2,1));// 3
System.out.println(sb.offsetByCodePoints(2,2));// 4 index 落在𝕆的后半部分

getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)

此方法将 [srcBegin, srcEnd) 范围的字符序列复制到字符数组 dst 中,存放下标从 dstBegin 开始。同样下标不能越界。

public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
{
    if (srcBegin < 0)
        throw new StringIndexOutOfBoundsException(srcBegin);
    if ((srcEnd < 0) || (srcEnd > count))
        throw new StringIndexOutOfBoundsException(srcEnd);
    if (srcBegin > srcEnd)
        throw new StringIndexOutOfBoundsException("srcBegin > srcEnd");
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}

setCharAt(int index, char ch)

将索引为 index 的字符设置为 ch,因为底层本身存储的就是字符数组,所以这个方法很高效。

replace(int start, int end, String str)

此方法将 [start, end) 字符序列替换为 str,替换时先确保容量足够,然后将 [end, count) 之间的序列复制到后面,然后再将 str 复制到中间。同样有索引越界检查。

public AbstractStringBuilder replace(int start, int end, String str) {
    if (start < 0)
        throw new StringIndexOutOfBoundsException(start);
    if (start > count)
        throw new StringIndexOutOfBoundsException("start > length()");
    if (start > end)
        throw new StringIndexOutOfBoundsException("start > end");
    if (end > count)
        end = count;
    int len = str.length();
    int newCount = count + len - (end - start);
    ensureCapacityInternal(newCount);
    System.arraycopy(value, end, value, start + len, count - end);
    str.getChars(value, start);
    count = newCount;
    return this;
}

substring(int start), subSequence(int start, int end), substring(int start, int end)

前 2 个方法调用了第 3 个方法。此方法会创建一个新的 String 对象并返回。

public String substring(int start, int end) {
    if (start < 0)
        throw new StringIndexOutOfBoundsException(start);
    if (end > count)
        throw new StringIndexOutOfBoundsException(end);
    if (start > end)
        throw new StringIndexOutOfBoundsException(end - start);
    return new String(value, start, end - start);
}

insert(int offset, String str)

此方法将 str 插入到字符序列中,插入索引从 offset 开始。

insert() 方法有若干重载方法,这些重载方法套路大同小异,流程如下:

  • 检查索引是否越界
  • 判断是否为空,若为空则转化为 "null" 再继续
  • 确保 value 容量足够插入
  • 将 [offset, count] 部分的代码复制到后面
  • 将 str 复制到中间

indexOf(String str) 与 lastIndexOf(String str)

这两个 API 直接调用了 String 的静态方法,分别返回字符序列中第一次出现 str 的索引和最后一次出现 str 的索引。

reverse()

此方法将字符序列反转并返回,注意,代码并非单纯地将 value 数组反转,而是按照码点将字符反转。否则,反转之后可能出现乱码。
例如:

StringBuilder sb =new StringBuilder("a𝕆b");
System.out.println(sb.reverse());// "b𝕆a"
public AbstractStringBuilder reverse() {
    boolean hasSurrogates = false;
    int n = count - 1;
    for (int j = (n-1) >> 1; j >= 0; j--)
        int k = n - j;
        char cj = value[j];
        char ck = value[k];
        value[j] = ck;
        value[k] = cj;
        if (Character.isSurrogate(cj) ||
            Character.isSurrogate(ck)) {
            hasSurrogates = true;
        }
    }
    if (hasSurrogates) {
        reverseAllValidSurrogatePairs();
    }
    return this;
}

以上就是 AbstractStringBuilder 的内容了。

StringBuffer

StringBuffer 是一个可变的、线程安全的字符序列,在 JDK1.0 的时候出现。它继承自 AbstractStringBuilder,复用了父类的数据存储结构和几乎所有的方法。通过在方法前面增加 synchronized 关键字来达到线程安全的效果。

StringBuffer 构造器

StringBuffer 包含了 4 个构造方法,所有的构造方法都调用了父类的构造器 AbstractStringBuilder(int capacity),前面提到这个构造器实例化了字符数组 value。由下面代码此可知,StringBuffer 默认最小的 capacity (value 的长度)为 16,除非手动传入一个值。


    public StringBuffer() { // 实例化 value = new char[16]
        super(16);
    }

    public StringBuffer(int capacity) { // 自定义 capacity
        super(capacity);
    }

    public StringBuffer(String str) { // 实例化 value = new char[str.length()+16],并将初始序列设置为 str
        super(str.length() + 16);
        append(str);
    }

    public StringBuffer(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }

StringBuffer append(String str) , StringBuffer insert(int offset, boolean b) 和 StringBuffer replace(int start, int end, String str)

append(String str)synchronized` 关键字修饰,这是线程安全的保证。它先将成员变量 toStringCache 置为了空,然后调用父类的 append(String str) 方法,最后返回当前对象。toStringCache 是 StringBuffer toString() 方法返回值的缓存,在调用 toString() 方法且 toStringCache 为空时被赋值,在 StringBuffer 对象发生改变的时候被清空(或者说缓存失效),即赋值为 null。

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

StringBuffer 复用了父类 AbstractStringBuilder 的几乎所有 API, 然后用 synchronized 修饰自己的方法。如果是读取操作,则直接调用父类方法,如:String substring(int start) 方法。如果是增、删、改操作则先将 toStringCache 清空,再调用父类方法。

下面的这个 insert 方法没有被 synchronized 修饰,原因它调用了同一个 StringBuffer 对象的其它方法。从注释也可以看出原因。

当前方法调用了父类方法 AbstractStringBuilder insert(int offset, boolean b),父类方法又调用了 AbstractStringBuilder insert(int offset, String str)AbstractStringBuilder insert(int dstOffset, CharSequence s, int start, int end),而着两个方法都均已被子类 StringBuffer 覆盖,所以不需要再使用一次 synchronized 获取对象锁,以提高运行效率。

@Override
public StringBuffer insert(int dstOffset, CharSequence s) {
    // Note, synchronization achieved via invocations of other StringBuffer methods
    // after narrowing of s to specific type
    // Ditto for toStringCache clearing
    super.insert(dstOffset, s);
    return this;
}

String toString()

toString() 将字符序列转化为字符串。它先判断了 toStringCache 是否为空;若为空,则复制出一个数组并赋值给 toStringCache 并返回;若不为空则直接返回。toStringCache 保证了连续多次 toString() 方法不会重复产生同样的字符串对象,在一定程度上节省了空间,提高了效率。但真正能提高多少效率还需要打个问号,毕竟缓存的结果既不需要通过 CPU 的密集计算得到也不需要 IO 操作得到。

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

以上就是 StringBuffer 在 AbstractStringBuilder 基础上增加的内容。在方法前面增加 synchronized 关键字,通过对象锁来保证线程安全,但这极大损失了效率。

StringBuilder

StringBuilder 在 JDK1.5 时引入,它与 StringBuffer 一样继承了 AbstractStringBuilder,也复用了父类的存储结构和几乎所有 API。它的引入是为了在单线程环境下替代 StringBuffer,毕竟在每线程安全问题的场景下它的效率要高很多。

StringBuilder 构造器

StringBuilder 的构造器和 StringBuffer 一模一样。

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

append(), insert(), delete()

StringBuilder 完全复用了父类 AbstractStringBuilder 的方法,没有新增任何特殊代码。

@Override
public StringBuilder append(String str) {
    super.append(str);
    return this;
}

toString() 方法

StringBuilder 的 toString() 方法通过复制一份 value 到 String 中产生了一个新的 String 对象。因此,连续多次调用 toString() 将返回多个内容相同 String 对象。之所以 StringBuilder 没有缓存,是因为 String 对象应该保证不可变,而 StringBuilder 是非线程安全的,无法像 StringBuffer 一样确保任何时候都只有一个线程访问成员变量,从而严格保证 toStringCache 的同步。(更详细的解释:为什么 StringBuffer 有 toStringCache 而 StringBuilder 没有?

小结

  • StringBuffer 和 StringBuilder 均为 AbstractStringBuilder 的子类,两者均复用了父类的存储结构和几乎所有方法;
  • StringBuffer 在方法前面增加了 synchronized 关键字,通过对象锁来保证线程安全,但是效率很低;
  • StringBuilder 为了在无线程安全环境中替代 StringBuffer 而生,在无线程安全问题时应该使用 StringBuilder,局部变量显然得用 StringBuilder。
posted @ 2020-10-29 15:28  Robothy  阅读(156)  评论(0编辑  收藏  举报