Java 面向数据编程新篇章:告别 Record 悬崖,拥抱 Carrier 类
在 Java 世界里,Record 绝对是“懒人福音”般的存在——几行代码就能实现一个浅不可变的数据载体类,自带构造器、访问器、equals/hashCode/toString,还能通过模式匹配轻松解构。但用过的人都懂,Record 就像个“完美主义者”,只要你想稍微偏离它的规则:比如加个可变状态、缓存派生值、让表示和 API 不一致,甚至想搞个继承体系,立马就会从“温柔乡”跌进样板代码的悬崖——手写构造器、访问器、重写所有 Object 方法,还得放弃解构和模式匹配,主打一个前功尽弃。
2026 年 OpenJDK Amber 项目给出了破局之法:Carrier 类(和接口),作为 Java 面向数据编程(Data-Oriented Programming)的下一个核心特性,它保留了 Record 的所有“甜头”,又打破了其严苛约束,让所有数据载体类都能摆脱样板代码的折磨。
本文就跟着 Java 顶级架构师Brian Goetz 的设计思路,聊聊 Carrier 类到底是什么、解决了什么问题,以及它会如何改变我们写 Java 数据类的方式。
先回顾:Record 为什么香,又为什么“挑刺”
Record 的核心是状态描述(State Description),这是它能自动生成所有代码的根本。我们定义一个 Record 只需要一行:
// 状态描述:int x, int y,剩下的全靠编译器生成
record Point(int x, int y) {}
编译器会自动为我们生成:规范构造器、组件访问器(x()/y())、equals/hashCode/toString,还有解构模式,甚至还支持紧凑构造器,让我们只写自定义校验逻辑,就像golang一样去掉了繁琐的getter,setter。如下:
// 有理数 Record,只校验分母非0,字段赋值编译器自动做
record Rational(int num, int denom) {
Rational {
if (denom == 0) throw new IllegalArgumentException("分母不能为0");
}
}
Record 的两大核心承诺
Record 之所以能做到“一行顶百行”,是因为它做了两个语义承诺,这也是它的约束来源:
- 外部承诺:数据访问 API(构造器、访问器、解构模式)完全由状态描述定义;
- 内部承诺:类的表示(字段)也完全由状态描述定义,且字段是
private final,类是final,只能继承java.lang.Record。
Record 的致命痛点
这两个承诺让 Record 失去了灵活性,现实开发中很多数据类都成了**“准 Record”**,却享受不到 Record 的福利:
- 想缓存派生状态(比如给点计算模长,缓存
norm值); - 想让内部表示和 API 不一致(比如 API 暴露
Optional<String>,内部存String避免空指针); - 想做继承(比如 2D 点扩展为 3D 点);
- 想加个可变状态,或者非状态相关的字段。
这些场景下,我们只能回到 JDK 8 之前的开发模式,手写所有样板代码,这就是所谓的**“悬崖效应”**。
而 Amber 项目的目标,就是抹平这个悬崖:为非 Record 的数据类设计一套弱语义约束,让它们能按“偏离 Record 理想状态的程度”,享受对应的编译器福利,而非直接失去所有。
小插曲:Record 的重构能力——with 表达式
在聊 Carrier 类之前,先提一下 Record 一个待上线的特性:重构(Reconstruction),对应 JEP 468 的with表达式。这个特性一度被搁置,原因很简单:开发者希望它不仅能用于 Record,还能用于其他数据类。
with表达式的核心是**“伪可变”**——基于现有 Record 实例,生成一个仅修改部分组件的新实例,不用手动调用构造器,代码超简洁:
record Complex(double re, double im) {}
Complex c = new Complex(1.0, 2.0);
// 生成共轭复数,仅修改im为-2.0,不用写new Complex(c.re(), -c.im())
Complex cConjugate = c with { im = -im; };
它的工作原理和 Record 紧凑构造器异曲同工:先解构原实例得到所有组件变量,执行with块内的逻辑,最后通过规范构造器生成新实例。不变量校验依然由构造器保证,安全性拉满。
而 Carrier 类的设计,正是为了让这个实用的特性能普及到所有数据类。
核心主角:Carrier 类——Record 的“灵活版平替”
Carrier 类的核心设计思路很简单:保留 Record 的外部承诺,放弃内部承诺,并增加简单的映射机制,让开发者能手动关联“状态描述”和“类的表示”。
官方定义:Carrier 类是带有状态描述的普通 Java 类,它和 Record 一样,要求状态描述是类状态的完整、规范、命名化描述,但编译器不会对其内部表示做任何假设——字段的定义、修饰符、类型,全由开发者自己决定。
基础 Carrier 类怎么写?
和 Record 写法几乎一致,只是把record换成class,这就是一个基础的 Carrier 类:
// Carrier 类:带状态描述的普通类
class Point(int x, int y) {
// 开发者自己定义字段,编译器不做假设
private int x;
private int y;
// 必须实现和状态描述匹配的规范构造器
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// 必须实现组件访问器
public int x() { return x; }
public int y() { return y; }
// 编译器自动生成:equals/hashCode/toString(基于访问器)
// 编译器自动生成:解构模式、支持with表达式重构
}
从代码能看到,Carrier 类的最小要求只有三个:
- 声明状态描述(和 Record 一致);
- 实现和状态描述匹配的规范构造器;
- 实现所有组件的访问器方法(命名和 Record 一致,
组件名())。
只要满足这三点,编译器就会为我们自动生成equals/hashCode/toString、解构模式,还能支持重构——不用再手写这些样板代码了!
关键增强:component 字段——让 Carrier 懒出境界
基础 Carrier 类还是要手写构造器和访问器,有点麻烦。于是设计团队加了一个**component修饰符**,用来标记和状态描述一一对应的字段。
只要字段被标记为component,且名字、类型和状态描述的组件完全一致,编译器就会自动生成该字段的访问器方法,还能在构造器中自动完成字段赋值——直接把样板代码省到底!
// 带component字段的Carrier类,懒人版
class Point(int x, int y) {
// component字段:和状态描述对应,编译器自动生成访问器
private component int x;
private component int y;
// 规范构造器:只需要写自定义逻辑,字段赋值编译器自动做
public Point(int x, int y) {
// 自定义校验:x/y不能为负
if (x < 0 || y < 0) throw new IllegalArgumentException("坐标不能为负");
// this.x = x; this.y = y; 编译器自动执行
}
// 编译器自动生成:x()/y()访问器、equals/hashCode/toString、解构模式
}
终极偷懒:紧凑构造器
和 Record 一样,Carrier 类也支持紧凑构造器,直接省略构造器的参数列表和字段赋值,只写自定义逻辑,代码更简洁:
// Carrier类的紧凑构造器,极简版
class Point(int x, int y) {
private component int x;
private component int y;
// 紧凑构造器:无参数,只写自定义逻辑
public Point {
if (x < 0 || y < 0) throw new IllegalArgumentException("坐标不能为负");
}
// 编译器自动生成:访问器、构造器体、equals/hashCode/toString、解构模式
}
如果你的 Carrier 类没有任何自定义逻辑,甚至可以省略紧凑构造器——编译器会生成一个空的紧凑构造器,自动完成所有字段赋值,真正做到和 Record 一样的“一行顶百行”!
解决 Record 痛点:Carrier 类的超能力
Carrier 类的设计初衷,就是解决 Record 的各种约束,下面我们看看它是如何搞定 Record 搞不定的场景。
超能力1:轻松实现“表示与API不一致”
现实开发中,我们经常需要让类的外部 API和内部表示不一致(比如优化性能、避免空指针),这是 Record 的死穴,但 Carrier 类信手拈来。
API 暴露Optional<String>,内部存String(用null表示空),只用写差异化代码,样板代码全由编译器生成:
// 表示与API不一致的Carrier类,只写差异化逻辑
class AlmostRecord(int x, int y, Optional<String> s) {
// x/y是component字段,编译器自动生成访问器和赋值
private final component int x;
private final component int y;
// 非component字段,自定义内部表示
private final String s;
// 紧凑构造器:只处理s的转换,x/y自动赋值
public AlmostRecord {
this.s = s.orElse(null); // 仅写差异化代码
}
// 仅手动实现s的访问器,x/y由编译器生成
public Optional<String> s() {
return Optional.ofNullable(s);
}
// 编译器自动生成:equals/hashCode/toString(基于所有访问器)
}
超能力2:缓存派生状态,告别重复计算
Record 无法定义非状态相关的字段,因此不能缓存派生状态(比如点的模长、圆的面积),每次调用都要重新计算,浪费性能。
Carrier 类可以随意添加非 component 字段,在构造器中初始化派生状态,实现缓存:
// 缓存派生状态的Carrier类
class Point(int x, int y) {
private final component int x;
private final component int y;
// 非component字段:缓存点的模长
private final double norm;
// 紧凑构造器:初始化派生状态
public Point {
norm = Math.hypot(x, y); // 计算模长,只算一次
}
// 自定义访问器:获取派生状态
public double norm() { return norm; }
// 编译器自动生成:x()/y()、equals/hashCode/toString
}
超能力3:支持解构和重构,和 Record 无缝兼容
Carrier 类和 Record 一样,自动拥有解构模式,可以在instanceof、switch中直接解构,语法和 Record 完全一致:
// Carrier类的解构模式,和Record一模一样
Object obj = new Point(3, 4);
if (obj instanceof Point(var x, var y)) {
System.out.printf("坐标:(%d, %d),模长:%f%n", x, y, ((Point)obj).norm());
}
同时,因为 Carrier 类有规范构造器和解构模式,它也能完美支持**with表达式重构**,语法和 Record 无差别:
Point p = new Point(3, 4);
// 重构:修改x为5,生成新实例
Point p2 = p with { x = 5; };
延伸特性:Carrier 接口——让接口也能“面向数据”
Carrier 的设计不仅适用于类,还适用于接口,也就是Carrier 接口——带状态描述的接口,编译器会为其自动生成抽象访问器方法,实现类必须实现这些访问器。
Carrier 接口的核心价值是让接口支持模式匹配解构,让基于接口的面向数据编程成为可能。
基础 Carrier 接口
// Carrier接口:带状态描述,自动生成抽象访问器
interface Pair<T, U>(T first, U second) {
// 编译器自动生成:abstract T first(); abstract U second();
}
// 实现类:普通类/Record都可以
class PairImpl<T, U> implements Pair<T, U> {
private final T first;
private final U second;
public PairImpl(T first, U second) {
this.first = first;
this.second = second;
}
// 实现Carrier接口的访问器
@Override
public T first() { return first; }
@Override
public U second() { return second; }
}
实用场景:遍历 Map 更优雅
如果 JDK 中的Map.Entry成为 Carrier 接口,我们遍历 Map 时就能直接解构,告别getKey()/getValue():
// 若Map.Entry是Carrier接口,遍历Map可以这样写,超优雅
Map<String, Integer> map = Map.of("a", 1, "b", 2);
for (Map.Entry(var key, var val) : map.entrySet()) {
System.out.printf("key: %s, val: %d%n", key, val);
}
密封 Carrier 接口 + Record 实现
Carrier 接口和密封接口结合,能实现“接口定义数据结构,Record 实现具体逻辑”的优雅模式,比传统的接口+实现类简洁百倍:
// 密封Carrier接口,仅允许PairImpl实现
public sealed interface Pair<T, U>(T first, U second) permits PairImpl {}
// Record实现Carrier接口,不用手写任何访问器/构造器
private record PairImpl<T, U>(T first, U second) implements Pair<T, U> {}
终极灵活:Carrier 类的继承体系
Record 是final的,无法继承,这是它的一大痛点。而 Carrier 类是普通类,可以随意继承、被继承,还支持Carrier 类之间的继承,编译器会自动关联父类和子类的状态描述。
简单继承:2D Point → 3D Point
子类 Carrier 类的状态描述包含父类的所有组件时,编译器会自动匹配同名同类型的组件,无需额外标记,还能自动生成超类构造器的调用:
// 父类:2D Point Carrier类
class Point(int x, int y) {
protected component int x;
protected component int y;
// 编译器自动生成访问器、构造器等
}
// 子类:3D Point Carrier类,继承2D Point
class Point3d(int x, int y, int z) extends Point {
// 仅定义新增的component字段z
private component int z;
// 编译器自动生成:super(x, y)、z的访问器、全量equals/hashCode/toString
}
这种写法完全摆脱了 Record 的final约束,让数据类的继承体系设计变得无比轻松。
小彩蛋:抽象 Record 终于来了
基于 Carrier 类的设计框架,Record 的一个重要约束也被打破:Record 可以是抽象的了,还能继承其他抽象 Record。
抽象 Record 只定义状态描述,由子类实现具体的逻辑,编译器会自动处理父类和子类的组件关联,让 Record 也能支持继承体系设计。
本质:Record 只是特殊的 Carrier 类
看到这里,你应该能明白 Record 和 Carrier 类的关系了:Record 是 Carrier 类的“严格版”,它在 Carrier 类的基础上增加了额外的约束:
- 隐式被
final修饰; - 隐式继承
java.lang.Record; - 所有组件都隐式对应
private final component字段; - 不允许定义任何非 component 字段。
简单来说:Record ⊂ Carrier 类,Carrier 类是更通用的设计,Record 是其中的一个特例。
兼容性:现有类可无缝迁移为 Carrier 类
如果你有现有的数据类,想迁移为 Carrier 类,只要满足两个条件,就是兼容迁移,不会破坏现有代码:
- 类的现有成员和 Carrier 类要求的规范构造器、访问器无冲突;
- 类的状态描述是其状态的完整、规范、命名化描述。
同时,Record 也能兼容演化:比如给 Record 新增组件时,只要把新组件加在末尾,提供旧版构造器,且解构模式支持前缀匹配(用_表示匹配所有),现有代码就不会报错:
// 原始Record
record R(A a, B b) {}
// 演化后的Record:新增C c、D d
record R(A a, B b, C c, D d) {
// 提供旧版构造器,兼容现有代码
public R(A a, B b) {
this(a, b, DEFAULT_C, DEFAULT_D);
}
}
// 现有解构模式依然可用,编译器自动解析为前缀匹配
// case R(P1, P2) → case R(P1, P2, _, _)
if (obj instanceof R(var a, var b)) {
// 代码正常执行
}
总结:Java 面向数据编程的新时代
Record 是 Java 面向数据编程的第一次尝试,而 Carrier 类和接口则是更成熟、更灵活的升级。它解决了 Record 的所有严苛约束,保留了 Record 的全部“甜头”,还做到了:
- 让所有数据载体类都能摆脱样板代码;
- 支持表示与 API 不一致、派生状态缓存等现实开发场景;
- 支持继承体系,让数据类的设计更灵活;
- 让接口也能面向数据,支持模式匹配解构;
- 无缝兼容 Record,支持
with表达式重构。
对于 Java 开发者来说,Carrier 类的到来,意味着手写 equals/hashCode/toString、手动实现构造器和访问器的日子,一去不复返了。未来的 Java 数据类开发,会更简洁、更优雅,也更贴近实际开发的需求。
而这,正是 Amber 项目的核心目标:让 Java 更适合面向数据编程,让开发者把精力放在业务逻辑上,而非样板代码上。
更多推荐




所有评论(0)