回忆之前我们讨论过的“用快照图理解值与对象”(译者注:“Java基础”),有一些对象的内容是不变的(immutable):一旦它们被创建,它们总是表示相同的值。另一些对象是可变的(mutable):它们有改变内部值对应的方法。
String
就是不变对象的一个例子,一个String
对象总是表示相同的字符串。而StringBuilder
则是可变的,它有对应的方法来删除、插入、替换字符串内部的字符,等等。
因为 String
是不变的,一旦被创建,一个 String
对象总是有一样的值。为了在一个 String
对象字符串后加上另一个字符串,你必须创建一个新的 String
对象:
String s = "a";
s = s.concat("b"); // s+="b" and s=s+"b" also mean the same thing
与此相对, StringBuilder
对象是可变的。这个类有对应的方法来改变对象,而不是返回一个新的对象:
StringBuilder sb = new StringBuilder("a");
sb.append("b");
所以这有什么关系呢?在上面这两个例子中,我们最终都让s
和sb
索引到了"ab"
。当对象的索引只有一个时,它们两确实没什么去呗。但是当有别的索引指向同一个对象时,它们的行为会大不相同。例如,当另一个变量t
指向s
对应的对象,tb
指向sb
对应的对象,这个时候对t
和tb
做更改就会导致不同的结果:
String t = s;
t = t + "c";
StringBuilder tb = sb;
tb.append("c");
可以看到,改变t
并没有对s
产生影响,但是改变tb
确实影响到了sb
——这可能会让编程者惊讶一下(如果他没有注意的话)。这也是下面我们会重点讨论的问题。
既然我们已经有了不变的 String
类,为什么还要使用可变的 StringBuilder
类呢?一个常见的使用环境就是当你要同时创建大量的字符串,例如:
String s = "";
for (int i = 0; i < n; ++i) {
s = s + i;
}
如果使用不变的字符串,这会发生很多“暂时拷贝”——第一个字符“0”实际上就被拷贝了n次,第二个字符被拷贝了n-1次,等等。总的来说,它会花费O(N^2)的时间来做拷贝,即使最终我们的字符串只有n个字符。
StringBuilder
的设计就是为了最小化这样的拷贝,它使用了简单但是聪明的内部结构避免了做任何拷贝(除非到了极限情况)。如果你使用StringBuilder
,可以在最后用 toString()
方法得到一个String
的结果:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; ++i) {
sb.append(String.valueOf(i));
}
String s = sb.toString();
优化性能是我们使用可变对象的原因之一。另一个原因是为了分享:程序中的两个地方的代码可以通过共享一个数据结构进行交流。
可变性带来的风险
可变的类型看起来比不可变类型强大的多。如果你在“数据类型商场”购物,为什么要选择“无聊的”不可变类型而放弃强大的可变类型呢?例如 StringBuilder
应该可以做任何 String
可以做的事情,加上 set()
和 append()
这些功能。
答案是使用不可变类型要比可变类型安全的多,同时也会让代码更易懂、更具备可改动性。可变性会使得别人很难知道你的代码在干吗,也更难制定开发规定(例如规格说明)。这里举出了两个例子:
#1: 传入可变对象
下面这个方法将列表中的整数相加求和:
/** @return the sum of the numbers in the list */
public static int sum(List<Integer> list) {
int sum = 0;
for (int x : list)
sum += x;
return sum;
}
假设现在我们要创建另外一个方法,这个方法将列表中数的绝对值相加,根据DRY原则(Don’t Repeat Yourself),实现者写了一个利用 sum()
的方法:
/** @return the sum of the absolute values of the numbers in the list */
public static int sumAbsolute(List<Integer> list) {
// let's reuse sum(), because DRY, so first we take absolute values
for (int i = 0; i < list.size(); ++i)
list.set(i, Math.abs(list.get(i)));
return sum(list);
}
注意到这个方法直接改变了数组 —— 这对实现者来说很合理,因为利用一个已经存在的列表会更有效率。如果这个列表有几百万个元素,那么你节省内存的同时也节省了大量时间。所以实现者的理由很充分:DRY与性能。
但是使用者可能会对结果很惊奇,例如: