Effective Java笔记(17)使可变性最小化

        不可变类是指其实例不能被修改的类 。 每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期( lifetime )内固定不变 。Java 平台类库中包含许多不可变的类,其中有 String 、基本类型的包装类、Biginteger 和 BigDecimal 。 存在不可变的类有许多理由:不可变的类比可变类更加易于设计、实现和使用 。 它们不容易出错,且更加安全 。

为了使类成为不可变,要遵循下面五条规则:

  • 不要提供任何会修改对象状态的方法 (也称为设值方法) 。
  • 保证类不会被扩展 。 这样可以防止粗心或者恶意的子类假装对象的状态已经改变,从而破坏该类的不可变行为 。 为了防止子类化,一般做法是声明这个类成为 final 的 ,但是后面我们还会讨论到其他的做法 。
  • 声明所有的域都是 final 的 。 通过系统的强制方式可以清楚地表明你的意图 。 而且,如果一个指向新创建实例的引用在缺乏同步机制的情况下,从一个线程被传递到另一个线程,就必须确保正确的行为,正如内存模型中所述。
  • 声明所有的域都为私有的 。 这样可以防止客户端获得访问被域引用的可变对象的权限,井防止客户端直接修改这些对象 。 虽然从技术上讲,允许不可变的类具有公有的 final域,只要这些域包含基本类型的值或者指向不可变对象的引用,但是不建议这样做,因为这样会使得在以后的版本中无法再改变内部的表示法。
  • 确保对于任何可变组件的互斥访问 。 如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。 并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法( accessor )中返回该对象引用 。 在构造器、访问方法和 readObject 方法中请使用保护性拷贝技术 。

        下面是个稍微复杂一点的例子:

public final class Complex
{
{
    private final double re;
    private final double im;

    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    public double realPart() {
        return re;
    }

    public double imaginaryPart() {
        return im;
    }

    public Complex plus(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }

    public Complex minus(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }

    public Complex times(Complex c) {
        return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
    }

    public Complex dividedBy(Complex c) {
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex((re * c.re + im * c.im) / tmp, (im * c.re - re * c.im) / tmp);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Complex))
            return false;
        Complex c = (Complex) 0;

        return Double.compare(c.re, re) == 0 && Double.compare(c.im, im) == 0;
    }

    @Override
    public int hashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }

    @Override
    public String toString() {
        return "(" + re + " + " + im + "i)";
    }
}

        这个类表示一个复数( complex number ,具有实部和虚部) 。 除了标准的 Object 方法之外,它还提供了针对实部和虚部的访问方法,以及 4 种基本的算术运算 : 加法、减法、乘法和除法 。 注意这些算术运算如何创建井返回新的 Complex 实例,而不是修改这个实例 。大多数重要的不可变类都使用了这种模式 。 它被称为函数的( functional )方法,因为这些方法返回了一个函数的结果,这些函数对操作数进行运算但并不修改它 。 与之相对应的更常见的是过程的(procedural)或者命令式的( imperative )方法,使用这些方法时,将一个过程作用在它们的操作数上, 会导致它的状态发生改变 。 注意,这些方法名称都是介词(如plus ),而不是动词(如 add ) 。 这是为了强调该方法不会改变对象的值 。Biginteger 类和 BigDecimal 类由于没有遵守这一命名习惯,就导致了许多用法错误 。

        如果你对函数方式的做法还不太熟悉,可能会觉得它显得不太自然 ,但是它带来了不可变性,具有许多优点 。 不可变对象比较简单 。 不可变对象可以只有一种状态,即被创建时的状态 。 如果你能够确保所有的构造器都建立了这个类的约束关系,就可 以确保这些约束关系在整个生命周期内永远不再发生变化,你和使用这个类的程序员都无须再做额外的工作来维护这些约束关系 。 另一方面,可变的对象可以有任意复杂的状态空间 。 如果文档中没有为设值方法所执行的状态转换提供精确的描述,要可靠地使用可变类是非常困难的,甚至是不可能的 。

        不可变对象本质上是线程安全的,它们不要求同步 。 当多个线程并发访问这样的对象时它们不会遭到破坏 。 这无疑是获得线程安全最容易的办法 。 实际上,没有任何线程会注意到其他线程对于不可变对象的影响 。 所以, 不可变对象可以被自由地共享 。 不可变类应该充分利用这种优势,鼓励客户端尽可能地重用现有的实例 。 要做到这一点,一个很简便的办法就是 : 对于频繁用到的值,为它们提供公有的静态 final 常量 。 例如,Complex 类有可能会提供下面的常量 :

