Android进阶;关于Java中的不可变性

1 场景

在面向对象编程里,不变性是一个有点冷门的话题,一般在考察String特性的面试题中比较常见,但其实不变性是一个非常有用的设计。
例如:
某个通用的配置对象,只在初始化时赋值一次,之后不能被修改,如何避免被同事调用和修改?
某个为多线程同时提供数据的只读对象,如何确保只读,也就是避免被同事修改?
...

2 分析

要在Java中实现不变性的对象,主要是确保它禁止修改,但完全写死的对象实用性很低,还是需要为对象赋值一次,这就可以分解为两个问题:

2.1 禁止修改

1-私有化方法和字段
setter和public字段当然是禁止的,这样在普通语法层面上屏蔽修改入口。
2-使用final字段
final修饰符的直接作用是从编译器级别就锁死引用,引用不可变更,否则编译器会报错。
final可以用于修饰类、方法和字段,分别作用如下:
final类—禁止继承,把类锁死。
final方法—禁止重写,在编译期即绑定具体方法。
final字段—禁止修改,在类加载时即把final字段放进常量池。

所以,需要使用final字段,声明该字段为不可变。
3-小心里式替换
里式替换本身是六大设计原则之一,但是在不可变对象中,这是一个非常危险的性质,因为一个继承自基类的子类实例,可以轻松替换掉基类实例,从而破坏不可变性。

解决方法就是给类加上final修饰,禁止继承。
4-杜绝变量逃逸/逸出
变量逸出是一个很容易忽视的点,如果外部能够拿到内部字段的引用,或者在赋值时能够传入一个引用,就可以通过修改引用去修改内部字段,这也是不允许的。

解决方法就是,涉及外部引用或赋值时,一律使用深拷贝,如:

public String(char value[]) {
     this.value = Arrays.copyOf(value, value.length); // 深拷贝赋值
 }

2.2 赋值一次

不可变对象一般是需要赋值一次的,完全写死的不可变对象意味着扩展性很差,只能通过改代码来修改,不具备实用性。
但是,采用了final修饰符的字段,只能在三种情况下得到值:
1-常量;
2-代码块;
3-构造函数赋值;
前两种情况其实就是代码写死的方式,所以我们只能在构造函数中实现赋值一次。

3 实现

根据上文分析,要实现不可变对象,需要做到以下几点:
1.代码中禁用setter和public字段;
2.使用final修饰类和字段;
3.在构造函数中赋值,注意要使用深拷贝赋值;

经过这样的操作,就可以实现一个比较安全的不可变对象了。

4 漏洞

为什么说比较安全呢,因为还有一些漏洞可以破坏不可变性。
1- 反射
final字段虽然实现了不可变,但那是常规操作,如果使用反射,还是可以操作final字段的。
不过,如果final字段在代码中定义为常量,就是安全的,因为编译器会优化getter函数,直接返回定义的这个常量,而不是final字段。
当然,这种情况下,final字段就不能通过构造函数赋值了。
2- 序列化
如果不可变对象实现了序列化,通过反序列化也可以得到一个对象的拷贝。
我们知道序列化可以破坏单例,因为他会创建一个新的内存对象及其引用。
不过序列化对于不可变性还是比较友好的,因为反序列化得到的新内存对象与原始对象内容一致,在只读时没有不良影响。


 

猜你喜欢

转载自blog.csdn.net/feiyu1947/article/details/86633398