一、不可变类是什么?
不可变类只是其实例不能被修改的类。 每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。Java平台类库中包含许多不可变的类,其中有String、基本类型的包装类、BigInteger和BigDecimal。
二、使类成为不可变类,应遵循的五条规则
1、不要提供任何会修改对象状态(属性)的方法。
2、保证类不会被扩展。 为了防止子类化,一般做法是使这个类成为final的。这样可以防止粗心或者恶意的子类假装对象的状态已经改变,从而破坏该类的不可变行为。
3、使所有的域都是final的。 通过系统的强制方式,这可以清楚地表明你的意图。而且,如果一个指向新创建实例的引用在缺乏同步机制的情况下,从一个线程被传递到另一个 线程,就必须确保正确的行为。
4、使所有的域都成为私有的。 这样可以防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象。虽然从技术上讲,允许不可变的类具有公有的final 域,只要这些域包含基本类型的值或者指向不可变对象的引用,但是不建议这样做,因为这样会使得在以后的版本中无法再改变内部的表示法。
5、确保对于任何可变组件的互斥访问。 如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用初始化这样的域,也不要从任何访问方法中返回该对象引用。在构造器、访问方法和readObject中请使用保护性拷贝技术。
这些规则比真正的要求强硬了点,为了提高性能可以有所放松,事实上应该是这样:没有一个方法能够对对象的状态产生外部可见的改变。然而,许多不可变的类拥有一个或者多个非final的域,它们在第一次被请求执行这些计算的时候,把一些开销昂贵的类拥有一个或者多个非final的域,他们在第一次被请求执行这些计算的时候,把一些开销昂贵的计算结果缓存在这些域中。如果将来再次请求同样地计算,就直接返回这些缓存的值,从而解决了重新计算所需要的开销。这种技巧可以很好地工作,因为对象是不可变的,它的不可变性保证了这些计算如果被再次执行,就会产生同样地结果。
三、理解不可变类
例如:String类为什么是不可变类。
不可变对象:
String str = “testA”;
str = “testB”;
String的实例 "testA"的内容没有改变,只是改变了引用。
成员变量hash虽然没有用final声明,但是由于第一次调用hashCode()会重新计算hash值,并且以后调用会使用已缓存的值,当然最关键的是每次计算时都得到相同的结果,所以也保证了对象的不可变。
value是String封装的数组,value中的所有字符都是属于String这个对象的。由于value是private的,并且没有提供setValue等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改。此外,value变量是final的, 也就是说在String类内部,一旦这个值初始化了,value引用类型变量所引用的地址不会改变,即一直引用同一个对象。所以可以说String对象是不可变对象。
String的substring, replace, replaceAll, toLowerCase等方法都是返回了一个新的String对象,而不是改变了之前String实例。
书上的例子:
/**
* @description: 复数(具有实部和虚部)不是一个工业强度的复数实现
* @author: lty
* @create: 2021-05-19 10:48
**/
public 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,im*c.re+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 (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Complex complex = (Complex) o;
return Double.compare(complex.re, re) == 0 &&
Double.compare(complex.im, 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 "Complex{" +
"re=" + re +
", im=" + im +
'}';
}
}
代码中实现加减乘除时,都是返回一个新的Complex实例,而不是修改这个实例。这个被称为函数的做法,因为这些方法返回了一个函数的结果,这些函数对操作数进行运算但并不修改它。
四、不可变性的优点
1、不可变对象 比较简单,不可变对象只有一种状态,即被创建时的状态。
2、不可变对象本质上是线程安全的,它们不要求同步。
3、不可变对象可以被自由地共享。所以鼓励客户端尽可能重用现有的实例。
一个简单的方法就是对于频繁用到的值,为它们提供公有的静态final常量。
例如:
private static final Complex ZERO = new Complex(0,0);
这种方法进一步扩展就是不可变的类可以提供一些静态工厂把频繁被请求的实例缓存起来。
不可变对象永远也不需要进行保护性拷贝,所以不可变的类不需要提供clone方法或者拷贝构造器。
4、不仅可以共享不可变对象,甚至也可以共享它们的内部信息。
5、不可变对象为其他对象提供了大量的构件,无论是可变的还是不可变的对象
五、不可变性的缺点
唯一的缺点就是,对于每个不同的值都需要一个单独的对象。 有时候创建这种对象的代价可能很高。
如果你执行一个多步骤的操作,并且每个步骤都会产生一个新的对象,除了最后的结果之外其他的对象最终都会被丢弃,此时性能问题就会显露出来。
处理这种问题有两种办法。
第一种办法:
先猜测一下会经常用到哪些多步骤的操作,然后将他们作为基本类型提供。如果某个多步骤操作已经作为基本类型提供,不可变的类就可以不必在每个步骤单独创建一个对象。不可变的类在内部可以更加灵活。
第二种方法:
提供一个公有的可变配套类。在Java平台类库中,这种方法的主要例子是String类,它的可变配套类时StringBuilder(和基本上已经废弃的StringBuffer)。
如果能够精确地预测出客户端将要在不可变的类上执行哪些复杂的多阶段操作,这种包级私有的可变配套类的方法就可以工作的很好。如果不能预测,使用第二种方法。
为了确保不可变性,类绝对不允许自身被子类化。
六、如何让类自身被子类化
1、使类成为final的
2、让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂来代替公有的构造器。
七、有关序列化功能的一条告诫
如果你选择让自己的不可变类实现Serializable接口,并且它包含一个或者多个指向可变对象的域,就必须提供一个显式的readObject或者readResolve方法,或者使用ObjectOutputStream.writeunshared和ObjectInputStream.readUnshared方法,即使默认的序列化形式是可以接受的,也是如此。否则攻击者可能从不可变的类创建可变的实例。
八、总结
1、坚决不要为每个get方法编写一个相应的set方法。除非有很好地理由要让类成为可变的类,否则就应该是不可变的。
2、对于有些类而言,其不可变性是不切实际的。如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性。降低对象可以存在的状态数,可以更容易地分析该对象的行为,同时降低出错的可能性。因此,除非有令人信服的理由要使域变成是非final的,否则要使每个域都是final的。
3、构造器应该创建完全初始化的对象,并建立其所有的约束关系。不要在构造器或者静态工厂之外再提供公有的初始化方法,除非有令人信服的理由必须这么做。同样地,也不应该提供“重新初始化”方法(它使得对象可以被重用,就好像这个对象是由另一不同的初始状态构造出来一样)。与所增加的复杂性相比,“重新初始化”方法通常并没有带来太多的性能优势。