public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I= new Complex(0, 1);

        这种方法可以被进一步扩展 。 不可变的类可以提供一些静态工厂,它们把频繁被请求的实例缓存起来 ,从而当现有实例可以符合请求的时候,就不必创建新的实例 。 所有基本类型的包装类和 Biginteger 都有这样的静态工厂 。 使用这样的静态工厂也使得客户端之间可以共享现有的实例,而不用创建新的实例,从而降低内存占用和垃圾回收的成本 。 在设计新的类时,选择用静态工厂代替公有的构造器可以让你以后有添加缓存的灵活性,而不必影响客户端 。

        “不可变对象可以被自由地共享”导致的结果是,永远也不需要进行保护性拷贝( defensive copy)。 实际上,你根本无须做任何拷贝 ,因为这些拷贝始终等于原始的对象 。 因此,你不需要,也不应该为不可变的类提供 clone 方法或者拷贝构造器。 这一点在 Java 平台的早期并不好理解,所以 String类仍然具有拷贝构造器,但是应该尽量少用它 。

        不仅可以共享不可变对象,甚至也可以共享它们的内部信息 。 例如,Biginteger 类内部使用了符号数值表示法。符号用一个 int 类型的值来表示,数值则用一个 int 数组表示 。negate 方法产生一个新的 Biginteger ,其中数值是一样的,符号则是相反的 。 它并不需要拷贝数组,新建的Biginteger 也指向原始实例中的同一个内部数组 。

        不可变对象为其他对象提供了大量的构件,无论是可变的还是不可变的对象 。 如果知道一个复杂对象内部的组件对象不会改变,要维护它的不变性约束是比较容易的 。 这条原则的一种特例在于,不可变对象构成了大量的映射键( map key )和集合元素( set element ); 一旦不可变对象进入到映射( map )或者集合( set )中,尽管这破坏了映射或者集合的不变性约束,但是也不用担心它们的值会发生变化 。

        不可变对象无偿地提供了失败的原子性 。 它们的状态永远不变,因此不存在临时不一致的可能性 。

        不可变类真正唯一的缺点是 , 对于每个不同的值都需要一个单独的对象 。 创建这些对象的代价可能很高,特别是大型的对象 。 例如,假设你有一个上百万位的 Biginteger,想要改变它的低位:

Bigintege moby = ...;
moby = moby.flipBit(0);

        flipBit 方法创建了一个新的 Biginteger 实例,也有上百万位长,它与原来的对象只差一位不同 。 这项操作所消耗的时间和空间与 Biginteger 的成正比 。 我们拿它与java.util.BitSet 进行比较 。 与 Biginteger 类似,BitSet 代表一个任意长度的位序列,但是与 Biginteger 不同的是,BitSet 是可变的 。BitSet 类提供了一个方法,允许在固定时间( constant time )内改变此“百万位”实例中单个位的状态:

B·itSet moby = ... ;
moby.flip(0);

        如果你执行一个多步骤的操作,并且每个步骤都会产生一个新的对象 ,除了最后的结果之外,其他的对象最终都会被丢弃,此时性能问题就会显露出来 。 处理这种问题有两种办法 。 第一种办法,先猜测一下经常会用到哪些多步骤的操作,然后将它们作为基本类型提供 。 如果某个多步骤操作已经作为基本类型提供,不可变的类就无须在每个步骤单独创建一个对象 。 不可变的类在内部可以更加灵活 。 例如, Biginteger 有一个包级私有的可变“配套类”( companing class ),它的用途是加速诸如“模指数”( mo dular exponentiation )这样的多步骤操作 。由于前面提到的诸多原因,使用可变的配套类比使用 Biginteger 要困难得多,但幸运的是,你并不需要这样做 。 因为 Biginteger 的实现者已经替你完成了所有的困难工作 。

        如果能够精确地预测出客户端将要在不可变的类上执行哪些复杂的多阶段操作,这种包级私有的可变配套类的方法就可以工作得很好 。 如果无法预测,最好的办法是提供一个公有的可变配套类 。 在 Java 平台类库中,这种方法的主要例子是 String 类,它的可变配套类是 StringBuilder (及其己经被废弃的祖先 StringBuffer ) 。

        现在你已经知道了如何构建不可变的类,并且了解了不可变性的优点和缺点,现在我们来讨论其他的一些设计方案 。 前面提到过,为了确保不可变性,类绝对不允许自身被子类化 。 除了“使类成为 final 的”这种方法之外,还有另外一种更加灵活的办法可以做到这一点 。 不可变的类变成 final 的另 一种办法就是,让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂( static factory )来代替公有的构造器(详见第 1 条) 。 为了具体说明这种方法,下面以 Complex 为例,看看如何使用这种方法:

