软件构造 7:可变性与不变性

本文探讨了可变性与不变性在软件构造中的影响,指出可变对象带来的风险,如意外修改、难以理解和维护的代码。文章通过实例展示了传入和返回可变对象可能导致的问题,并建议使用不可变类型以提高代码的安全性、可读性和可维护性。不可变类型如String和一些集合类的不可变视图提供了安全的选择,但需要注意不可变对象的别名问题。此外,文章还强调了正确指定和理解方法规格说明的重要性,以及如何利用不可变对象优化性能。
摘要由CSDN通过智能技术生成

可变性

回忆之前我们讨论过的“用快照图理解值与对象”(译者注:“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");

所以这有什么关系呢?在上面这两个例子中,我们最终都让ssb索引到了"ab" 。当对象的索引只有一个时,它们两确实没什么去呗。但是当有别的索引指向同一个对象时,它们的行为会大不相同。例如,当另一个变量t指向s对应的对象,tb指向sb对应的对象,这个时候对ttb做更改就会导致不同的结果:

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与性能。

但是使用者可能会对结果很惊奇,例如:


        
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值