为什么对象应该是不可变的
在面向对象的编程领域中,一个对象如果在创建后,它的状态不能改变,那么我们就认为这个对象是不可变的(Immutable)。
在Java中,String这个不可变对象就是个很好的例子。一旦创建String对象后,我们不能对它的状态进行改变。我们可以创建新的String对象,但是不能改变原有的String对象。
然而,在JDK中有不可变对象只是很少的一部分。类似Date这样的类,我们能够通过调用setTime()方法改变它的状态。
我不清楚为什么JDK的设计者把如此相似的两个对象采取截然相反的实现方式。然而,我认为Date作为一个可变对象有很多缺陷。与此同时,不可变的String更能体现面向对象编程的本质。
更进一步,我认为在一个纯面向对象的世界里,所有的类都应该是不可变的。然而,有时会因为JVM的限制很难实现这一点。但不管怎么说,我们都应该尽全力做到最好。
下面几点是支持对象不可变性的一些理由:
- 不可变对象更容易构造、测试与使用。
- 真正的不可变对象都是线程安全的。
- 不可变对象可以避免耦合。
- 不可变对象的使用没有副作用(没有保护性拷贝)。
- 对象变化的问题得到了避免。
- 不可变对象的失败都是原子性的。
- 不可变对象更容易缓存。
- 不可变对象可以避免空值(NULL)引用,这通常是很糟糕的
线程安全
不可变对象最重要的特征是线程安全。这意味着多个线程能够在同时访问同一个对象,而且不需要担心与其他线程产生冲突。
如果对象的方法都不能改变对象的状态,那么不管有多少个对象,不管它们被并行调用的频率——不可变对象运行在自己的堆栈中。
Goetz等人在他们一本非常有名的书Java Concurrency in Pratice中更加细致的讨论了不可变对象的优势,强烈推荐大家去看。
避免时间上的耦合
下面给出一个时间上耦合的例子(下面的代码发送两个连续的 HTTP POST请求,第二个有HTTP body):
这段代码可以工作。但是,第一个方法必须在第二个方法之前调用,如果我们把第一个方法注释掉(也就是去掉第二行与第三行),编译器不会报任何错误:
现在,这段代码虽然没有编译错误,但仍然失效了。这就是所谓的时间上的耦合——总是有些隐藏信息需要程序员去记住。在这个例子中,我们必须记着在使用第二个方法前,需要调用第一个方法。
我们必须记住第二个方法必须与第一个方法一起使用,并且是在第一个方法之后使用。
如果Request对象不可变,第一个代码片段也是不对的,很有可能是下面这个样子:
这下这两个方法就没有耦合了,我们可以很放心的去掉第一个方法。你也许会说上面的代码有重复,确实是有。但是我们可以改成这样:
这样一来,我们重构后的代码也是正确的,而且没有了时间上的耦合。第一个请求可以在不影响第一个请求的情况下取消掉。
我希望这个例子能够向你展示操作不可变对象是更可读且可维护的,因为它没有时间上的耦合。
避免副作用
让我们在一个新方法中使用Request对象现在它是可变的了):
下面让我们发送两个请求——第一个用GET方法,第二个用POST方法:
这样代码就安全了,而且没有副作用。
避免身份可变性(Identity Mutability)
通常而言,对于内部状态相同的对象,我们认为它们是相同的。Date 类就是这方面一个很好的例子:
这里有两个对象,但是由于它们的内部状态是一样的,所以我们认为它们是相同的。可以通过重写它们的equals()与hashCode()方法实现。
这种便捷的方式的后果是:当我们在处理可变对象时,一旦我们改变了它们的内部状态,那么也就改变了它们的身份。
这也许看起来很自然,但是如果我们把可变对象作为Map的key时,情况就不一样了:
当我们改变date的状态时,我们不希望改变它的身份。我们不想仅仅因为改变了key的状态就失去了这个条目。但是上面的例子确实会发生丢失条目的问题。
当我们向map中添加一个对象时,这个对象的hashCode()会返回一个值。HashMap根据这个值来决定当前条目在内部哈希表的位置。当我们调用containsKey()方法时,由于对象的hashcode不一样了(因为 hashcode 依赖于内部状态),所以HashMap在内部的哈希表中找不到相应条目了。
这是个非常烦人的问题,而且很难去调试可变对象的副作用而产生的问题。不可变对象就能从根本上避免这个问题了。
原子性失败
下面是个简单的例子:
很明显,如果程序因为溢出而导致抛出异常时,Stack 对象就会处于一种不健康的状态。它的size属性会增加,但是items中并不包含新元素。
不可变性可以避免这个问题,因为一个不可变对象只能在构造时改变状态。构造函数要么失败,这样就不会初始化这个对象;要么成功,这时才会构造一个合法可靠且的对象。因为这时对象的内部属性不会再发生改变了。
如果想了解更多关于这方面的内容,可以参考Joshua Bloch写的Effective Java, 2nd Edition
反对不可变性的论据
下面是一些反对不可变性的争论:
- “不可变性不适合企业级项目”。通常,我会听人说到不可变性是个假想的特征,在真正的企业级项目中并不适用。作为一个反对这个争论的人,我可以仅仅列举出下面一些例子,它们都是真实的应用,并且使用到了不可变的Java对象:jcabi-http,jcabi-xml,jcabi-github,jcabi-s3,jcabi-dynamo,jcabi-simpledb。 上面的这些Java库都使用了不可变对象。netbout.com与stateful.co是两个使用了不可变对象实现的Web 应用程序。
- “更新一个已有对象的状态比创建一个新对象的成本要低”。Oracle认为“对象创建的成本往往被高估了,而且,不可变对象带来的便利可以抵消掉创建对象时的开销,因为垃圾回收机制能够减少开销,同时,我们可以不用去写专门防止可变对象出错的代码了。”我同意这种说法。