String及StringTable(四):StringBuffer与StringBuilder对比

时间:2022-07-23
本文章向大家介绍String及StringTable(四):StringBuffer与StringBuilder对比,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

分析完StringBuilder,然后再聊StringBuffer就简单多了。因为StringBuffer同样也是继承了AbstractStringBuilder。

1.类结构及成员变量

1.1 类结构

此时对为啥要将StringBuilder还拆分为一个抽象类AbstractStringBuilder恍然大悟了。因为StringBuffer可以复用。

/**
 * A thread-safe, mutable sequence of characters.
 * A string buffer is like a {@link String}, but can be modified. At any
 * point in time it contains some particular sequence of characters, but
 * the length and content of the sequence can be changed through certain
 * method calls.
 * <p>
 * String buffers are safe for use by multiple threads. The methods
 * are synchronized where necessary so that all the operations on any
 * particular instance behave as if they occur in some serial order
 * that is consistent with the order of the method calls made by each of
 * the individual threads involved.
 * <p>
 * The principal operations on a {@code StringBuffer} are the
 * {@code append} and {@code insert} methods, which are
 * overloaded so as to accept data of any type. Each effectively
 * converts a given datum to a string and then appends or inserts the
 * characters of that string to the string buffer. The
 * {@code append} method always adds these characters at the end
 * of the buffer; the {@code insert} method adds the characters at
 * a specified point.
 * <p>
 * For example, if {@code z} refers to a string buffer object
 * whose current contents are {@code "start"}, then
 * the method call {@code z.append("le")} would cause the string
 * buffer to contain {@code "startle"}, whereas
 * {@code z.insert(4, "le")} would alter the string buffer to
 * contain {@code "starlet"}.
 * <p>
 * In general, if sb refers to an instance of a {@code StringBuffer},
 * then {@code sb.append(x)} has the same effect as
 * {@code sb.insert(sb.length(), x)}.
 * <p>
 * Whenever an operation occurs involving a source sequence (such as
 * appending or inserting from a source sequence), this class synchronizes
 * only on the string buffer performing the operation, not on the source.
 * Note that while {@code StringBuffer} is designed to be safe to use
 * concurrently from multiple threads, if the constructor or the
 * {@code append} or {@code insert} operation is passed a source sequence
 * that is shared across threads, the calling code must ensure
 * that the operation has a consistent and unchanging view of the source
 * sequence for the duration of the operation.
 * This could be satisfied by the caller holding a lock during the
 * operation's call, by using an immutable source sequence, or by not
 * sharing the source sequence across threads.
 * <p>
 * Every string buffer has a capacity. As long as the length of the
 * character sequence contained in the string buffer does not exceed
 * the capacity, it is not necessary to allocate a new internal
 * buffer array. If the internal buffer overflows, it is
 * automatically made larger.
 * <p>
 * Unless otherwise noted, passing a {@code null} argument to a constructor
 * or method in this class will cause a {@link NullPointerException} to be
 * thrown.
 * <p>
 * As of  release JDK 5, this class has been supplemented with an equivalent
 * class designed for use by a single thread, {@link StringBuilder}.  The
 * {@code StringBuilder} class should generally be used in preference to
 * this one, as it supports all of the same operations but it is faster, as
 * it performs no synchronization.
 *
 * @author      Arthur van Hoff
 * @see     java.lang.StringBuilder
 * @see     java.lang.String
 * @since   JDK1.0
 */
 public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

}

其注释大意为:提供一个线程安全,可变的字符串序列。这个字符串序列可以通过一些方法进行调整。 StringBuffer对于对现场而言是安全的,因为内部所有的方法都是同步方法,通过synchronized确保了这些方法的顺序性。主要的一些操作是append和insert等方法。每当涉及到对StringBuffer源序列进行同步操作的时候,这个类仅仅执行字符缓冲区,而不是源对象。 每个StringBuffer有一个容量,只要长度不超过buffer的容量,没必要重新分配。一旦长度大于内部buffer长度,则会自动放大。 将null传递给构造函数会抛出NullPointerException。 StringBuilder与之等效,但是其效率更优,因为它不同步。

