声明:本答案仅扩展其他答案的总结一些含义并举例说明。您不应该基于这些信息做出任何决定,因为模式匹配和值类型仍然是变化的主题。
关于数据类别(records)与值类型(value types)有两个有趣的文档:
旧版文档发布于2018年2月
http://cr.openjdk.java.net/~briangoetz/amber/datum_2.html#are-data-classes-the-same-as-value-types
新版文档发布于2019年2月
https://cr.openjdk.java.net/~briangoetz/amber/datum.html#are-records-the-same-as-value-types
每个文档都包括一个关于记录和值类型之间差异的段落。
旧版文档中写道:
缺少布局多态性意味着我们必须牺牲其他东西:自我引用。值类型V不能直接或间接地引用另一个V。
此外,
与值类型不同,数据类很适合表示树和图节点。
但是,
但是值类不需要放弃任何封装,实际上,封装对某些值类型的应用是必不可少的。
让我们澄清一下:
您无法使用值类型实现基于节点的组合数据结构,例如链接列表或分层树。但是,您可以使用值类型作为这些数据结构的元素。
此外,相反地,值类型支持某些形式的封装,而记录则根本不支持。这意味着您可以在值类型中拥有未在类头中定义且对值类型用户隐藏的附加字段。记录无法做到这一点,因为它们的表示受限于其API,即所有字段都在类头中声明(且仅在那里!)。
以下是一些示例,以说明以上内容。
例如,你可以使用记录创建组合的逻辑表达式,但是不能使用值类型:
sealed interface LogExpr { boolean eval(); }
record Val(boolean value) implements LogExpr {}
record Not(LogExpr logExpr) implements LogExpr {}
record And(LogExpr left, LogExpr right) implements LogExpr {}
record Or(LogExpr left, LogExpr right) implements LogExpr {}
这将无法使用值类型,因为这需要同一值类型的自我引用能力。你希望能够创建类似于"Not(Not(Val(true)))"的表达式。
例如,您还可以使用记录来定义类
分数:
record Fraction(int numerator, int denominator) {
Fraction(int numerator, int denominator) {
if (denominator == 0) {
throw new IllegalArgumentException("Denominator cannot be 0!");
}
}
public double asFloatingPoint() { return ((double) numerator) / denominator; }
}
计算该分数的浮点数值怎么样呢?
您可以在记录Fraction中添加一个方法asFloatingPoint()。
每次调用该方法时,它将始终计算(并重新计算)相同的浮点数值。
(默认情况下,记录和值类型是不可变的)。
但是,您无法以对用户隐藏的方式预先计算并存储该记录中的浮点数值。
而且,您也不想在类头中显式声明浮点数值作为第三个参数。
幸运的是,值类型可以做到这一点:
inline class Fraction(int numerator, int denominator) {
private final double floatingPoint;
Fraction(int numerator, int denominator) {
if (denominator == 0) {
throw new IllegalArgumentException("Denominator cannot be 0!");
}
floatingPoint = ((double) numerator) / denominator;
}
public double asFloatingPoint() { return floatingPoint; }
}
当然,隐藏字段可能是您想使用值类型的原因之一。但它只是其中的一个方面,可能是比较次要的。
如果您创建了许多 Fraction 实例并将它们存储在集合中,那么从平坦内存布局中受益会很大。
这肯定是更重要的原因,有利于将值类型优先于记录。
有一些情况您可能希望同时从记录和值类型中受益。
例如,您可能想开发一个游戏,在游戏中通过地图移动棋子。
您之前保存了一份包含每个移动的步数的历史记录列表,并且现在想根据该移动列表计算下一个位置。
如果您的类 Move 是一个值类型,那么该列表可以使用平坦的内存布局。
如果您的类 Move 同时也是一条记录,则无需定义显式解构模式即可使用模式匹配。
您的代码可以像这样:
enum Direction { LEFT, RIGHT, UP, DOWN }´
record Position(int x, int y) { }
inline record Move(int steps, Direction dir) { }
public Position move(Position position, List<Move> moves) {
int x = position.x();
int y = position.y();
for(Move move : moves) {
x = x + switch(move) {
case Move(var s, LEFT) -> -s;
case Move(var s, RIGHT) -> +s;
case Move(var s, UP) -> 0;
case Move(var s, DOWN) -> 0;
}
y = y + switch(move) {
case Move(var s, LEFT) -> 0;
case Move(var s, RIGHT) -> 0;
case Move(var s, UP) -> -s;
case Move(var s, DOWN) -> +s;
}
}
return new Position(x, y);
}
当然,实现相同行为的方法还有很多种。但是,记录和值类型为您提供了更多可以非常有用的实现选项。
inline
! - sprinter