不可变类

阅读经典——《Effective Java》06

不可变类是指实例不能被修改的类。每个实例中包含的信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。你也许时常听说,String就是一个典型的不可变类。

  1. 不可变类的优点
  1. 需要遵循的规则

不可变类的优点

让我们思考一下,如果一个类的实例从创建后就不能再修改,它有什么好处?

最容易想到的是,它用起来简单。它不会提供过多的方法,大概只会提供一些访问方法,这对于用户是件非常愉快的事情。

其次,当一个类不可变的时候,我们就可以复用它。就像String对象池那样,相同的对象只需要创建一个,这样可以极大地节省空间。

不仅可以复用,还可以共享。不可变对象本质上是线程安全的,多个线程并发访问同一个对象不会造成任何线程安全性问题。

最后,不可变类可以很方便地作为其它不可变类的成员。这将在后面的叙述中给出原因。

不可变类唯一的缺点是,对于每个不同的值都需要一个单独的对象。如果对象很大,对它做一点点的修改就重新生成另一个对象,在性能上是无法接受的。即使是String类也存在这样的问题,但好在Java平台给出了解决方案,当频繁改变字符串时使用StringBuilder代替String,可以获得较好的性能。

需要遵循的规则

要想使一个类成为不可变类,需要遵循下面五条规则:

  1. 不要提供任何会修改对象状态的方法(也称为mutator)。
  1. 保证类不会被扩展。否则恶意的子类有可能改变类的不可变性,比如在子类方法中修改域指向的可变对象。
  2. 使所有的域都是final的。
  3. 使所有的域都成为私有的。
  4. 确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须保证客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象引用。在需要接受或返回引用的地方,使用保护性拷贝技术。

让我们根据这些规则实现一个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对象赋值给私有域,而是各拷贝了一份相同的对象再赋值给私有域。同样的,在访问方法startend中,也没有直接把私有域指向的对象返回给客户端,而是各拷贝了一份副本返回。这种做法就叫做保护性拷贝,客户端永远无法拿到不可变对象私有域的引用,而只能拿到相应的副本,因此也就无法改变不可变对象。

这段程序还有几处需要注意的地方:

  • 构造方法中对参数的有效性检查一定要在保护性拷贝之后,否则可能受到TOCTOU攻击(Time-Of-Check/Time-Of-Use)。假如先执行有效性检查再保护性拷贝,那么恶意客户端有可能在另一个线程中试图改变参数的值,一旦恶意客户端这一行为恰好发生在有效性检查之后和保护性拷贝之前,那么不可变类的有效性检查被绕过。
  • 不要使用clone方法进行保护性拷贝。因为恶意客户端传入的Date类型参数可能是子类化的,该子类化类型很可能覆写了clone方法,从而执行恶意代码。

其实,Java API的设计者早已把这些规则应用到了String等不可变类上,大家可以查看String类的源码,它的构造方法、静态工厂方法以及其它很多方法都使用了保护性拷贝技术。

关注作者或文集《Effective Java》,第一时间获取最新发布文章。

你可能感兴趣的:(不可变类)