每日一问:为啥要使可变性最小化?
前面举了个例子,就像有些小区可以随时进入,保安看到也不会问你是不是小区里的,有的小区会问你是不是业主道理一样的。你猜猜看可访问性越小好还是越大好?
不可变类是指其实例不能被修改的类 。 每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期( lifetime )内固定不变 。Java 平台类库中包含许多不可变的类,其中有 String 、基本类型的包装类、Biginteger 和 BigDecimal 。 存在不可变的类有许多理由:不可变的类比可变类更加易于设计、实现和使用 。 它们不容易出错,且更加安全 。
为了使类成为不可变,要遵循下面五条规则:
1 不要提供任何会修改对象状态的方法 (也称为设值方法) 。
2 保证类不会被扩展 。 这样可以防止粗心或者恶意的子类假装对象的状态已经改变,从而破坏该类的不可变行为 。 为了防止子类化,一般做法是声明这个类成为 final 的 ,但是后面我们还会讨论到其他的做法 。
3 声明所有的域都是 final 的 。 通过系统的强制方式可以清楚地表明你的意图 。 而且,如果一个指向新创建实例的引用在缺乏同步机制的情况下,从一个线程被传递到另一个线程,就必须确保正确的行为,正如内存模型中所述。
4 声明所有的域都为私有的 。 这样可以防止客户端获得访问被域引用的可变对象的权限,井防止客户端直接修改这些对象 。 虽然从技术上讲,允许不可变的类具有公有的 final域,只要这些域包含基本类型的值或者指向不可变对象的引用,但是不建议这样做,因为这样会使得在以后的版本中无法再改变内部的表示法。
类似:
public class Person {
private int age;
private String name;
}
5 确保对于任何可变组件的互斥访问 。 如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。 并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法( accessor )中返回该对象引用 。 在构造器、访问方法和 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 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;
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)";
}
}
验证:
public class TestDemo {
public static void main(String[] args) {
Complex complex = new Complex(4, 1);
Complex plus = complex.plus(complex);
Complex minus = complex.minus(complex);
Complex times = complex.times(complex);
Complex divided = complex.dividedBy(complex);
System.out.println(complex);
System.out.println(plus);
System.out.println(minus);
System.out.println(times);
System.out.println(divided);
}
}
结果:
(4.0 + 1.0i)
(8.0 + 2.0i)
(0.0 + 0.0i)
(15.0 + 8.0i)
(1.0 + 0.0i)
Process finished with exit code 0
注意这些算术运算如何创建井返回新的 Complex 实例,而不是修改这个实例 。这句话什么意思?
就是每次计算的结果不是上个结果继续玩往下计算,而是每次计算都是从最开始的数值重新计算所得的。
不可变对象本质上是线程安全的,它们不要求同步 。 当多个线程并发访问这样的对象时它们不会遭到破坏 。 这无疑是获得线程安全最容易的办法 。 实际上,没有任何线程会注意到其他线程对于不可变对象的影响 。 所以, 不可变对象可以被自由地共享 。 不可变类应该充分利用这种优势,鼓励客户端尽可能地重用现有的实例 。 要做到这一点,一个很简便的办法就是 : 对于频繁用到的值,为它们提供公有的静态 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 也指向原始实例中的同一个内部数组 。
不可变对象为其他对象提供了大量的构件,无论是可变的还是不可变的对象 。 如果知道一个复杂对象内部的组件对象不会改变,要维护它的不变性约束是比较容易的 。 这条原则的一种特例在于,不可变对象构成了大量的映射键( map key )和集合元素( set element ); 一旦不可变对象进入到映射( map )或者集合( set )中,尽管这破坏了映射或者集合的不变性约束,但是也不用担心它们的值会发生变化 。
不可变对象无偿地提供了失败的原子性 。 它们的状态永远不变,因此不存在临时不一致的可能性 。
不可变类真正唯一的缺点是 , 对于每个不同的值都需要一个单独的对象 。 创建这些对象的代价可能很高,特别是大型的对象 。 例如,假设你有一个上百万位的 Biginteger,想要改变它的低位:
Bigintege moby = ...;
moby = moby.flipBit(0);
还有另外一种更加灵活的办法可以做到这一点 。 不可变的类变成 final 的另 一种办法就是,让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂( static factory )来代替公有的构造器(详见第 1 条) 。 为了具体说明这种方法,下面以 Complex 为例,看看如何使用这种方法:
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
}
运行:
Complex complex1 = Complex.valueOf(4, 1);
System.out.println("complex1"+complex1);
结果:结果与上面一样。
complex1(4.0 + 1.0i)
Process finished with exit code 0
本条开头关于不可变类的诸多规则指出没有方法会修改对象,并且它的所有域都必须是 final 的 。 实际上,这些规则比真正的要求更强硬了一点,为了提高性能可以有所放松 。 事实上应该是这样:没有一个方法能够对对象的状态产生外部可见( externally visib le )的改变 。然而,许多不可变的类拥有一个或者多个非 fina l 的域 , 它们在第一次被请求执行这些计算的时候,把一些开销昂贵的计算结果缓存在这些域中 。 如果将来再次请求同样的计算,就直接返回这些缓存的值,从而节约了重新计算所需要的开销 。 这种技巧可以很好地工作,因为对象是不可变的,它的不可变性保证了这些计算如果被再次执行,就会产生同样的结果 。
总之,坚决不要为每个 get 方法编写一个相应的 set 方法 。 除非有很好的理由要让类成为可变的类 ,否则它就应该是不可变的 。 不可变的类有许多优点,唯一的缺点是在特定的情况下存在潜在的性能问题 。 你应该总是使 一些小的值对象。 你也应该认真考虑把一些较大的值对象做成不可变的,例如 String 和 Biginteger 。 只有当你确认有必要实现令人满意的性能时,才应该为不可变的类提供公有的可变配套类 。
对于某些类而言,其不可变性是不切实际的 。 如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性 。 降低对象可以存在的状态数,可以更容易地分析该对象的行为,同时降低出错的可能性 。 因此,除非有令人信服的理由使域变成非 final 的 ,否则让每个域都是final的 。 结合这条的建议和第 15 条的建议,你自然倾向于 : 除非有令人信服的理由要使域变成是非 final的,否则要使每个域都是 private final 的 。
构造器应该创建完全初始化的对象 ,并建立起所有的约束关系 。 不要在构造器或者静态工厂之外再提供公有的初始化方法,除非有令人信服的理由必须这么做 。 同样地,也不应该提供“重新初始化”方法(它使得对象可以被重用,就好像这个对象是由另一不同的初始状态构造出来的一样) 。 与所增加的复杂性相比,“重新初始化”方法通常并没有带来太多的性能优势 。
通过 CountDownLatch 类的例子可以说明这些原则 。 它是可变的,但是它的状态空间被有意地设计得非常小 。 比如创建一个实例,只使用一次,它的任务就完成了 : 一旦定时器的计数达到零,就不能重用了 。
最后值得注意的一点与本条目中的 Complex 类有关 。 这个例子只是被用来演示不可变性的,它不是一个工业强度的复数实现 。 它对复数乘法和除法使用标准的计算公式,会进行不正确的四舍五入,并且对复数 NaN 和无穷大也没有提供很好的语义。
所有文章无条件开放,顺手点个赞不为过吧!