引言
在传统 java 开发中,创建一个纯粹用于封装数据的类(如 dto 或值对象)往往需要编写大量样板代码,包括构造器、getter、equals、hashcode 和 tostring 方法。这些代码虽然重复,却难以避免,不仅影响开发效率,也降低了代码可读性。
为了解决这一问题,java 在 jep 359 中提出了“记录类”这一语言特性,并于 java 14 首次以预览形式引入,在 java 16 中正式发布。记录类的核心设计目标是为数据携带类提供一种简洁、可读性强且类型安全的声明方式。
本篇文章将带你深入理解记录类的原理与使用,掌握其各种高级特性,并通过丰富示例、性能对比和最佳实践指导,助你在实际项目中合理使用记录类,写出更加优雅、现代化的 java 代码。
语法与基本用法
记录类的定义方式非常简单,使用 record
关键字声明,取代了传统类的冗长结构。基本语法如下:
public record person(string name, int age) { }
上述代码定义了一个 person
记录类,包含两个组件(component):name
和 age
,它们将被编译器自动提升为私有 final 字段,并生成相应的方法(稍后将详细介绍)。
与传统类相比,记录类具备以下特性:
- 自动生成构造器、访问器、
equals
、hashcode
、tostring
。 - 所有字段默认
private final
。 - 构造器参数与字段一一对应。
- 支持接口实现、泛型和静态成员。
示例:基本记录类定义
public record book(string title, double price) { } public class main { public static void main(string[] args) { book book = new book("java 精通之路", 79.9); system.out.println(book.title()); system.out.println(book); } }
输出结果:
java 精通之路
book[title=java 精通之路, price=79.9]
自动生成方法详解
记录类的核心价值之一是其自动生成的标准方法,这不仅减少了开发者的重复工作,也保证了行为的一致性和语义清晰性。以下是记录类在编译阶段自动生成的方法列表:
- 所有字段的 访问器方法(accessor)
- 一个 规范构造器(canonical constructor)
- equals(object obj) 方法
- hashcode() 方法
- tostring() 方法
1. 访问器方法
每个组件都会生成一个访问器方法,其方法名与字段名一致。例如:
public record user(string username, string role) { } user user = new user("alice", "admin"); system.out.println(user.username()); // 输出:alice system.out.println(user.role()); // 输出:admin
注意:记录类不生成传统的 getusername()
形式的方法。
2. equals 和 hashcode 方法
记录类会基于字段值自动生成合理的 equals
和 hashcode
方法。
user user1 = new user("alice", "admin"); user user2 = new user("alice", "admin"); system.out.println(user1.equals(user2)); // true system.out.println(user1.hashcode() == user2.hashcode()); // true
比较是基于组件值进行的,而非引用。
3. tostring 方法
记录类默认实现了符合逻辑的 tostring()
方法,格式为:类名[字段1=值1, 字段2=值2]
system.out.println(user1.tostring()); // 输出:user[username=alice, role=admin]
4. 规范构造器
编译器会生成一个公共构造器,其参数列表与组件顺序一致:
public user(string username, string role) { this.username = username; this.role = role; }
此构造器不能省略字段赋值,且不能绕过不可变性。
不可变性的特性与价值
记录类的一个显著特性是:不可变性(immutability)。一旦创建了记录类实例,其状态便不可更改,这使得记录类天然适合用于线程安全的数据传递、缓存键、日志记录和函数式编程等场景。
不可变性的实现方式
在记录类中:
- 所有组件字段默认是
private final
,且不能被修改。 - 没有生成
setter
方法。 - 构造函数中必须为所有字段赋值,且不能通过反射等方式绕过字段 final 的限制(除非使用非法手段)。
public record customer(string id, string name) { // 无法修改 id 和 name } customer c1 = new customer("001", "alice"); c1.name = "bob"; // 编译错误,字段是 final 的
不可变性的优势
- 线程安全:多个线程可安全共享记录类实例,而无需加锁。
- 更少的副作用:状态不可更改,避免由于共享状态带来的错误。
- 更简单的调试与测试:数据不会在调用链中被意外修改,提高可预测性。
- 易于缓存和哈希结构使用:不可变对象可作为 map 的键,不会影响哈希值。
与传统类的对比
在传统 java 类中,需要人为地将字段设置为 private final
,并避免提供 setter
方法,才能勉强模拟不可变性:
public final class immutableperson { private final string name; private final int age; public immutableperson(string name, int age) { this.name = name; this.age = age; } public string getname() { return name; } public int getage() { return age; } }
记录类通过语法层级的约束,天然支持不可变性,避免了人为失误。
注意事项:深层不可变性
虽然记录类本身是不可变的,但如果其字段是可变对象(如 list、map、数组),则需要谨慎处理。
public record userprofile(string username, list<string> tags) { } list<string> list = new arraylist<>(); list.add("java"); userprofile profile = new userprofile("bob", list); profile.tags().add("record"); // 可变字段被修改
上述代码中,虽然 tags
字段本身是 final
,但它引用的是一个可变列表,因此对象并不是真正不可变。
解决方案:
- 在构造函数中进行 defensive copy(防御性复制)。
- 使用
list.copyof()
创建不可变集合。
public record safeuserprofile(string username, list<string> tags) { public safeuserprofile { tags = list.copyof(tags); } }
这样就能保证 tags
无法在外部被修改,真正实现深层不可变。
构造器机制
尽管记录类自动为我们生成了标准构造器,但在实际开发中,仍有很多场景需要我们自定义构造逻辑,比如字段校验、数据转换等。java 记录类支持三种构造器形式:
- 规范构造器(canonical constructor)
- 紧凑构造器(compact constructor)
- 自定义构造器(custom constructor)
规范构造器(canonical constructor)
规范构造器是由编译器自动生成的构造函数,其参数列表与组件顺序一致,默认行为是将所有字段赋值:
public record product(string name, double price) { // 编译器生成如下构造器: // public product(string name, double price) { // this.name = name; // this.price = price; // } }
我们也可以显式地定义规范构造器,来加入自定义逻辑,例如参数校验:
public record product(string name, double price) { public product { if (price < 0) { throw new illegalargumentexception("价格不能为负数"); } } }
紧凑构造器(compact constructor)
java 为记录类提供了一种语法糖,称为“紧凑构造器”。在紧凑构造器中,我们无需列出参数列表,编译器会自动将构造器参数与字段绑定。
public record student(string name, int age) { public student { if (age < 0) { throw new illegalargumentexception("年龄不能为负数"); } } }
等价于:
public record student(string name, int age) { public student(string name, int age) { if (age < 0) { throw new illegalargumentexception("年龄不能为负数"); } this.name = name; this.age = age; } }
紧凑构造器简化了字段赋值过程,适用于多数校验逻辑场景。
自定义构造器与重载
记录类可以定义额外的构造器,但这些构造器必须调用规范构造器,不能绕过字段赋值流程:
public record coordinate(int x, int y) { public coordinate() { this(0, 0); // 默认构造 } public coordinate(int value) { this(value, value); // 重载构造 } }
注意:
- 所有非规范构造器都必须显式调用
this(...)
。 - 不能定义不初始化所有字段的构造器。
通过构造器机制,记录类不仅保持了不可变性,还为开发者提供了足够的灵活性,用于字段校验、默认值填充、工厂构造等实际需求。
高级特性
记录类虽然语法简洁,但功能并不简单。它支持许多高级语言特性,包括静态成员、接口实现、泛型定义以及本地记录类的声明,使其具备在复杂应用中广泛使用的能力。
静态成员
记录类可以像普通类一样包含静态字段、静态方法和静态代码块,这些静态成员的行为与传统类完全一致。
public record color(int red, int green, int blue) { public static final color black = new color(0, 0, 0); public static final color white = new color(255, 255, 255); public static string tohex(color c) { return string.format("#%02x%02x%02x", c.red(), c.green(), c.blue()); } } color white = color.white; system.out.println(color.tohex(white)); // 输出:#ffffff
接口实现
记录类可以实现接口(包括函数式接口),并提供对应方法实现。
public interface identifiable { string id(); } public record employee(string id, string name) implements identifiable { // 自动实现 id() 方法 }
记录类实现接口时,可以通过组件直接满足接口方法签名,进一步提升了类型的表达力。
泛型支持
记录类可以定义为泛型类型,从而适用于多种数据类型的封装。
public record pair<k, v>(k key, v value) { } pair<string, integer> entry = new pair<>("age", 30); system.out.println(entry.key()); // 输出:age system.out.println(entry.value()); // 输出:30
泛型记录类尤其适用于通用值对象、键值对封装、元组等使用场景。
本地记录类(local records)
从 java 16 开始,记录类也可以在方法内部定义,称为“本地记录类”,适用于封装局部方法逻辑中的中间数据结构。
public class reportgenerator { public void generate() { record summary(string title, int count) {} summary summary = new summary("周报", 12); system.out.println(summary); } }
本地记录类只在方法范围内可见,能够提升临时数据处理的类型安全性与可读性,避免引入冗余的外部类定义。
通过这些高级特性,记录类在保持简洁的同时,依然具备高度灵活性,能够应对多种开发需求,适用于从简单 dto 到泛型模型、工具类的广泛场景。
限制与注意事项
尽管记录类极大简化了不可变数据类的开发,但其设计也存在一些固有的限制,了解这些限制有助于合理使用,避免不合适的场景导致代码设计问题。
1. 继承限制
- 记录类隐式继承自
java.lang.record
,不允许继承其他类。 - 记录类本身是
final
的,不能被继承。 - 这意味着无法通过继承扩展记录类行为,只能通过组合或接口实现进行扩展。
// 编译错误,记录类不能继承其他类 public record myrecord extends someclass { } // 编译错误,记录类不能被继承 public class subrecord extends myrecord { }
2. 字段限制
- 所有组件字段自动为
private final
,不能定义非final
字段。 - 不支持额外的实例字段,只能通过静态字段扩展类功能。
3. 不支持无参构造器
- 记录类必须初始化所有组件字段,不能定义无参构造器。
- 但可通过自定义构造器为字段赋默认值,实现类似无参构造器的效果。
4. 不支持可变字段
- 记录类设计为不可变,因此组件字段不能是可变的。
- 如果字段引用了可变对象,需自行保证深不可变性(如使用不可变集合或防御性复制)。
5. 限制扩展性
- 由于不能继承和添加实例字段,记录类不适合表示需要复杂行为和状态管理的实体。
- 更适合用于简单数据载体(如 dto、值对象)。
6. 序列化限制
- 记录类实现了
serializable
,但序列化时遵循组件字段的序列化规则。 - 如果字段不可序列化,会导致序列化失败。
理解并遵守这些限制,有助于避免误用记录类导致设计不佳和维护困难。
到此这篇关于java 记录类record详解的文章就介绍到这了,更多相关java record 类内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论