第15条:使可变性最小

术语:

不可变类:实例不能变修改的类。



        不可变类比可变类更加易于设计、实现和使用,它们不容易出错,且更加安全。

        为了使类成不为不可变量,需要遵循以下五条原则:

        1、不要提供任何会修改对象状态的方法。(也称为mutator) 。

        2、保证类不会被扩展。这样可以防止精心或者恶意的子类假装对象的状态已经改变,从而破坏该类的不可变行为。为了防止子类化,一般的做法就使这个类成为final的。

        3、使所的有域都是final的。通过系统的强制方式,这可以清楚的表明你的意图。而且如果一个指向新创建实例的引用在缺乏同步机制的情况下,从一个线程被传到另一个线程就必需确保正确的行为。

        4、使所有的域都是私有的。这样可以防止客户端获得访问被域引用的可变对象的权限。并防止客户端直接修改这些对象。虽然从技术上来讲,允许不可变的类具有仅有的final域,只要这些域包含基本类型的值或者指向不可变的对象引用。但是不建议这样做,因为这样会便得在以后的版本中无法再改变内部的表示法,因为已经把它暴露给了客户端。

        5、确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象的引用。在构造器、访问方法和readObject方法中申请使用保护性拷贝(defensive copy)技术。

        针对上面的原则,看如下的例子:

package EffectiveJava;

public final class Complex {
	private final double re;
	private final double im;
	
	public Complex(double re, double im) {
		this.re = re;
		this.im = im;
	}
	
	// Accessors with no corresponding mutators
	public double realPart() { return re; }
	public double imaginaryPart() { return im; }
	
	public Complex add(Complex c) {
		return new Complex(re + c.re, im + c.im);
	}
	
	public Complex subtract(Complex c) {
		return new Complex(re - c.re, im - c.im);
	}
	
	public Complex multiply(Complex c) {
		return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
	}
	
	public Complex divide(Complex c) {
		double tmp = c.re * c.re + c.im * c.rm;
		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) o;
		
		return Double.compare(re, c.re) == 0 &&
			   Double.compare(im, c.im) == 0;
	}
	
	@Override
	public int hashCode() {
		int result = 17 + hashDouble(re);
		result = 31 * result + hashDouble(im);
		return result;
	}
	
	@Override
	public String toString() {
		return "(" + re + " + " + im + "i)";
	}
	
	private int hashDouble(double val) {
		long longBits = Double.doubleToLongBits(val);
		return (int)(longBits ^ (longBits >>> 32));
	} 
}
        (上面的代码其中的hashDouble方法中,原书在求longBits传入的参数为re,根据上下文判断应该是笔误,已修改。)

        这个表示复数的类遵循了上述规则,它实现了四则运算,这些自述运算返回新的Complex实例而不修改当前实例。大多数重要的不可变类都使用这种模式,它被称为函数式的(functional)做法,这些函数对操作数进行运算但是不修改它。与之相应的更常见的是过程式的(procedural)或者命令式的(imperative)做法,使用这些方式时,将一个过程作用在它们的操作数上,会导致它们的状态发生改变。    

        不可变对象比较简单,它只有一种状态,即被创建时的状态。如果能保证所有的构造器都建立了这个类的约束关系,就可以确保这些约束关系在整个生命周期内永远不再发生变化,另一方面,可变的对象可以有任意复杂的状态空间,如果文档中没有对mutator方法所执行的状态转换提供精确的描述,要可靠地使用一个可变类是非常困难的,甚至是不可能的。

        不可变对象本质上是线程安全的,它们不要求同步。因为它们不可改变,所以不可变对象可以被自由的共享。不可变类应该充分利用这种优势,鼓励客户端尽可能地重用现有的实例。要做到这一点,一个很简便的办法就是,对于频繁用到的值,为它们提供仅有的静态final常量(但是使域为仅有的就失去了改变内部数据表示形式的灵活性)。这种方法可以被进一步扩展。不可变类可以提供一些静态工厂,它们把被频繁请求的实例缓存起来,从而当现有实例可以符合请求要求的时候,就不必创建新的实例了。在设计新的类时,选择用静态工厂代替仅有的构造器可以使以后添加缓存具有灵活性,而不必影响到客户端。不可变对象不需要进行保护性拷贝,也不应该为不可变对象提供clone方法或者拷贝构造器。

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

/**
     * Returns a BigInteger whose value is {@code (-this)}.
     *
     * @return {@code -this}
     */
    public BigInteger negate() {
        return new BigInteger(this.mag, -this.signum);
    }
