Java 记录是否打算最终成为值类型?
Are Java records intended to eventually become value types?
JDK14 中引入的record
预览功能 (JEP 384) 是一项伟大的创新。它们使创建简单的不可变 类 变得容易得多,这些 类 是纯粹的值集合,不会丢失各种库中通用元组 类 固有的上下文。
Brian Goetz (https://openjdk.java.net/jeps/384) 撰写的 JEP 描述很好地解释了其意图。然而,我期待与值类型的最终引入有更紧密的联系。值类型的最初目标非常广泛:通过删除这些类型的对象不需要的所有开销(例如引用间接,同步)。此外,它还可以提供语法细节,例如 myPosition != yourPosition
而不是 !myPosition.equals(yourPosition)
.
似乎记录的限制与潜在值类型所需的限制类型非常接近。然而,JEP 并未在动机中提及这些目标。我试图找到关于这些审议的任何 public 记录,但没有成功。
所以我的问题是:记录是否打算成为可能转向值类型的一部分,或者这些完全不相关的概念和未来的值类型可能看起来完全不同?
我问这个问题的动机:如果记录成为语言的永久部分,那么如果在未来的版本中有可能获得显着的性能优势,那么在代码中采用它们将是一种额外的激励。
注意:我可能不太正确,因为这是关于 Java 的未来动机或社区关于 值类型 的意图。答案是基于我个人的知识和互联网上公开的信息。
我们都知道 Java 社区如此庞大和成熟,除非另有说明,否则他们不会(也不能)为实验添加任何随机功能。牢记这一点,我记得OpenJDK网站上的this article在Java中简要描述了value types
的想法。这里要注意的一件事是,它是 2014 年 4 月的 written/updated,而 record
首次出现是在 2020 年 3 月的 Java 14。
但是在上面的文章中,他们在解释值类型时确实给出了record
的例子。它的大部分描述也与当前的 record
匹配。
The JVM type system is almost entirely nominal as opposed to structural. Likewise, components of value types should be identified by names, not just their element number. (This makes value types more like records than tuples.)
毫无意外,Brian Goetz 也是这篇文章的 co-author。
但宇宙中还有其他地方record
也被表示为data classes
。参见 this article, it's also written/updated by Brain. The interesting part is here。
Values Victor will say "a data class is really just a more transparent value type."
现在,综合考虑所有这些步骤,看起来 record
确实是由(或针对)元组、数据 classes、值类型等驱动的功能。 .. 但它们不能相互替代。
正如 Brain 在评论中提到的:-
A better way to interpret the documents cited here that tuple-like types are one possible use for value types, but by far not the only one. And it is possible to have record types that need an identity. So the two will commonly work together, but neither subsumes the other -- each brings something unique to the table.
针对您对性能提升的担忧,这里是 an article 比较了 Java 14 条记录(预览版)与传统 class 的性能。你可能会觉得很有趣。从上述 link.
的结果来看,我没有看到任何显着的性能改进
据我所知,栈比堆快很多。因此,由于 record
实际上只是一个特殊的 class,然后进入堆而不是堆栈(值 type/primitive 类型应该像 [=18 一样存在于堆栈中=],记住 Brian“代码像 class,工作起来像 int!”)。顺便说一句,这是我个人的看法,我在这里对堆栈和堆的陈述可能是错误的。我很乐意看到是否有人在这方面纠正我或支持我。
记录和原语 类(值类型的新名称)有很多共同点——它们隐含地是最终的并且浅层不可变。因此,将两者视为同一事物是可以理解的。现实中他们是不同的,两者都有发挥的余地co-exist,但也可以一起工作
这两种新的 类 都涉及某种限制,以换取某些好处。 (就像 enum
,您放弃对实例化的控制,并获得更精简的声明、switch
中的支持等奖励)
A record
要求您放弃扩展、可变性和将表示与 API 分离的能力。在 return 中,您可以获得构造函数、访问器、equals
、hashCode
等的实现。
A primitive class
要求您放弃身份,这包括放弃扩展性和可变性,以及其他一些事情(例如,同步)。在 return 中,您将获得一组不同的好处——扁平化表示、优化的调用序列以及 state-based equals
和 hashCode
.
如果您愿意做出两种妥协,您可以获得两种 套好处 -- 这将是 primitive record
。原始记录有很多用例,因此 类 今天的记录明天可能就是原始记录,而且会变得更快。
但是,我们不想强制所有记录都是原始记录或所有原始记录都是记录。有想要使用封装的原始 类 和想要身份的记录(因此它们可以组织成树或图),这很好。
免责声明: 此答案仅通过总结一些含义并提供一些示例来扩展其他答案。您不应依赖此信息做出任何决定,因为模式匹配和值类型仍会发生变化。
有两篇关于数据 class 也就是记录与值类型的有趣文档:
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
每个文档都包含一段关于记录和值类型之间差异的段落。
旧版本说
The lack of layout polymorphism means we have to give up something else: self-reference. A value type V cannot refer, directly or indirectly, to another V.
此外,
Unlike value types, data classes are well suited to representing tree and graph nodes.
然而,
But value classes need not give up any encapsulation, and in fact encapsulation is essential for some applications of value types
让我们澄清一下:
您将无法实现 node-based 复合数据结构,例如具有值类型的链表或分层树。但是,您可以为这些数据结构的元素使用值类型。
此外,与根本不支持的记录相反,值类型支持某些形式的封装。这意味着您可以在值类型中包含您尚未在 class header 中定义并且对值类型的用户隐藏的其他字段。记录不能这样做,因为它们的表示仅限于它们的 API,即它们的所有字段都在 class header 中声明(并且仅在那里!)。
让我们举一些例子来说明这一切。
例如您将能够创建包含记录但不包含值类型的复合逻辑表达式:
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 {}
这不适用于值类型,因为这需要相同值类型的 self-references 的能力。
您希望能够创建像“Not(Not(Val(true)))”这样的表达式。
例如您还可以使用记录来定义 class Fraction:
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() 添加到记录 Fraction。
每次调用它时,它总是会计算(并重新计算)相同的浮点值。
(默认情况下,记录和值类型是不可变的)。
但是,您不能以对用户隐藏的方式预先计算浮点值并将其存储在此记录中。
而且您不会喜欢在 class header 中将浮点值显式声明为第三个参数。
幸运的是,值类型可以做到这一点:
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 的实例并且可能将它们存储在 collections 中,
您将从扁平化的内存布局中获益良多。
这绝对是更喜欢值类型而不是记录的一个更重要的原因。
在某些情况下,您希望同时受益于记录和值类型。
例如。您可能想开发一款游戏,让您在地图上移动您的棋子。
前段时间您在列表中保存了移动历史记录,其中每个移动都存储了一个方向的多个步骤。并且您想现在根据该移动列表计算下一个位置。
如果你的 class Move 是值类型,那么列表可以使用扁平内存布局。
如果你的 class 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);
}
当然,还有许多其他方法可以实现相同的行为。
但是,记录和值类型为您提供了更多非常有用的实现选项。
JDK14 中引入的record
预览功能 (JEP 384) 是一项伟大的创新。它们使创建简单的不可变 类 变得容易得多,这些 类 是纯粹的值集合,不会丢失各种库中通用元组 类 固有的上下文。
Brian Goetz (https://openjdk.java.net/jeps/384) 撰写的 JEP 描述很好地解释了其意图。然而,我期待与值类型的最终引入有更紧密的联系。值类型的最初目标非常广泛:通过删除这些类型的对象不需要的所有开销(例如引用间接,同步)。此外,它还可以提供语法细节,例如 myPosition != yourPosition
而不是 !myPosition.equals(yourPosition)
.
似乎记录的限制与潜在值类型所需的限制类型非常接近。然而,JEP 并未在动机中提及这些目标。我试图找到关于这些审议的任何 public 记录,但没有成功。
所以我的问题是:记录是否打算成为可能转向值类型的一部分,或者这些完全不相关的概念和未来的值类型可能看起来完全不同?
我问这个问题的动机:如果记录成为语言的永久部分,那么如果在未来的版本中有可能获得显着的性能优势,那么在代码中采用它们将是一种额外的激励。
注意:我可能不太正确,因为这是关于 Java 的未来动机或社区关于 值类型 的意图。答案是基于我个人的知识和互联网上公开的信息。
我们都知道 Java 社区如此庞大和成熟,除非另有说明,否则他们不会(也不能)为实验添加任何随机功能。牢记这一点,我记得OpenJDK网站上的this article在Java中简要描述了value types
的想法。这里要注意的一件事是,它是 2014 年 4 月的 written/updated,而 record
首次出现是在 2020 年 3 月的 Java 14。
但是在上面的文章中,他们在解释值类型时确实给出了record
的例子。它的大部分描述也与当前的 record
匹配。
The JVM type system is almost entirely nominal as opposed to structural. Likewise, components of value types should be identified by names, not just their element number. (This makes value types more like records than tuples.)
毫无意外,Brian Goetz 也是这篇文章的 co-author。
但宇宙中还有其他地方record
也被表示为data classes
。参见 this article, it's also written/updated by Brain. The interesting part is here。
Values Victor will say "a data class is really just a more transparent value type."
现在,综合考虑所有这些步骤,看起来 record
确实是由(或针对)元组、数据 classes、值类型等驱动的功能。 .. 但它们不能相互替代。
正如 Brain 在评论中提到的:-
A better way to interpret the documents cited here that tuple-like types are one possible use for value types, but by far not the only one. And it is possible to have record types that need an identity. So the two will commonly work together, but neither subsumes the other -- each brings something unique to the table.
针对您对性能提升的担忧,这里是 an article 比较了 Java 14 条记录(预览版)与传统 class 的性能。你可能会觉得很有趣。从上述 link.
的结果来看,我没有看到任何显着的性能改进据我所知,栈比堆快很多。因此,由于 record
实际上只是一个特殊的 class,然后进入堆而不是堆栈(值 type/primitive 类型应该像 [=18 一样存在于堆栈中=],记住 Brian“代码像 class,工作起来像 int!”)。顺便说一句,这是我个人的看法,我在这里对堆栈和堆的陈述可能是错误的。我很乐意看到是否有人在这方面纠正我或支持我。
记录和原语 类(值类型的新名称)有很多共同点——它们隐含地是最终的并且浅层不可变。因此,将两者视为同一事物是可以理解的。现实中他们是不同的,两者都有发挥的余地co-exist,但也可以一起工作
这两种新的 类 都涉及某种限制,以换取某些好处。 (就像 enum
,您放弃对实例化的控制,并获得更精简的声明、switch
中的支持等奖励)
A record
要求您放弃扩展、可变性和将表示与 API 分离的能力。在 return 中,您可以获得构造函数、访问器、equals
、hashCode
等的实现。
A primitive class
要求您放弃身份,这包括放弃扩展性和可变性,以及其他一些事情(例如,同步)。在 return 中,您将获得一组不同的好处——扁平化表示、优化的调用序列以及 state-based equals
和 hashCode
.
如果您愿意做出两种妥协,您可以获得两种 套好处 -- 这将是 primitive record
。原始记录有很多用例,因此 类 今天的记录明天可能就是原始记录,而且会变得更快。
但是,我们不想强制所有记录都是原始记录或所有原始记录都是记录。有想要使用封装的原始 类 和想要身份的记录(因此它们可以组织成树或图),这很好。
免责声明: 此答案仅通过总结一些含义并提供一些示例来扩展其他答案。您不应依赖此信息做出任何决定,因为模式匹配和值类型仍会发生变化。
有两篇关于数据 class 也就是记录与值类型的有趣文档:
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
每个文档都包含一段关于记录和值类型之间差异的段落。 旧版本说
The lack of layout polymorphism means we have to give up something else: self-reference. A value type V cannot refer, directly or indirectly, to another V.
此外,
Unlike value types, data classes are well suited to representing tree and graph nodes.
然而,
But value classes need not give up any encapsulation, and in fact encapsulation is essential for some applications of value types
让我们澄清一下:
您将无法实现 node-based 复合数据结构,例如具有值类型的链表或分层树。但是,您可以为这些数据结构的元素使用值类型。 此外,与根本不支持的记录相反,值类型支持某些形式的封装。这意味着您可以在值类型中包含您尚未在 class header 中定义并且对值类型的用户隐藏的其他字段。记录不能这样做,因为它们的表示仅限于它们的 API,即它们的所有字段都在 class header 中声明(并且仅在那里!)。
让我们举一些例子来说明这一切。
例如您将能够创建包含记录但不包含值类型的复合逻辑表达式:
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 {}
这不适用于值类型,因为这需要相同值类型的 self-references 的能力。 您希望能够创建像“Not(Not(Val(true)))”这样的表达式。
例如您还可以使用记录来定义 class Fraction:
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() 添加到记录 Fraction。 每次调用它时,它总是会计算(并重新计算)相同的浮点值。 (默认情况下,记录和值类型是不可变的)。 但是,您不能以对用户隐藏的方式预先计算浮点值并将其存储在此记录中。 而且您不会喜欢在 class header 中将浮点值显式声明为第三个参数。 幸运的是,值类型可以做到这一点:
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 的实例并且可能将它们存储在 collections 中, 您将从扁平化的内存布局中获益良多。 这绝对是更喜欢值类型而不是记录的一个更重要的原因。
在某些情况下,您希望同时受益于记录和值类型。
例如。您可能想开发一款游戏,让您在地图上移动您的棋子。
前段时间您在列表中保存了移动历史记录,其中每个移动都存储了一个方向的多个步骤。并且您想现在根据该移动列表计算下一个位置。
如果你的 class Move 是值类型,那么列表可以使用扁平内存布局。
如果你的 class 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);
}
当然,还有许多其他方法可以实现相同的行为。 但是,记录和值类型为您提供了更多非常有用的实现选项。