Effective Java 第三版读书笔记——条款17:最小化可变性

不可变类是一个实例不能被修改的类。包含在每个实例中的所有信息在对象的生命周期中是固定的,因此不会观察到任何变化。Java 平台类库包含许多不可变的类,包括 String 类,基本类型包装类以及 BigInteger 类和 BigDecimal 类。使用不可变类有很多理由:不可变类比可变类更容易设计,实现和使用。 它们不太容易出错,更安全。

要使一个类不可变,请遵循以下五条规则:

  • 不要提供修改对象状态的方法(也称为mutators)。
  • 确保这个类不能被继承。这可以防止粗心的或恶意的子类破坏类的不可变行为。防止子类化通常是通过使用 final 关键字修饰类来实现,但是我们稍后将讨论另一种方法。
  • 把所有属性设置为final。通过系统强制执行来清楚地表达你的意图。另外,如果一个新创建的实例的引用从一个线程传递到另一个线程而没有同步,就必须保证正确的行为。
  • 把所有的属性设置为private。这可以防止客户端获得对属性引用的可变对象的访问权限并直接修改这些对象。虽然技术上允许不可变类拥有基本类型的公共 final 属性或对不可变对象的引用,但不建议这样做,因为它不允许在以后的版本中更改内部表示(条目 15 和 16)。
  • 确保对任何可变组件的排斥访问。如果你的类有任何引用可变对象的属性,请确保该类的客户端无法获得对这些对象的引用。

下面是一个不可变类的例子:

// Immutable complex number class
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) o;
		
        // See page 47 to find out why we use compare instead of ==
		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)";
	}
}

这个类代表了一个复数(一个包含实部和虚部的数)。除了标准的 Object 方法之外,它还为实部和虚部提供访问方法,并提供加减乘除四个基本运算。注意算术运算如何创建并返回一个新的 Complex 实例,而不是修改这个实例。这种模式被称为函数式(functional)方法,因为方法返回将函数应用于操作数的结果,而不修改它们。与其对应的过程(procedural)或命令(imperative)式方法将一个过程作用在操作数上,导致其状态改变。请注意,方法名称是介词(如 plus)而不是动词(如 add),这强调了方法不会改变对象的值的事实。

函数式方法具有许多优点:

  • 不可变对象很简单

  • 不可变对象本质上是线程安全的,它们不需要同步

  • 不可变对象可以被自由地共享。因此,不可变类应鼓励客户端尽可能重用现有的实例。一个简单的方法是为常用的值提供公共的静态 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类在内部使用符号数值表示法。符号用 int 值表示,数值用 int 数组表示。negate 方法生成了一个数值相同但符号相反的新的 BigInteger 实例,新创建的 BigInteger 对象指向与原始对象相同的内部数组。

不可变类的主要缺点是对于每个不同的值都需要一个单独的对象。创建这些对象可能代价很高,尤其是大型对象。如果执行一个多步操作,在每一步生成一个新对象,除最终结果之外丢弃所有对象,则性能问题会被放大。这里有两种方式来处理这个问题。第一种办法,先猜测一下会经常用到哪些多步的操作,然后将它们作为基本类型提供。如果一个多步操作是作为一个基本类型提供的,那么不可变类就不必在每一步创建一个独立的对象。在内部,不可变的类可以是任意灵活的。例如,BigInteger 有一个包级私有的可变的“伙伴类(companion class)”,它用来加速多步操作,比如模幂运算(modular exponentiation)。

如果你可以准确预测客户端要在你的不可变类上执行哪些复杂的操作,那么包级私有可变伙伴类的方式可以正常工作。如果不能的话,那么最好的办法就是提供一个公开的可变伙伴类。这种方法在 Java 平台类库中的主要例子是 String 类,它的可变伙伴类是 StringBuilder(及其过时的前身 StringBuffer 类)。

扫描二维码关注公众号,回复: 4995405 查看本文章

现在你已经知道如何创建一个不可改变类,并且了解了不变性的优点和缺点,下面我们来讨论几个设计方案。回想一下,为了保证不变性,一个类不允许被子类化。你可以用 final 修饰这个类来实现这个目的,但是还有另外一个更灵活的选择——使其所有的构造方法私有或包级私有,并添加公共静态工厂,而不是公共构造方法(条款 1)。为了具体说明这种方法,下面以 Complex 为例,看看如何使用这种方法:

// Immutable class with static factories instead of constructors
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 属性,在第一次需要时将开销昂贵的计算结果缓存在这些属性中。如果再次请求相同的值,则返回缓存的值,从而节省了重新计算的成本。这个技巧能正确工作恰恰是因为对象是不可变的,这保证了重复计算会得到相同的结果。例如,PhoneNumber 类的 hashCode 方法(条款 11)在第一次调用该方法时计算哈希码,并对其进行缓存以防它被再次调用。

总而言之,除非有充分的理由使类成为可变类,否则类应该是不可变的。如果一个类不能设计为不可变类,那么也要尽可能地限制它的可变性。除非有充分的理由不这样做,否则应该把每个属性声明为 private final 的

猜你喜欢

转载自blog.csdn.net/sky_asd/article/details/86505128