Java 中如何创建不可变类型

前言

原创不易,禁止转载。

本文准备讨论一下如何创建不可变对象,先简单介绍一下相关知识点,后面再谈如何创建。读者可以根据需要跳过前面的部分。

Java 中,一个对象创建后不能修改其状态,称之为不可变对象。

优点(Benefits)

  • 线程安全:不可变对象在并发环境中天然线程安全,多个线程可以同时读取而不需要额外同步。
  • 简化代码:不可变对象的状态在创建后不能更改,简化了代码的理解和维护。
  • 更易于调试:不用担心状态变化带来的问题,程序行为更具可预测性。
  • 可以缓存:由于对象不可变,它们可以在内存中共享和重用,提升性能。

缺点(Disadvantages)

  • 内存开销:如果需要修改不可变对象的状态,你必须创建一个新的对象。频繁的对象创建可能导致内存浪费。
  • 复杂度:在处理大对象图(包含多个字段或嵌套对象)时,确保每个对象的不可变性会增加开发的复杂度。
  • 不适用于所有场景:对于需要频繁修改的对象,使用不可变类可能会导致性能问题。

深不可变(Deep Immutability) vs. 浅不可变(Shallow Immutability)

  • 浅不可变(Shallow Immutable):只有对象本身的字段(如基本数据类型或引用类型的字段)不可变,但是如果字段本身是可变对象(如 ListMap 等),其内部的内容仍然可以修改。
  • 深不可变(Deep Immutable):不仅对象本身的字段不可变,即使字段包含的可变对象(如 ListMap)也必须是不可变的。

最强辅助: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);  // 会抛出异常
    }
}

不可变类型的创建

  1. 使用 record 创建

Java17支持record关键字创建不可变类,记录类自动生成了不可变类的所有常见方法,包括构造函数、toString()hashCode()equals() 等。非常适合存储不可变的数据。

arduino 复制代码
public record Person(String name, int age) {
    // 记录类的构造方法自动生成
    // 你可以添加方法,比如:
    public String greet() {
        return "Hello, my name is " + name;
    }
}
  1. 手动创建

手动创建的不可变对象要求字段为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); // 防止外部修改
    }
}
  1. 使用 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;
}
  1. 使用 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. 其他常见对象

数值计算 BigIntegerBigDecimal

用于处理大整数和高精度小数,修改时返回新对象,广泛用于金融和科学计算等需要高精度计算的场景。

Java 8 日期/时间 API LocalDate, LocalDateTime, Duration, Period

用于表示日期和时间的不可变对象,所有修改操作返回新的对象,广泛应用于时间戳生成、事件调度等业务场景。

文件系统 API Path

表示文件路径的不可变对象,路径一旦创建就无法修改,常用于文件系统操作,支持路径合并、计算等功能。

范围 Range(Guava)

表示不可变的范围对象,常用于定义数值区间(如 [1, 10]),用于区间计算等。

ListenableFuture (Guava)

用于表示异步计算结果的类,提供比 Future 更强大的功能,允许注册回调函数,适用于异步计算结果处理。

CompletableFuture (Java 8+)

用于处理异步计算的类,支持回调和链式调用,可以进行异步计算组合和并发操作,是处理异步任务时常用的工具。注意需要用户自己不要覆盖结果就行,使用起来和不可变对象具有一样的效果(线程安全性)。严格来说,需要禁止覆盖结果,推荐使用 minimalStage 方法或者 CFFU 实现。

总结

  • 不可变类 在 Java 中是一种非常有用的设计模式,确保对象的状态在创建之后不能被修改。
  • 记录类(Record) 提供了一种简洁的方式来实现不可变对象。
  • Guava 不可变集合和数组封装 提供了非常方便的工具,可以帮助你快速创建和使用不可变集合。
  • 在设计不可变类时,需要注意不可变性的深浅。
相关推荐
uzong1 小时前
技术故障复盘模版
后端
GetcharZp2 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
lifallen2 小时前
Java Stream sort算子实现:SortedOps
java·开发语言
IT毕设实战小研2 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi3 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
没有bug.的程序员3 小时前
JVM 总览与运行原理:深入Java虚拟机的核心引擎
java·jvm·python·虚拟机
甄超锋3 小时前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat
阿华的代码王国4 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Zyy~4 小时前
《设计模式》装饰模式
java·设计模式