10.1 促进不变性
函数式风格编程倾向于使用不变对象。一旦创建了不变对象,便无法修改其状态了。尽管Java有诸如String
、Class
和Integer
这样的不变类,但更为常见的是使用可变对象和命令查询分离(command query separation,参见附录A)。创建一个实例,调用改变对象属性的方法或修饰符修改对象的状态。让我们花点时间看看为什么可变对象不可取。
看看下面的Java类,Counter
类有一个字段,叫count
。这个字段可以通过getter和setter访问和修改。
//Java code
public class Counter {
private int count;
synchronized public int getCount() { return count; }
synchronized public void setCount(int value) { count = value; }
}
为了在多线程访问时保护count
,这里及时地将这两个方法同步(synchronized)起来。然而,这还不够。下面的代码就有很大的问题:
//Java code
int currentValue = counter.getCount();
counter.setCount(currentValue + 100);
假定Counter
实例由多个线程使用,每个线程都去像上面的例子那样执行操作。count
的值会完全不可预测。即便Counter
的两个方法都是同步的,在getCount()
调用和setCount()
调用之间,另一个线程也有可能得到监听器(monitor)或锁,对值进行修改。这是一个很容易掉进去的陷阱。为了线程安全,不得不把上面代码里的两个调用放在一个适当的同步块里。而且,在每一处使用Counter
的地方,都必须要进行检查,确保这件事做对了。这是一个很高的要求,对于任何一个有实际意义的应用而言,想要用可变对象写出线程安全的代码,即便是有可能做到,那也是极端困难的。这个简单的例子只是冰山一角。
不变对象从根源上解决了这个问题。因为没有状态改变,也就无需顾虑竞争。如果想改变,只要创建出另一个不变对象的实例即可。对于不习惯函数式编程的人而言,这或许有些不适应。不过,逐渐适应这种风格之后,你就会意识到,你已经不再需要绞尽脑汁解决线程安全问题了。不变对象提供的优势如下所述。
它们天生就是线程安全的。因为无法修改其状态,所以,可以自由地在线程间传递,而无需顾虑竞争。而且,也没有必要对它们进行同步。
因为没有复杂的状态转换,它们很简单,用起来也很容易。
它们可以在应用间共享和重用,这有助于减轻应用资源的负担。比如,在
Flyweight
模式里③,不变对象用来共享几个对象公用的数据。
③参见Gamma等人的Design Patterns: Elements of Reusable Object-Oriented Software [GHJV95]里的
Flyweight
模式。
- 它们不易出错。因为不会随意修改对象状态,也就少了一些需要处理的错误。同使用可变对象相比,使用不变对象的代码更容易验证其正确性。
即便是纯Java代码,Joshua Bloch在Effective Java [Blo01]中也推荐“将可变性降到最低”,尽可能让类不变。
Scala的并发模型依赖于不变性。Scala期望我们把不变对象当作消息在actor间传递。在本章余下的部分,我们会学到,同Java提供的并发API相比,Scala对于并发的支持是多么的轻量级且简单易用。