不可变类是实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。Java平台包含的不可变类:String、基本类型的包装类、BigInteger、BigDecimal。不可变类比可变类更容易设计、实现和使用。
为了使类成为不可变,要遵循五条规则:
1.不要提供任何会修改对象状态的方法。
2.保证类不会被扩展,为了防止恶意子类假装对象状态被改变,从而破坏不可变。
3.使所有的域都是final的。
4.使所有的域都成为私有的,防止客户端获得访问被域引用的可变对象的权限,防止客户端直接修改这些对象,从技术上讲,允许不可变的类具有公有的final域,只要这些域包含基本类型的值或者指向不可变对象的引用,但不建议这样做,因为会导致无法再以后的版本改变内部表示法。
5.确保对任何可变组件的互斥访问,如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象的引用。在构造器、访问方法和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 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; 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(val); return (int)(longBits ^ (longBits >>> 32)); } @Override public String toString() { char s = '-'; return "(" + re + " + " + im + "i)"; } }
加法、减法、乘法和除法四种基本的算术运算返回的是一个新的Complex实例,而不是修改这个实例。这样实例的状态就无法被改变(状态在实例创建的时候就确定了,并且没有提供改变状态的方法),Complex类成为一个不可变类。
不可变对象本质上是线程安全的,不要求同步。因为状态无法被修改,所以多线程并发访问这样的对象不会破坏它的不可变性。客户端应该尽可能地重用现有的实例。例如Complex类可能提供下面的常量:
public static final Complex ZERO = new Complex(0, 0);
进一步扩展,不可变类可以提供一些静态工厂,把频繁被请求的实例缓存起来,从而当现有实例可以符合请求时,不必创建新的实例:
private static final Complex ZERO = new Complex(0, 0); public Complex valueOfZero() { return ZERO; }
不可变对象可以被自由地共享导致,永远都不需要进行保护性拷贝,因为这些拷贝始终等于原始的对象,所以,不需要,也不应该为不可变类提供clone方法或者拷贝构造器。
不可变对象也可以共享它们的内部信息。如BigInteger类的内部使用了符号数值表示法。符号用一个int类型的值signum表示,数值则用一个int数组mag表示。它有一个negate方法产生一个新的BigInteger,其中数值是一样的,符号是相反的,它不需要拷贝数组,新建的BigInteger也指向原始实例中的同一个内部数组。
public BigInteger negate() { return new BigInteger(this.mag, -this.signum);//构造器传入的参数是原始实例的数组this.mag }
不可变对象真正唯一的缺点是,对于不同的值都需要一个单独的对象。考虑String的“+”,String s = “a”+“b”会创建三个字符串“a”、“b”、“ab”
让不可变类变成final,除了在类声明为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 Comolex valueOf(double re, double im) { return new Complex(re, im); } }
优势:如果希望提供一种基于“极坐标创建复数”的方式,使用这样的构造器,因为与Complex(double,double),参数类型一致,必要通过另外的方法在使得不会与原构造器冲突。通过静态工厂,很容易做到:
public static Complex valueOfPolar(double r, double theta) { return new Complex(r * Math.cos(theta), r * Math.sin(theta)); }
实际上,不可变对象所有域都必须是final的,过于严格,为了提高性能可以有所放松,比如String类有一个private int hash;域,当hashCode方法第一次被调用时,把结果放在hash域中,之后调用hashCode方法直接返回hash的值,减少了计算开销,有可能提高性能。因为String被设计成不可变类,所以每次可以确保hashCode计算的结果都是一致的。