Effective Java 读书笔记——15:使可变性最小化

不可变类

不可变类是它的实例不能被修改的类。每个实例中所有的信息,必须在创建的时候提供,并在其整个对象周期内固定不变,例如:String,基本的包装类,BigInteger和BigDecimal。不可变的类更加的易于设计、实现和使用。它们不容易出错,而且更加安全。


为了使类成为不可变的,一般遵循以下几个原则:

  1. 不要提供任何会修改对象状态的方法(改变对象属性的方法,也称为mutator,也就是set方法)。
  2. 保证类不会被扩展。防止恶意的子类假装对象的状态已经改变,一般是将该类设置为final。
  3. 使所有的域都为final。通过系统的强制方法,清晰的表示你的意图。
  4. 使所有的域都为private。防止通过继承获取访问被域引用的可变对象的权限,实际上用final修饰的public域也足够满足这个条件,但是不建议这么做 ,为以后的版本的维护作考虑。
  5. 确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获取指向这些对象的引用。并且, 永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何方法(access)中返回该对象的引用。在构造器中和访问方法中,请使用保护性拷贝(defensive copy)。(保护性拷贝,比如在构造器中,需要传递某个对象进行初始化,那么初始化的时候不要使用这个对象的引用,因为外部是可以修改这个引用中的数据的。因此初始化的时候,应该使用这个引用中的数据重新初始化这个对象,可参考39条)
下面给出一个关于不可变类的例子:

// Immutable class - pages 76-78
package effectiveJava.Chapter4.Item15;

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 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);

	// Accessory 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.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 43 to find out why we use compare instead of ==
		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;
	}

	private int hashDouble(double val) {
		long longBits = Double.doubleToLongBits(re);
		return (int) (longBits ^ (longBits >>> 32));
	}

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

以上是一个关于复数的类,其中部分方法,如:加减乘除,可以返回新的对象,而不是修改当前的对象。很多不可变类都使用了这种方法,这是一种常见的函数式(functional)做法,因为 它们返回的是函数的结果,对对象进行运算的结果,而不改变这些对象。

特点


事实上,不可变对象非常简单,它只有一种状态, 创建时的状态。只要你在构造器中能保证这个类的约束关系,并遵守以上几条原则,那么在该对象的整个生命周期里,永远都不会再发生改变,维护人员也不需要过额外的时间来维护它。
另外,可变的对象拥有任意复杂的状态空间。如果文档中没有对其精确的描述,那么要可靠的使用一个可变类是非常困难的。
由于不可变对象本身的特点, 它本质上就是线程安全的,不需要对其进行同步。因为不可变对象的状态永远不发生改变,所以当多个线程同时访问这个对象的时候,对其不会有任何影响。基于这一点,不可变类应该尽量鼓励 重用现有的实例,而不是new一个新的实例。方法之一就是,对于频繁用到的值,使用public static final。如下:
	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);

不可变类还可以提供一些静态工厂(见第1条),将频繁请求的实例保存起来,所有基本类型的包装类和BigInteger都有这样的工厂。静态工厂还可以代替公有构造器,使得以后可以有添加缓存的灵活性。


永远不需要对不可变对象进行保护性拷贝,因为不可变对象内部的数据不可变,没有保护性拷贝的必要。

不可变对象唯一的缺点是, 对于每个不同的值,都需要一个单独的对象。如果是一个大型对象,那么创建这种对象的代价可能很高。如果你执行一个多步骤操作,然而除了最后的结果之外,其他的对象都被抛弃了,此时性能将会是一个比较大的问题。有两种常用的方法:
  1. 将多步操作作为一个安全的基本操作提供,这样就免除了中间的多步对象。
  2. 如果无法精准的预测客户端将会在不可变的类上执行那些复杂的多步操作,不如提供一个公有的可变配套类。StringBuilder就是一个很好的例子。

另一种方法


除了使类成为final这种方法之外,还有另外一种更加灵活的方法来实现不可变类。让类的所以构造器都变成私有的,并添加静态工厂来代替公有的构造器。

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);
	}...

这种方法最大的好处是它允许多个包级私有类的实现,并且在后续的维护中扩展静态工厂的功能。例如,你想添加一个通过极坐标生成复数的功能。如果通过构造器,可能会显得非常凌乱,但是只用添加第二个静态工厂即可。

	public static Complex valueOfPolar(double r, double theta) {
		return new Complex(r * Math.cos(theta), r * Math.sin(theta));
	}


总结

开头所提到的几个原则,其实强硬了,为了提高性能,往往有所放松。事实上应该是, 没有一个方法能够对对象的状态产生外部可见的改变(在初始化之后,自身还是可以改变自身内部的数据 ,许多不可变对象拥有一个或多个非final的域,在第一次请求的时候,将一些昂贵的计算结果缓存在这些域里,如果将来还有同样的计算,直接将缓存的数据返回即可。因为对象是不可变的,因此如果相同的计算再次执行,一定会返回相同的结果。例如,延迟初始化(lazy  initialization)就是一个很好的例子。

总之, 不要为每个get方法都写一个set方法,除非有必要。换句话说, 除非有很好的理由让类成为可变的类,否则就应该是不可变。尽管它存在一些性能问题,但是你总可以找到让一些较小的类成为不可变类。

构造器 应该创建完整的构造方法,并建立起所有的关系约束。不应该在构造器或者静态工厂外,再提供公有的初始化方法,或者是重新初始化方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值