ITEEDU

12.4 只读类

尽管在一些特定的场合,由clone()产生的本地副本能够获得我们希望的结果,但程序员(方法的作者)不得不亲自禁止别名处理的副作用。假如想制作一个库,令其具有常规用途,但却不能担保它肯定能在正确的类中得以克隆,这时又该怎么办呢?更有可能的一种情况是,假如我们想让别名发挥积极的作用——禁止不必要的对象复制——但却不希望看到由此造成的副作用,那么又该如何处理呢?

一个办法是创建“不变对象”,令其从属于只读类。可定义一个特殊的类,使其中没有任何方法能造成对象内部状态的改变。在这样的一个类中,别名处理是没有问题的。因为我们只能读取内部状态,所以当多处代码都读取相同的对象时,不会出现任何副作用。

作为“不变对象”一个简单例子,Java的标准库包含了“封装器”(wrapper)类,可用于所有基本数据类型。大家可能已发现了这一点,如果想在一个象Vector(只采用Object句柄)这样的集合里保存一个int数值,可以将这个int封装到标准库的Integer类内部。如下所示:

//: ImmutableInteger.java
// The Integer class cannot be changed
import java.util.*;

public class ImmutableInteger {
  public static void main(String[] args) {
    Vector v = new Vector();
    for(int i = 0; i < 10; i++) 
      v.addElement(new Integer(i));
    // But how do you change the int
    // inside the Integer?
  }
} ///:~

Integer类(以及基本的“封装器”类)用简单的形式实现了“不变性”:它们没有提供可以修改对象的方法。

若确实需要一个容纳了基本数据类型的对象,并想对基本数据类型进行修改,就必须亲自创建它们。幸运的是,操作非常简单:

//: MutableInteger.java
// A changeable wrapper class
import java.util.*;

class IntValue { 
  int n;
  IntValue(int x) { n = x; }
  public String toString() { 
    return Integer.toString(n);
  }
}

public class MutableInteger {
  public static void main(String[] args) {
    Vector v = new Vector();
    for(int i = 0; i < 10; i++) 
      v.addElement(new IntValue(i));
    System.out.println(v);
    for(int i = 0; i < v.size(); i++)
      ((IntValue)v.elementAt(i)).n++;
    System.out.println(v);
  }
} ///:~

注意n在这里简化了我们的编码。

若默认的初始化为零已经足够(便不需要构建器),而且不用考虑把它打印出来(便不需要toString),那么IntValue甚至还能更加简单。如下所示:

class IntValue { int n; }

将元素取出来,再对其进行造型,这多少显得有些笨拙,但那是Vector的问题,不是IntValue的错。

12.4.1 创建只读类

完全可以创建自己的只读类,下面是个简单的例子:

//: Immutable1.java
// Objects that cannot be modified
// are immune to aliasing.

public class Immutable1 {
  private int data;
  public Immutable1(int initVal) {
    data = initVal;
  }
  public int read() { return data; }
  public boolean nonzero() { return data != 0; }
  public Immutable1 quadruple() {
    return new Immutable1(data * 4);
  }
  static void f(Immutable1 i1) {
    Immutable1 quad = i1.quadruple();
    System.out.println("i1 = " + i1.read());
    System.out.println("quad = " + quad.read());
  }
  public static void main(String[] args) {
    Immutable1 x = new Immutable1(47);
    System.out.println("x = " + x.read());
    f(x);
    System.out.println("x = " + x.read());
  }
} ///:~

所有数据都设为private,可以看到没有任何public方法对数据作出修改。事实上,确实需要修改一个对象的方法是quadruple(),但它的作用是新建一个Immutable1对象,初始对象则是原封未动的。

方法f()需要取得一个Immutable1对象,并对其采取不同的操作,而main()的输出显示出没有对x作任何修改。因此,x对象可别名处理许多次,不会造成任何伤害,因为根据Immutable1类的设计,它能保证对象不被改动。

12.4.2 “一成不变”的弊端

从表面看,不变类的建立似乎是一个好方案。但是,一旦真的需要那种新类型的一个修改的对象,就必须辛苦地进行新对象的创建工作,同时还有可能涉及更频繁的垃圾收集。对有些类来说,这个问题并不是很大。但对其他类来说(比如String类),这一方案的代价显得太高了。

为解决这个问题,我们可以创建一个“同志”类,并使其能够修改。以后只要涉及大量的修改工作,就可换为使用能修改的同志类。完事以后,再切换回不可变的类。

因此,上例可改成下面这个样子:

//: Immutable2.java
// A companion class for making changes
// to immutable objects.

class Mutable {
  private int data;
  public Mutable(int initVal) {
    data = initVal;
  }
  public Mutable add(int x) { 
    data += x;
    return this;
  }
  public Mutable multiply(int x) {
    data *= x;
    return this;
  }
  public Immutable2 makeImmutable2() {
    return new Immutable2(data);
  }
}

public class Immutable2 {
  private int data;
  public Immutable2(int initVal) {
    data = initVal;
  }
  public int read() { return data; }
  public boolean nonzero() { return data != 0; }
  public Immutable2 add(int x) { 
    return new Immutable2(data + x);
  }
  public Immutable2 multiply(int x) {
    return new Immutable2(data * x);
  }
  public Mutable makeMutable() {
    return new Mutable(data);
  }
  public static Immutable2 modify1(Immutable2 y){
    Immutable2 val = y.add(12);
    val = val.multiply(3);
    val = val.add(11);
    val = val.multiply(2);
    return val;
  }
  // This produces the same result:
  public static Immutable2 modify2(Immutable2 y){
    Mutable m = y.makeMutable();
    m.add(12).multiply(3).add(11).multiply(2);
    return m.makeImmutable2();
  }
  public static void main(String[] args) {
    Immutable2 i2 = new Immutable2(47);
    Immutable2 r1 = modify1(i2);
    Immutable2 r2 = modify2(i2);
    System.out.println("i2 = " + i2.read());
    System.out.println("r1 = " + r1.read());
    System.out.println("r2 = " + r2.read());
  }
} ///:~

和往常一样,Immutable2包含的方法保留了对象不可变的特征,只要涉及修改,就创建新的对象。完成这些操作的是add()和multiply()方法。同志类叫作Mutable,它也含有add()和multiply()方法。但这些方法能够修改Mutable对象,而不是新建一个。除此以外,Mutable的一个方法可用它的数据产生一个Immutable2对象,反之亦然。

两个静态方法modify1()和modify2()揭示出获得同样结果的两种不同方法。在modify1()中,所有工作都是在Immutable2类中完成的,我们可看到在进程中创建了四个新的Immutable2对象(而且每次重新分配了val,前一个对象就成为垃圾)。

在方法modify2()中,可看到它的第一个行动是获取Immutable2 y,然后从中生成一个Mutable(类似于前面对clone()的调用,但这一次创建了一个不同类型的对象)。随后,用Mutable对象进行大量修改操作,同时用不着新建许多对象。最后,它切换回Immutable2。在这里,我们只创建了两个新对象(Mutable和Immutable2的结果),而不是四个。

这一方法特别适合在下述场合应用:

  • (1) 需要不可变的对象,而且
  • (2) 经常需要进行大量修改,或者
  • (3) 创建新的不变对象代价太高