Java 不可变类与不变模式
Java 中的不可变类
不可变类:当创建了这个类的实例后,就不允许修改它的属性值。在 JDK 的基本类库中,所有基本类型的包装类,如 Integer
和 Long
类,都是不可变类。String
也是不可变类。
用户在创建自己的不可变类时,可以考虑采用以下设计模式:
- 把属性定义为
private final
类型。 - 不对外公开用于修改属性的
setXXX()
方法 - 只对外公开用于读取属性的
getXXX()
方法 - 在构造方法中初始化所有属性。
- 覆盖
Object
类的equals()
和hashCode()
方法,在equals()
方法中根据对象的属性值来比较两个对象是否相等,并且保证用equals()
方法判断为相等的两个对象的hashCode()
方法的返回值也相等,这可以保证这些对象能正确地放到HashMap
或HashSet
集合中。 - 如果需要的话,提供实例缓存和静态工厂方法,允许用户根据特定参数获得与之匹配的实例。
String
类是不可变的String
类由final
修饰,说明其不可继承。- 字符数组
char value[]
是由private final
修饰的,保证引用地址不可变。(不保证数组内容不可变) String
类中,没有公开修改内部成员字段的方法。- 读取
value
数组时,采用了保护性拷贝Arrays.copyof()
实际上,可以通过反射修改 value
属性。
不可变类的实例在整个生命周期中永远保持初始化的状态,它没有任何状态变化,简化了与其他对象之间的关系。不可变类具有以下优点:
- 不可变类能使程序更加安全,不容易出错;
- 不可变类是线程安全的,当多个线程访问不可变类的同一个实例时,无须进行线程的同步。
把不可变类实例属性的 final
修饰符去除,增加 public
类型的 set
方法就变成了可变类。使用可变类更容易使程序代码出错。因为随意改变一个可变类对象的状态,有可能会导致与之关联的其他对象的状态被错误地改变。
由此可见,应该优先考虑把类设计为不可变类,假使必须使用可变类,也应该把可变类的尽可能多的属性设计为不可变的,即用 final
修饰符来修饰,并且不对外公开用于改变这些属性的方法。
在创建不可变类时,假如它的属性所属的类是可变类,在必要的情况下,必须提供保护性复制,否则,这个不可变类的实例的属性仍然有可能被错误地修改。即使可变类属性已经用 final
修饰,也必须提供保护性复制。
保护性复制是一种防御性编程技术。
例子:
1 | public final class Schedule { |
假如某个类中被 final
修饰的属性所属的类是不可变类,就无须提供保护性复制,因为该属性所引用的实例的值永远不会被改变,这进一步体现了不可变类的优点。
具有实例缓存的不可变类
推荐阅读:站内文章享元模式:共享实例 中 Java 的
Integer
和String
带缓存的不可变类的实现。
不可变类的实例的状态不会变化,这样的实例可以安全地被其他与之关联的对象共享,还可以安全地被多个线程共享。为了节省内存空间,优化程序的性能,应该尽可能地重用不可变类的实例,避免重复创建具有相同属性值的不可变类的实例。
缓存并没有固定的实现方式,完善的缓存实现不仅要考虑何时把实例加入缓存,还要考虑何时把不再使用的实例从缓存中及时清除,以保证有效合理地利用内存空间。一种简单的实现是直接用 Java 集合来作为实例缓存。
另外要注意的是,没有必要为所有的不可变类提供实例缓存。随意创建大量实例缓存,反而会浪费内存空间,降低程序的运行性能。通常,只有满足以下条件的不可变类才需要实例缓存:
- 不可变类的实例的数量有限。
- 在程序运行过程中,需要频繁访问不可变类的一些特定实例。这些实例拥有与程序本身同样长的生命周期。
不变模式 Immutable
不变模式:一个对象的状态在对象创建之后不再改变。涉及的 Java 类即不变类(Immutable Class),如果是对象则为不变对象(Immutable Object)。
不变模式分为两类:
- 普通不变模式:对象中包含的引用对象是可以改变的。如果不特别说明,通常我们所说的不变模式,指的就是普通的不变模式。
- 深度不变模式:对象包含的引用对象也不可变
它们之间的关系类似于 站内文章深拷贝和浅拷贝 之间的关系。
不变模式使用到不可变类,而不可变类不存在线程问题,因此不变模式常用于多线程场景。所以,不变模式也常被归类为多线程设计模式。
不变集合
不变集合是一种特殊的不变类。
Google Guava 针对集合类 Collection
、List
、Set
、Map
等提供了对应的不变集合类 ImmutableCollection
、ImmutableList
、ImmutableSet
、ImmutableMap
……
Google Guava 提供的不变集合类属于普通不变模式,集合中的对象不会增删,但是对象的成员变量是可以改变的。
JDK 中也提供了类似的集合,只不过它提供的是一个集合的「不可修改的视图」,而不是不变集合。当原始集合被修改后,Collections.unmodifiableXXX
里面的元素会跟着发生变化。
1 | public static void main(String[] args) { |
JDK 类库中提供的 unmodifiableXXX
方法存在以下不足:
- 笨拙:代码量多;
- 不安全:只有在不会引用到原来的集合情况下,才能保证集合唯一且永恒不变;
- 效率很低:返回的不可修改的集合数据结构仍然具有可变集合的所有开销。
本文参考
- 《Java 面向对象编程》孙卫琴
- 极客时间专栏 - 设计模式之美 - 王争
- 理解不可变集合 | Guava Immutable与JDK unmodifiableList - 简书