文章目录

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

为了使一个类成为非可变类,要遵循下面五条规则:
1.不要提供任何会修改对象的方法。

2.保证没有任何可被子类改写的方法。这可以防止粗心或者恶意的子类破坏该类的不可变行为。为了禁止改写方法,一般做法是使这个类成为final的,但是后面我们还会讨论到其他的做法。

3.使所有的域都是final的。通过系统的强制方式,可以清楚地表明你的意图。而且,如果一个指向新创建的实例的引用在缺乏同步机制的情况下,被从一个线程传递到另一个线程,那么有可能还必须要保证正确的行为,这取决于正在重新设计中的内存模型的情况。

4.使所有的域都成为私有的。这样可以防止客户直接修改域中的信息。虽然非可变类可以具有公有的final域,只要这些域包含原语类型的值或者指向非可变对象的引用,从技术上讲这样是允许的,但是,这样做不值得提倡,因为这使得在以后的版本中不可能再改变内部表示。

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

前面条目中的许多例子都是非可变的,针对每一个属性它都有一个访问方法,但是没有对应的改变函数。

package com.zjucs.test;

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

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

public float realPart() {
return re;
}

public float 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 mutiply(Complex c) {
return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
}

public Complex divide(Complex c) {
float 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);
}

public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Complex))
return false;
Complex c = (Complex) o;
return (Float.floatToIntBits(re) == Float.floatToIntBits(c.re))
&& (Float.floatToIntBits(im) == Float.floatToIntBits(c.im));
}

public int hashCode() {
int result = 17 + Float.floatToIntBits(re);
result = 37 * result + Float.floatToIntBits(im);
return result;
}

public String toString() {
return "(" + re + "+" + im + "j)";
}
}

这个类表示一个复。大多数重要的非可变类都使用了这种模式。它被称为函数的做法,因为这些方法返回了一个函数的结果,这些函数对操作数进行运算但并不修改它。与之相对应的更常见的是过程做法,在这种方式下,方法内部有一个过程作用在它们的操作数上使得它的状态发生了变化。

非可变对象比较简单。一个非可变对象可以只有一个状态,即最初被创建时刻的状态。如果你能够被确保所有的构造函数都建立了这个类的约束关系,那么可以保证这些约束关系在整个生命周期内永远不再发生变化,无需你和使用这类的程序员再做额外的工作来维护这些约束关系。相反,可变对象可以有任意复杂的状态空间。如果文档中没有对mutator方法所执行的状态转换提供精确地描述,那么,要可靠地使用一个可变类是非常困难的,甚至是不可能的。

非可变对象本质上是线程安全地,它们不要求同步。当多个线程并发访问这样的对象时,它们不会被破坏。这无疑是获得线程安全最容易的办法。实际上,没有一个线程会注意到其他线程对于一个非可变对象所施加的影响。所以,非可变对象可以被自由地共享。非可变类应该充分利用这种优势,鼓励客户尽可能地重用已有的实例。要做到这一点,一个很简便的办法是,对于频繁被用到的值,为它们提供公有的静态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和Boolean类都有这样的静态工厂。使用这样的静态工厂也使得客户之间可以共享已有的实例,而不用创建新的实例,从而降低内存占用和垃圾回收的代价。

“非可变对象可以被自由地共享”导致的一个结果是,你永远也不需要进行保护性拷贝。实际上,你根本不需要做任何拷贝,因为这些拷贝始终等于原始的对象。因此,这些拷贝始终等于原始的对象。因此,你不需要,也不应该为非可变类提供clone方法或者拷贝构造函数。这一点在Java平台早期出现的时候并不好理解,所以String类仍然提供给了一个拷贝构造函数,但是它应该很少被使用。

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

非可变对象为其他对象——无论是可变的还是非可变的——提供了大量的构件。如果一个复杂对象内部的组件对象不会改变的话,那么要维护它的不可变性约束是很容易的。这条原则的一种特殊情形是,非可变对象构成了大量的映射键和集合的不变约束,但是你不用担心它们的值会发生变化。

