为什么 StringBuffer 有 toStringCache 而 StringBuilder 没有?
对于 StringBuilder 和 StringBuffer 的源码会发现,StringBuffer 中有一个叫 toStringCache 的成员变量,用来缓存 toString() 方法返回字符串对应的字符数组,而在 StringBuilder 里面却没有。
先看一下代码。
StringBuffer 中的 toString() :
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
StringBuilder 中的 toString() :
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
StringBuffer 调用了 String(char[] value, boolean share) 来构造一个 String 对象,它传入了一个字符数组,也就是缓存 toStringCache。Java 中定义,一个 String 对象是常量,是不能被改变的,因此 StringBuffer 的 toString() 返回的对象也应该是不能被改变。也就意味着传入的 toStringCache 数组的元素的值也不能被改变,否则由它构造的 String 对象就会改变。
如下,我们通过反射调用 String(char[] value, boolean share) 构造处一个字符串对象,然后修改 value 数组的值,会发现字符串对象发生了改变。
class ToStringCacheTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<String> constructor = String.class.getDeclaredConstructor(char[].class, boolean.class);
constructor.setAccessible(true);
char[] value = new char[]{'R', 'o', 'b', 'o', 't', 'h', 'y'};
String str = constructor.newInstance(value, true);
System.out.println(str); // 输出:Robothy
value[0] = 'A'; // 修改 str 绑定的字符数组 value
System.out.println(str); // 输出:Aobothy
}
}
StringBuffer 中的 toStringCache 是字符数组 value 复制的一个副本,每当 value 发生改变时,toStringCache 都会被置为空。这就保证了每次只要 StringBuffer 对象发生改变,再调用 toString() 方法就必然产生一个新的 toStringCache 数组,从而保证了引用了旧的 toStringCache 的字符串对象不会发生改变。即使多个线程同时访问 StringBuffer 对象,某一时刻也只有一个线程能够进入修改 toStringCache 和 value 的代码块,这通过修饰 StringBuffer 方法的 synchroinzed 关键字来保证。
如 StringBuffer 中的 append(String str) 方法:
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
而假设 StringBuilder 为了提高效率,专门设计为单线程环境使用,方法没有 synchronized 修饰。如果也和 StringBuffer 一样这么做,非常容易出现不一致的情况,使得已经产生的 String 对象发生改变。
那么 StringBuffer 中 toStringCache 存在的必要性如何?它调用的是下面这个构造方法来创建 String 对象,构造 String 对象时直接共享传入的字符数组 value,而不是像 public String(char value[]) 一样复制一份。
/*
* 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;
}
从源码中可以知道,StringBuffer 中使用 toStringCache 通过共享一个字符数组,提供构造 String 的速度,这是一个好处。另一个好处是连续多次调用 toString() 方法是不会产生多个内容相同的 String 对象。
但是,这些好处仅仅是在多次调用 toString() 方法且 StringBuffer 对象没有发生改变时才能体现。而实际编写代码的过程中,很少会在没有修改 StringBuffer 的情况下重复调用 toString() 方法,所以它并没有太大的实际作用。
之所以它存在 JDK 源码里,有人解释是由于历史代码遗留的原因,现在不修改是因为它没有什么坏处,修改了反而需要重新测试代码。
参考
stackoverflow.com, Why StringBuffer has a toStringCache while StringBuilder not?