其中mag声名了包级私有的final的int型数组。由此看,mag是被原BigInteger和新生成的BigInteger所共享的。

        不可变对象为其他对象提供了大量构件(building blocks)。无论是可变的还是不可变的对象。如果知道一个复杂对象的内部的组件对象不会改变,要维护它的不变性约束是比较容易的。这条原则的一种特殊在于:不可变对象构成了大量是的映射键(map key)和集合元素(set element),一旦不可变对象进入到映射或者集合中,就不必担心它们的值会发生变化。而这种变化会破坏映射或者集合的不变性约束。(原文如下:A special case of this principle is that immutable objects make great map keys and set elements: you don’t have to worry about their values changing once they’re in the map or set, which would destroy the map or set’s invariants.中文翻译有些问题,已修正)

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

BigInteger moby = ...;
moby = moby.flipBit(0);
        这会返回一个新的BigInteger实例,但它与原来的实例只差一位。这项操作所消耗的时间和空间与原BigInteger的成正比,显然浪费了资源。与BigInteger类似,BitSet代表一个任意长度的位序列,但是与bigInteger不同的是,BitSet是可变的。BitSet类提供了方法,允许在常量时间(constant time)内改变实例中某个单位的状态。以flip方法为例:

    /**
     * Sets the bit at the specified index to the complement of its
     * current value.
     *
     * @param  bitIndex the index of the bit to flip
     * @throws IndexOutOfBoundsException if the specified index is negative
     * @since  1.4
     */
    public void flip(int bitIndex) {
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);

        int wordIndex = wordIndex(bitIndex);
        expandTo(wordIndex);

        words[wordIndex] ^= (1L << bitIndex);

        recalculateWordsInUse();
        checkInvariants();
    }
        其中words被声明为private但非final的long数组,所以可以直接改变数据域。对比BigInteger节省了不必要的开销。如果执行一个多步骤的操作,并且每个步骤都会产生一个新的对象,除了最后的结果之外的其他对象最终都会被丢弃,此时性能问题就会显露出来。处理这种问题有两种办法:第一种,先猜测一下会经常用到哪些多步骤的操作,然后将它们作为基本类型提供。如果某个多步骤操作已经作为基本类型提供,不可变的类就可以不必在每个步骤单独创建一个对象。例如BigInteger有一个包级私有的可变“配套类(companing class)”--MutableBigInteger,它的用途是加速诸如"模指数(modular exponentiation)"这样的多步骤操作。第二种,如果无法预测这些步骤,那么最好的办法就是提供一个公有的可变配套类。在Java平台类库中,这种方法的主要例子是String类,它的可变配套类是StringBuilder(和StringBuffer)。

        前面说,为了确保类的不变性,类绝对不允许自身被子类化,除了“使类成为final”之种方法外,还可以让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂方法来代替公有构造器。这种方法是最好的替代方法,它最灵活,因为它允许使用多个包级私有的实现类。对于处在它的包外部的客户端而言,洒中变的类实际上上final的,因为不可能把来自另一个包的类、缺少公有的或者受保护的构造器的类进行扩展,除了允许多个实现类的灵活性外,这种方法还便利很有可能通过改变静态工厂的缓存能力,在后续的版本中改进该类的性能。

        但是,BigInteger和BigDecimal并不是final的,如果编写的类安全性信赖于(来自不可信客户端的)BigInteger或者BigDecimal的不可变性,就必须进行检查以确定这个参数是否是真正的BigInteger或者BigDecimal,而不是不可信任的子类实例,如果是来自不可信任的子类的实例,那就必须在假设它可能是可变的前提下对它进行保护性拷贝。

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

        如果选择让自己的不可变类实现Serializable接口,并且它包含一个或者多个指向可变对象的域,就必须提供一个显式的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshared和ObjectInputStream.readUnshared方法。否则攻击者可能从不可变的类创建可变的实例。

        坚决不要为每个get方法编写一个相应的set方法,除非有很好的理由让类成为可变的类,否则应该是不可变的。应该总是使一些小的值对象成为不可变的,也应该考虑把一些较大的值对象做成不可变的,只有当确认有必要实现令人满意的性能时,才应该为不可变的类提供公有的可变配套类。对于有些类而言,其不可变性是不切实际的,如要类不能做成是不可变的,仍应该尽可能地限制它的可变性。降低对象可以存在的状态数,可以更容易的分析对象的行为,同时降低出错的可能性。

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

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值