非可变类真正的唯一缺点是,对于每一个不同的值都要求有一个单独的对象。创建这样的对象可能代价很高,特别是对于大型对象的情形。例如,假设你有一个上百万位的BigInteger,希望对它地位求反:

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

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

如果你执行一个多步骤的操作,并且每个步骤都会产生一个新的对象,除了最后的结果之外的其他对象最终都会被丢弃,那么此时性能问题就会显露出来,处理这种问题有两种办法。第一种办法,先测一下哪些多步骤操作会经常被用到,然后把它们作为基本单元来提供,如果一个多步骤操作已经成为一个基本单元了,那么非可变类就可以不必在每个步骤上创建一个独立对象了。非可变类可以在内部做的非常灵活。

除了”使一个类成为final”的这种方法之外,还可以有其他两种办法做到这一点。一种办法是,让这个类的每一个方法都成为final的,而不是让整个类成为final单独。这种方法的唯一好处在于,它使得程序员可以扩展这个类,在原来基础上增加新的方法。这种做法其效果与“在一个独立的、不可被实例化的工具类中增加新的静态方法。”相同,所以并不提倡。

“使一个非可变类为final”的第二种替代做法是,使其所有的构造函数成为私有的,或者包级私有的,并且增加公有的静态工厂,来代替公有的构造函数。为了具体说明,下面以Complex为例,

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

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

public static Complex valueOf(float re,float im)
{

return new Complex(re,im);
}
}

虽然这种方法并不常用,但是它往往是三种可选择的做法中最好的一种。它最灵活,因为它允许使用多个包级私有的实现类。对于包外面的客户而言,非可变类实际上是final的,因为要扩展这样一个来自其他包的类是不可能的,它缺少一个公有的或者受保护的构造函数。除了允许多个实现类的灵活性之外,这种做法也使得很有可能在后续的发行版本中改进这个类的性能,具体做法是增强静态工厂的对象缓存能力。

静态工厂相比构造函数具有许多其他的优势,加入,假设你希望Complex类提供一种“基于极坐标创建复数对象”的方式。如果使用构造函数来实现这样的功能,可能会使这个类很零乱。因为这样的构造函数与已有的构造函数Complex(float,float)具有同样的原型特征。但是通过静态工厂,这样很容易做到:只需用第二个静态工厂,并且工厂名字清楚地表明它的功能即可:

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

当BigInteger和BigDecimal最初被编写出来的时候,“为什么非可变类必须要有final的等同效果”还没有被普遍理解,所以它们的所有方法都很有可能会被改写。不幸的是,为了保持向上兼容,这个问题一直无法得以改正。如果你当前正在编写一个类,它的安全性依赖于BigInteger或者BigDecimal实参的非可变性,那么,你必须检查,以确定这个实参是不是一个“真正的“BigInteger或者BigDecimal,而不是一个不可信子类的实例。如果是后者的话,你必须在”假设它可能是可变的“前提下对它进行保护性拷贝:

public void foo(BigInteger b){
if(b.getClass()!=BigInteger.class)
b=new BigInteger(b.toByteArray());
}

总而言之,坚决不要为每个get方法编写一个相应的set方法。除非有很好的理由要让一个类成为非可变类,否则就应该是非可变的。非可变类有许多与优点,唯一的缺点是在特定的情况下存在潜在的性能问题。

对于有些类而言,非可变类是不切实际的,这样的类包括”过程类”,例如Thread和TimeTask。如果一个类不能被做成非可变类,那么你仍然应该尽可能地限制它的可变性。降低一个对象中存在的状态的数目,可以更容易地分析该对象的行为,同时降低出错的可能性。因此,构造函数不应该把”只构造了一部分实例”传递给其他的方法。你不应该在构造函数之外再提供一个”重新初始化”犯法。与所增加的复杂性相比,”重新初始化“方法通常并没有带来太多的优势性能。

文章目录