1.2 成员变量

与StringBuilder很大的不同是,在StringBuffer中,存在一个toStringCache属性。

    /**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache;

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    static final long serialVersionUID = 3388685877147921107L;

这个成员变量的意思是说在toString需要的时候,将最新的value中的内容。如果有修改的时候则将这个cache清空。

2.toString方法比较

我们在前面知道,StringBuffer比StringBuilder多了一个toStringCache数组,为什么StringBuffer的实现与StringBuilder不同呢?我们对其代码进行分析。

2.1 StringBuilder.toString

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

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

实际上这个还是使用的是 Arrays.copyOfRange。其底层是通过System.arraycopy进行的对象拷贝。

2.2 StringBuffer.toString

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

在看看String的这个构造方法:

/*
* Package private constructor which shares value array for speed.
* this constructor is always expected to be called with share==true.
* a separate constructor is needed because we already have a public
* String(char[]) constructor that makes a copy of the given char[].
*/
String(char[] value, boolean share) {
    // assert share : "unshared not supported";
    this.value = value;
}

这个构造方法只提供了同package中的类才能访问到。 我们可以看到,实际上是一开始,在String类中定义了一个char数组,之后调用这个String比较特殊的构造函数,直接修改指针,指向这个共享的数组。 最开始我也没看明白,这个toStringCache的意义。 但是,我们试想一种情况,在多线程的情况下,如果使用了StringBuffer,且其值不发生改变的时候,就可以直接复用前一次的结果。虽然多个线程new了不同的String,但是其本质的char数组只有一个,不会重复的System.arraycopy。因此会带来性能的提升。虽然StringBuffer的toString方法本身是synchronized的,但是这样一来可以降低对象拷贝带来的性能损耗。

3.同步

StringBuffer与StringBuilder最大的区别在于同步,可以简单认为StringBuffer就是加上了synchronized的StringBuilder。实际上StringBuffer的实现也是在各个方法上加上synchronized关键字。

    @Override
    public synchronized int length() {
        return count;
    }

    @Override
    public synchronized int capacity() {
        return value.length;
    }


    @Override
    public synchronized void ensureCapacity(int minimumCapacity) {
        super.ensureCapacity(minimumCapacity);
    }

    /**
     * @since      1.5
     */
    @Override
    public synchronized void trimToSize() {
        super.trimToSize();
    }

    /**
     * @throws IndexOutOfBoundsException {@inheritDoc}
     * @see        #length()
     */
    @Override
    public synchronized void setLength(int newLength) {
        toStringCache = null;
        super.setLength(newLength);
    }
    
        public synchronized StringBuffer append(StringBuffer sb) {
        toStringCache = null;
        super.append(sb);
        return this;
    }

    /**
     * @since 1.8
     */
    @Override
    synchronized StringBuffer append(AbstractStringBuilder asb) {
        toStringCache = null;
        super.append(asb);
        return this;
    }

可以看到所有核心的方法都是加上synchronized之后调用的super方法。 在对String中char数组有结构性变更的地方都会将toStringCache变为null。

4.总结

StringBuffer与StringBuilder的主要区别在于:

  • StringBuffer是线程安全的,适合在多线程环境下使用,StringBuilder不是线程安全的,只能在单线程下使用。
  • 二者的toString方法实现不同,StringBuffer中考虑到了多线程环境,因而进行了优化,加入了toStringCache,当StringBuffer固定不变时多线程进行范围的时候可以有效复用。不会反复调用System.arraycopy。但是这个优化对于StringBuilder实际上意义不大,StringBuilder本身不具有线程安全性。尤其是对于toString方法,一定不要混淆其使用场景。可以参考一次简单却致命的错误。该文章说明了一次由于错误使用toString方法而带来的问题。