一. 什么是不可变类?
不可变类是指,一旦一个类的对象被创建出来,在其整个生命周期中,它的成员变量就不能被修改。如说String、BigInteger、BigDecimal。
二. 优缺点
-
优点
直接复用
。就像String对象池那样,相同的对象只需要创建一个,这样可以极大地节省空间。可以
共享
。不可变对象本质上是线程安全的,多个线程并发访问同一个对象不会造成任何线程安全性问题。不可变类可以很方便地作为其它不可变类的成员。
-
缺点
对于每个不同的值都需要一个单独的对象。即使是String类也存在这样的问题,但也有了解决方案,当频繁改变字符串时使用StringBuilder代替String。
三. 如何设计一个不可变类?
- 不要提供任何会修改对象状态的方法(也称为mutator)。
- 保证类不会被扩展。否则恶意的子类有可能改变类的不可变性,比如在子类方法中修改域指向的可变对象。
- 使所有的域都是final的。
- 使所有的域都成为私有的。
- 确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须保证客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象引用。在需要接受或返回引用的地方,使用保护性拷贝技术。
四. 代码实现
实现一个Period类,该类用来表示一段不可变的时间周期。
public final class Period {
private final Date start;
private final Date end;
/**
* @param start the beginning of the period
* @param end the end of the period; must not precede start
* @throws IllegalArgumentException if start is after end
* @throws NullPointerException if start or end is null
*/
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(start + "after" + end);
}
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
}
在Period中,我们只提供了一个构造方法和两个访问方法,并没有提供任何会修改对象状态的方法,因此满足第一条规则。
通过在类名称前添加final关键字,保证了该类不会被扩展,满足第二条规则。
所有的域都是final并且私有的,满足三四条规则。
最关键的地方在于我们是如何满足第五条规则的。由于Date是一个可变类,因此在构造方法中,并没有直接把用户传入的Date对象赋值给私有域,而是各拷贝了一份相同的对象再赋值给私有域。同样的,在访问方法start和end中,也没有直接把私有域指向的对象返回给客户端,而是各拷贝了一份副本返回。这种做法就叫做保护性拷贝,客户端永远无法拿到不可变对象私有域的引用,而只能拿到相应的副本,因此也就无法改变不可变对象。
这段程序还有几处需要注意的地方:
- 构造方法中对参数的有效性检查一定要在保护性拷贝之后,否则可能受到TOCTOU攻击(Time-Of-Check/Time-Of-Use)。假如先执行有效性检查再保护性拷贝,那么恶意客户端有可能在另一个线程中试图改变参数的值,一旦恶意客户端这一行为恰好发生在有效性检查之后和保护性拷贝之前,那么不可变类的有效性检查被绕过。
- 不要使用clone方法进行保护性拷贝。因为恶意客户端传入的Date类型参数可能是子类化的,该子类化类型很可能覆写了clone方法,从而执行恶意代码。
其实,Java API的设计者早已把这些规则应用到了String等不可变类上,大家可以查看String类的源码,它的构造方法、静态工厂方法以及其它很多方法都使用了保护性拷贝技术。