免責聲明:这个答案只是通过总结一些含义并给出一些例子来扩展其他答案。您不应根据此信息做出任何决策,因为模式匹配和值类型仍然是更改的对象。
关于数据类(即记录与值类型)有两个有趣的文档: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; }
// operations like add, sub, mult or div
}
如何计算该分数的浮点值?您可以将方法 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; }
// operations like add, sub, mult or div
}
当然,隐藏字段可能是您要使用值类型的一个原因。它们只是一个方面,可能是一个次要的方面。如果您创建了许多 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);
}
当然,还有许多其他方法可以实现相同的行为。但是,记录和值类型为您提供了更多非常有用的实现选项。