public class Complex {
    private final double re;
    private final double im;
    private Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
    public static Complex valueOf(double re, double im) {
        return new Complex(re, im);
    }
    ...// Remainder unchanged
}

        这种方法虽然并不常用,但它通常是最好的替代方法 。 它最灵活,因为它允许使用多个包级私有的实现类 。 对于处在包外部的客户端而言,不可变的类实际上是 final 的,因为不可能对来自另一个包的类 、 缺少公有的或受保护的构造器的类进行扩展 。 除了允许多个实现类的灵活性之外,这种方法还使得有可能通过改善静态工厂的对象缓存能力,在后续的发行版本中改进该类的性能 。

        本条开头关于不可变类的诸多规则指出没有方法会修改对象,并且它的所有域都必须是 final 的 。 实际上,这些规则比真正的要求更强硬了一点,为了提高性能可以有所放松 。 事实上应该是这样:没有一个方法能够对对象的状态产生外部可见( externally visib le )的改变 。然而,许多不可变的类拥有一个或者多个非 fina l 的域 , 它们在第一次被请求执行这些计算的时候,把一些开销昂贵的计算结果缓存在这些域中 。 如果将来再次请求同样的计算,就直接返回这些缓存的值,从而节约了重新计算所需要的开销 。 这种技巧可以很好地工作,因为对象是不可变的,它的不可变性保证了这些计算如果被再次执行,就会产生同样的结果 。

        总之,坚决不要为每个 get 方法编写一个相应的 set 方法 。 除非有很好的理由要让类成为可变的类 ,否则它就应该是不可变的 。 不可变的类有许多优点,唯一的缺点是在特定的情况下存在潜在的性能问题 。 你应该总是使 一些小的值对象。 你也应该认真考虑把一些较大的值对象做成不可变的,例如 String 和 Biginteger 。 只有当你确认有必要实现令人满意的性能时,才应该为不可变的类提供公有的可变配套类 。

        对于某些类而言,其不可变性是不切实际的 。 如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性 。 降低对象可以存在的状态数,可以更容易地分析该对象的行为,同时降低出错的可能性 。 因此,除非有令人信服的理由使域变成非 final 的 ,否则让每个域都是final的 。 结合这条的建议和第 15 条的建议,你自然倾向于 : 除非有令人信服的理由要使域变成是非 final的,否则要使每个域都是 private final 的

        构造器应该创建完全初始化的对象 ,并建立起所有的约束关系 。 不要在构造器或者静态工厂之外再提供公有的初始化方法,除非有令人信服的理由必须这么做 。 同样地,也不应该提供“重新初始化”方法(它使得对象可以被重用,就好像这个对象是由另一不同的初始状态构造出来的一样) 。 与所增加的复杂性相比,“重新初始化”方法通常并没有带来太多的性能优势 。

        通过 CountDownLatch 类的例子可以说明这些原则 。 它是可变的,但是它的状态空间被有意地设计得非常小 。 比如创建一个实例,只使用一次,它的任务就完成了 : 一旦定时器的计数达到零,就不能重用了 。

        最后值得注意的一点与本条目中的 Complex 类有关 。 这个例子只是被用来演示不可变性的,它不是一个工业强度的复数实现 。 它对复数乘法和除法使用标准的计算公式,会进行不正确的四舍五入,并且对复数 NaN 和无穷大也没有提供很好的语义。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值