前言
原创不易,禁止转载。
本文准备讨论一下如何创建不可变对象,先简单介绍一下相关知识点,后面再谈如何创建。读者可以根据需要跳过前面的部分。
Java 中,一个对象创建后不能修改其状态,称之为不可变对象。
优点(Benefits)
- 线程安全:不可变对象在并发环境中天然线程安全,多个线程可以同时读取而不需要额外同步。
- 简化代码:不可变对象的状态在创建后不能更改,简化了代码的理解和维护。
- 更易于调试:不用担心状态变化带来的问题,程序行为更具可预测性。
- 可以缓存:由于对象不可变,它们可以在内存中共享和重用,提升性能。
缺点(Disadvantages)
- 内存开销:如果需要修改不可变对象的状态,你必须创建一个新的对象。频繁的对象创建可能导致内存浪费。
- 复杂度:在处理大对象图(包含多个字段或嵌套对象)时,确保每个对象的不可变性会增加开发的复杂度。
- 不适用于所有场景:对于需要频繁修改的对象,使用不可变类可能会导致性能问题。
深不可变(Deep Immutability) vs. 浅不可变(Shallow Immutability)
- 浅不可变(Shallow Immutable):只有对象本身的字段(如基本数据类型或引用类型的字段)不可变,但是如果字段本身是可变对象(如
List
、Map
等),其内部的内容仍然可以修改。 - 深不可变(Deep Immutable):不仅对象本身的字段不可变,即使字段包含的可变对象(如
List
、Map
)也必须是不可变的。
最强辅助:Guava 提供了不可变支持
Guava 对于不可变类型提供了大量支持,不可变集合还具有以下特点:
- 不支持null对象。这是个好事啊。
- 内存优化,不可变对象尽量复用。
常见的集合类型如ImmutableList, ImmutableMap 等等,还支持 BiMap, Multimap, Graph 等不可变形式。
此外,可能被忽略的还有,Range为不可变形式,支持底层数组的不可变封装,如:ImmutableIntArray, ImmutableDoubleArray。自己封装的底层数组常常不能保证不可变性,推荐使用Guava提供的实现,可以保证性能与安全。
arduino
public class GuavaImmutableArrayExample {
public static void main(String[] args) {
// 创建一个不可变的 int 数组
int[] array = {1, 2, 3, 4, 5};
ImmutableIntArray immutableArray = ImmutableIntArray.copyOf(array);
System.out.println("ImmutableIntArray: " + immutableArray);
// 尝试修改数组会抛出异常
// immutableArray.set(0, 10); // 会抛出异常
}
}
不可变类型的创建
- 使用 record 创建
Java17支持record关键字创建不可变类,记录类自动生成了不可变类的所有常见方法,包括构造函数、toString()
、hashCode()
、equals()
等。非常适合存储不可变的数据。
arduino
public record Person(String name, int age) {
// 记录类的构造方法自动生成
// 你可以添加方法,比如:
public String greet() {
return "Hello, my name is " + name;
}
}
- 手动创建
手动创建的不可变对象要求字段为final 不可修改,同时注意对于集合类型应该进行防御性复制,防止外部对于集合的修改。
arduino
public final class ImmutablePerson {
private final String name;
private final int age;
private final List<String> favoriteBooks;
// 构造函数
public ImmutablePerson(String name, int age, List<String> favoriteBooks) {
this.name = name;
this.age = age;
// 复制列表,确保它不可变
this.favoriteBooks = new ArrayList<>(favoriteBooks);
}
// 只提供 getter 方法,返回副本确保不可变性
public List<String> getFavoriteBooks() {
return new ArrayList<>(favoriteBooks); // 防止外部修改
}
}
- 使用 lombok 注解 @Value 创建
由于不可变对象的创建比较繁琐,对于不能使用 record 关键字的场景,也可以使用lombok注解辅助创建,这是不需要手动写 private final 了。
arduino
@Value
public class ImmutablePerson {
String name;
int age;
List<String> favoriteBooks;
// 防止外部修改内部列表
public ImmutablePerson(String name, int age, List<String> favoriteBooks) {
this.name = name;
this.age = age;
// 防御性拷贝,确保 favoriteBooks 不会被修改
this.favoriteBooks = new ArrayList<>(favoriteBooks);
}
// 返回副本,确保外部不能修改列表
public List<String> getFavoriteBooks() {
return new ArrayList<>(favoriteBooks); // 防止外部修改原始列表
}
}
这里更推荐使用不可变集合,可以避免反复的复制, 而且也相当简洁。
arduino
@Value
public class ImmutablePerson {
String name;
int age;
ImmutableList<String> favoriteBooks;
}
- 使用 Immutables 类库自动生成不可变实现
Immutables 和 Lombok 都基于注解生成代码,Immutables 对于不可变对象的支持更全,还支持多种特性,比如 Buider 模式,懒计算,静态方法生成,单例生成等等。
csharp
@Value.Immutable
public interface ValueObject {
String name();
List<Integer> counts();
Optional<String> description();
}
像搭积木一样创建
不可变对象的创建通常涉及多个不可变组件的组合,就像搭积木一样。每个不可变组件都能确保自己的状态不会被外部修改,最终构建出一个完全不可变的对象。以下是一些常用的不可变对象类型及其实现方式:
1. JDK 提供的不可变组件
String
Java 中最常用的不可变对象,内容一旦创建就不能修改,广泛用于存储文本数据,特别是在键值对、日志记录等场景。
包装类(Integer
, Double
, Boolean
等)
Java 基本数据类型的包装类,它们一旦赋值就无法修改,常用于对象化编程和集合中,确保基本类型的操作符合对象化需求。
Optional
一个容器对象,表示某个值可能存在或不存在,避免直接使用 null
,增强代码可读性和安全性。
Enum
枚举常量不可变,且枚举类型本身是 final
,常用于表示固定的常量集,如状态、类型等。
2. Guava 提供的不可变集合
ImmutableList
, ImmutableSet
, ImmutableMap
Guava 提供的不可变集合,通过 copyOf()
方法创建,确保集合一旦创建后不可修改,适用于需要共享数据且不希望被修改的场景。
ImmutableIntArray
Guava 提供的不可变整型数组,避免外部修改,适用于需要保持数据安全的整型数组场景。
3. 其他常见对象
数值计算 BigInteger
和 BigDecimal
用于处理大整数和高精度小数,修改时返回新对象,广泛用于金融和科学计算等需要高精度计算的场景。
Java 8 日期/时间 API LocalDate
, LocalDateTime
, Duration
, Period
用于表示日期和时间的不可变对象,所有修改操作返回新的对象,广泛应用于时间戳生成、事件调度等业务场景。
文件系统 API Path
表示文件路径的不可变对象,路径一旦创建就无法修改,常用于文件系统操作,支持路径合并、计算等功能。
范围 Range
(Guava)
表示不可变的范围对象,常用于定义数值区间(如 [1, 10]),用于区间计算等。
ListenableFuture
(Guava)
用于表示异步计算结果的类,提供比 Future
更强大的功能,允许注册回调函数,适用于异步计算结果处理。
CompletableFuture
(Java 8+)
用于处理异步计算的类,支持回调和链式调用,可以进行异步计算组合和并发操作,是处理异步任务时常用的工具。注意需要用户自己不要覆盖结果就行,使用起来和不可变对象具有一样的效果(线程安全性)。严格来说,需要禁止覆盖结果,推荐使用 minimalStage 方法或者 CFFU 实现。
总结
- 不可变类 在 Java 中是一种非常有用的设计模式,确保对象的状态在创建之后不能被修改。
- 记录类(Record) 提供了一种简洁的方式来实现不可变对象。
- Guava 不可变集合和数组封装 提供了非常方便的工具,可以帮助你快速创建和使用不可变集合。
- 在设计不可变类时,需要注意不可变性的深浅。