引言

在 Java 开发中,我们经常需要编写一些仅用于存储数据的类,比如 DTO(数据传输对象)、值对象或实体。这些类通常包含私有字段、Getter/Setter、equals()、hashCode()、toString() 和构造函数,导致大量的样板代码。虽然 IDE 可以自动生成这些代码,但维护起来依然繁琐。

为了解决这个问题,Java 14 引入了 Record 作为预览特性,并在 Java 16 中正式成为标准功能。Record 是一种全新的类型,专门用于声明不可变的数据载体。


背景

在 Java 14 之前,定义一个简单的“数据持有者”类需要编写大量重复代码。例如

public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 手动实现 getter、toString、equals、hashCode
    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public String toString() { return "Person[name=" + name + ", age=" + age + "]"; }
    
    @Override
    public boolean equals(Object o) { /* ... */ }
    @Override
    public int hashCode() { /* ... */ }
}

这种模式代码冗长、可读性差。因而催生出record,,解决这些问题,自动处理数据类的样板代码。


record语法和原理

record类的定义方式非常简单,基本语法如下:

public record Person(String name, int age) {}

编译器在编译时会自动生成以下内容:

  • 所有字段:自动声明为 private final
  • 构造函数:自动生成带参数的构造器(无其他构造方法);
  • 核心方法:自动生成 toString()、equals()、hashCode();
  • 访问器:自动生成字段的 getter 方法(如 name()),注意方法名与组件名相同,没有 get 前缀

关键机制:

  1. record 隐式继承 java.lang.Record,不可继承其他类;
  2. 字段是只读的,编译器强制不可变性;
  3. 生成的代码基于字段顺序(如 Person(“Alice”, 30) 的 name 和 age 顺序)。

使用方法

1. 基础定义和使用

public record Person(String name, int age) { }

// 使用
Person p = new Person("Alice", 30);
System.out.println(p.name());  // 访问器,注意不是 getName()
System.out.println(p.age());
System.out.println(p);          // 自动生成的 toString:Person[name=Alice, age=30]

2. 自定义方法

record 允许添加静态方法或实例方法(但不能添加额外字段或覆盖 equals/hashCode)
如:

public record Person(String name, int age) {
    public int birthYear() {
        return LocalDate.now().getYear() - age;
    }

    public static Person createAdult(String name) {
        return new Person(name, 18);
    }
}

// 使用
Person alice = Person.of("Alice", 30);
System.out.println(alice.birthYear());

3. 紧凑构造方法

如果你需要在构造时进行校验或规范化,可以定义紧凑构造方法,它没有参数列表,直接在方法体中编写逻辑。如:

public record Person(String name, int age) {
    public Person {
        if (age < 0) {
            throw new IllegalArgumentException("年龄不能为负数");
        }
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("姓名不能为空");
        }
        // 可以在此处对参数进行修改,比如统一格式化
        name = name.trim();
    }
}

紧凑构造方法会在标准构造方法之前执行,并且参数可以被修改(修改后会反映到字段中)。注意,紧凑构造方法只能用于校验和调整参数,不能为字段重新赋值(因为字段是 final 的,赋值由编译器生成的标准构造完成)。


4. 搭配注解使用

Record 的组件可以添加注解,这些注解会传播到对应的字段、访问器方法和构造参数上,适用于许多框架(如 JSON 序列化、验证框架)。如:

import jakarta.validation.constraints.*;

public record User(
    @NotNull String username,
    @Min(18) int age
) { 

}

5. 本地 Record

Record 也可以声明为局部类(在方法内部),这在某些场景下非常实用。如:

public List<String> getNames() {
    record NameEntry(String firstName, String lastName) {}
    return list.stream()
               .map(e -> new NameEntry(e.getFirstName(), e.getLastName()))
               .map(NameEntry::toString)
               .collect(Collectors.toList());
}

使用注意事项

1. 不可变性

Record 被设计为不可变类,所有字段都是 final 的。这意味着一旦创建,其内部状态不能改变。也不能添加可变字段。


2. 不能继承或扩展其他类

  • record 隐式继承 java.lang.Record,不能显式继承其他类;
  • Record 不能声明实例字段(除了组件对应的字段),只能声明静态字段;
  • Record 不能定义实例初始化块。

3. 不能有自定义构造函数

编译器只生成带参数的构造器,不能添加其他构造方法。

public record Person(String name, int age) {
    public Person(String name) { // 编译错误
        this(name, 0);
    }
}

4. 不能覆盖 equals/hashCode/toString

这些方法由编译器自动生成,禁止手动重写。


5. 序列化

Record 支持序列化,但序列化行为与传统 Java 对象略有不同。Record 的序列化会使用规范构造方法重建对象,而不是通过反射设置字段。这意味着即使你修改了 Record 的字段名(但保留组件名),反序列化也能正常工作。但需要注意,Record 的序列化版本号(serialVersionUID) 是根据组件和类型自动生成的,一般无需显式声明。


Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