Skip to main content
在现代 Java 开发中,不可变性(Immutability)已成为构建健壮、线程安全应用的核心原则之一。本文将深入探讨如何使用 Java Records 和 Lombok 的 @Value 注解来优雅地实现不可变对象。

什么是不可变对象?

不可变对象是指一旦创建完成,其内部状态就无法被修改的对象。在 Java 中,不可变对象需要满足以下特征:
  • 状态固定:对象创建后,所有字段的值保持不变
  • 字段不可变:所有字段都使用 final 关键字修饰
  • 无修改方法:不提供任何 setter 方法或其他可以修改内部状态的方法
  • 防御性拷贝:如果包含可变对象的引用,不直接暴露这些引用

常见的不可变对象示例

Java 标准库中有许多经典的不可变类:
String name = "张三";  // String 是不可变的
Integer count = 100;   // 包装类都是不可变的
LocalDate today = LocalDate.now();  // 日期时间 API 都是不可变的

为什么不可变性如此重要?

1. 线程安全性

不可变对象天生就是线程安全的。由于状态永远不会改变,多个线程可以同时访问同一个对象而不需要任何同步机制。
// 多线程环境下安全使用
public record User(String name, int age) {}

User user = new User("李四", 25);
// 多个线程可以安全地读取 user,无需担心并发问题

2. 简化代码逻辑

不可变对象消除了意外状态变化的可能性,使代码更容易理解和维护。你不需要追踪对象在程序运行过程中的状态变化。

3. 适合作为 HashMap 的键

不可变对象的哈希码永远不会改变,这使它们非常适合作为 HashMapHashSet 的键。
Map<User, String> userMap = new HashMap<>();
User user = new User("王五", 30);
userMap.put(user, "员工信息");
// user 的哈希码永远不会改变,保证了映射的可靠性

4. 避免防御性拷贝

当方法返回不可变对象时,不需要创建防御性拷贝,因为调用者无法修改返回的对象。

传统方式实现不可变类

在 Java 16 之前,我们需要手动编写大量样板代码来实现不可变类:
public final class Person {
    private final String name;
    private final int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}
这种方式虽然可行,但存在明显的问题:
  • 代码冗长,样板代码过多
  • 容易出错(比如忘记将类或字段声明为 final)
  • 维护成本高,增加字段时需要修改多个方法

使用 Java Records 实现不可变性

Java 14 引入的 Records 为定义不可变数据类提供了一种简洁优雅的方式。

基本用法

public record Person(String name, int age) {}
就是这么简单!这一行代码自动生成了:
  • 私有的 final 字段
  • 公共的构造函数
  • getter 方法(name()age()
  • equals()hashCode()toString() 方法

Records 的特性

public record Employee(String name, int age, String department) {
    
    // 紧凑构造函数:用于参数验证
    public Employee {
        if (age < 18) {
            throw new IllegalArgumentException("员工年龄必须大于等于 18 岁");
        }
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("姓名不能为空");
        }
    }
    
    // 可以添加自定义方法
    public boolean isRetirementAge() {
        return age >= 60;
    }
    
    // 可以添加静态工厂方法
    public static Employee of(String name, int age) {
        return new Employee(name, age, "未分配");
    }
}

使用示例

// 创建 Record 实例
Employee emp = new Employee("张三", 28, "技术部");

// 访问字段(注意:getter 方法名是字段名,不是 getXxx)
System.out.println(emp.name());        // 输出: 张三
System.out.println(emp.age());         // 输出: 28
System.out.println(emp.department());  // 输出: 技术部

// 自动生成的 toString()
System.out.println(emp);  // 输出: Employee[name=张三, age=28, department=技术部]

// equals() 和 hashCode() 基于所有字段
Employee emp2 = new Employee("张三", 28, "技术部");
System.out.println(emp.equals(emp2));  // 输出: true

处理可变字段

当 Record 包含可变对象时,需要特别注意:
public record Department(String name, List<String> employees) {
    
    // 使用紧凑构造函数创建防御性拷贝
    public Department {
        employees = List.copyOf(employees);  // 创建不可变副本
    }
    
    // 或者使用规范构造函数
    public Department(String name, List<String> employees) {
        this.name = name;
        this.employees = Collections.unmodifiableList(new ArrayList<>(employees));
    }
}

使用 Lombok @Value 实现不可变性

Lombok 是一个强大的 Java 库,通过注解自动生成样板代码。@Value 注解专门用于创建不可变类。

添加 Lombok 依赖

首先在项目中添加 Lombok 依赖:
<!-- Maven -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
    <scope>provided</scope>
</dependency>

基本用法

import lombok.Value;

@Value
public class Person {
    String name;
    int age;
}
@Value 注解会自动:
  • 将类声明为 final
  • 将所有字段声明为 private final
  • 生成全参构造函数
  • 生成 getter 方法(getName()getAge()
  • 生成 equals()hashCode()toString() 方法

@Value 的高级特性

import lombok.Value;
import lombok.With;

@Value
public class Employee {
    String name;
    int age;
    String department;
    
    // @With 注解生成 withXxx 方法,用于创建修改后的副本
    @With
    String email;
    
    // 可以自定义方法
    public boolean isRetirementAge() {
        return age >= 60;
    }
}

使用 @With 创建修改副本

Employee emp = new Employee("李四", 30, "销售部", "lisi@example.com");

// 创建一个新对象,只修改 email 字段
Employee updatedEmp = emp.withEmail("lisi_new@example.com");

System.out.println(emp.getEmail());        // 输出: lisi@example.com
System.out.println(updatedEmp.getEmail()); // 输出: lisi_new@example.com

自定义构造函数

import lombok.Value;

@Value
public class Product {
    String name;
    double price;
    
    // 自定义构造函数进行验证
    public Product(String name, double price) {
        if (price < 0) {
            throw new IllegalArgumentException("价格不能为负数");
        }
        this.name = name;
        this.price = price;
    }
}

处理集合字段

import lombok.Value;
import lombok.Singular;

import java.util.List;

@Value
public class Team {
    String name;
    
    // @Singular 注解提供构建器模式支持
    @Singular
    List<String> members;
}

// 使用构建器(需要配合 @Builder 使用)
import lombok.Builder;

@Value
@Builder
public class Team {
    String name;
    
    @Singular
    List<String> members;
}

// 使用示例
Team team = Team.builder()
    .name("开发团队")
    .member("张三")
    .member("李四")
    .member("王五")
    .build();