JDK 14中引入的记录
预览功能(JEP 384)是一个伟大的创新。它们使得创建简单的不可变类变得更加容易,这些类是纯值集合,而不会丢失各种库中泛型元组类固有的上下文。
Brian Goetz写的对JEP的描述(https://openjdk.java.net/jeps/384)很好地解释了意图。然而,我期望与值类型的最终引入有更密切的联系。值类型的最初目标非常广泛:通过消除这些类型对象(例如,引用间接寻址、同步)不需要的所有开销,本质上允许对其值至关重要的对象进行潜在的重大性能改进。此外,它还可以提供语法细节,如myPosition!=你的位置
而不是!我的位置。等于(你的位置)
。
似乎记录的限制与潜在值类型所需的限制类型非常接近。然而,JEP在动机中没有提到这些目标。我试图找到有关这些审议的任何公开记录,但没有成功。
所以我的问题是:记录是打算成为向价值类型转变的一部分,还是这些完全不相关的概念和未来的价值类型看起来完全不同?
我提出这个问题的动机是:如果记录成为语言的一个永久部分,那么如果在未来的版本中有可能获得显著的性能优势,那么在代码中采用它们将是一个额外的动机。
注意:我可能不太正确,因为这是关于Java的未来动机或社区对价值类型的意图。答案是基于我的个人知识和互联网上公开提供的信息。
我们都知道Java社区是如此的庞大和成熟,以至于他们直到现在才(也不能)为实验添加任何随机特性
但在上面的文章中,他们在解释值类型时给出了record
的示例。它的大部分描述也与当前的记录匹配。
JVM类型系统几乎完全是名义上的,而不是结构上的。同样,值类型的组件应该通过名称来标识,而不仅仅是它们的元素号。(这使得值类型更像记录而不是元组。)
毫无疑问,布莱恩·戈茨也是这篇文章的合著者。
但是在宇宙中的其他地方,
记录
也表示为数据类
。看到这篇文章,它也是由大脑编写/更新的。有趣的部分在这里。
值维克多会说“数据类实际上只是一种更透明的值类型。”
现在,综合考虑所有这些步骤,它看起来确实像是一个由元组、数据类、值类型等驱动的特性。。。等但它们不是彼此的替代品。
正如Brain在评论中提到的:-
更好的解释这里引用的文档的方法是,类元组类型是值类型的一种可能用途,但到目前为止不是唯一的用途。而且可能有需要标识的记录类型。因此,两者通常会一起工作,但两者都不包含对方——每一个都会带来一些独特的东西。
关于性能提高的问题,这里有一篇文章比较了Java14记录(预览)和传统类的性能。你可能会觉得很有趣。从上述链接的结果中,我没有看到任何显著的性能改进。
据我所知,堆栈的速度明显快于堆。因此,由于
record
实际上只是一个特殊的类,然后它进入堆而不是堆栈(值类型/基元类型应该像int
一样存在于堆栈中,请记住Brian“像类一样的代码,像int一样工作!”)。顺便说一句,这是我个人的观点,我在这里对stack和heap的陈述可能是错误的。如果有人在这方面纠正我或支持我,我将非常高兴。
免责声明:此答案仅通过总结一些含义和给出一些示例来扩展其他答案。您不应该依靠这些信息做出任何决策,因为模式匹配和值类型仍然是变化的主题。
关于数据类aka记录与值类型,有两个有趣的文档:2018年2月的旧版本
http://cr.openjdk.java.net/~briangoetz/amber/datum_2。html#数据类是否与值类型相同以及2019年2月的更新版本
https://cr.openjdk.java.net/~briangoetz/amber/datum。html#记录是否与值类型相同
每个文档都包含一段关于记录和值类型之间差异的内容。旧版本说
缺乏布局多态性意味着我们必须放弃其他东西:自我引用。值类型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 {}
这将不适用于值类型,因为这需要相同值类型的自引用能力。您希望能够创建像“不(不(瓦尔(真))”这样的表达式。
例如,您也可以使用记录来定义类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
}
计算那个分数的浮点值怎么样?您可以将一个方法作为floatingpoint()添加到记录分数中。每次调用它时,它都会计算(并重新计算)相同的浮点值。(默认情况下,记录和值类型是不可变的)。但是,不能以对用户隐藏的方式在此记录中预先计算和存储浮点值。并且您不希望显式地将浮点值声明为类头中的第三个参数。幸运的是,值类型可以做到这一点:
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
}
当然,隐藏字段可能是您希望使用值类型的原因之一。它们只是一个方面,可能只是一个次要方面。如果您创建了许多分数实例,并可能将它们存储在集合中,那么扁平化内存布局将使您受益匪浅。这无疑是更喜欢值类型而不是记录的一个更重要的原因。
在某些情况下,你想从记录和值类型中受益。例如,你可能想开发一个游戏,在这个游戏中你可以在地图上移动你的棋子。前一段时间,您在列表中保存了移动历史,其中每个移动都存储了一个方向的许多步骤。您现在想要根据移动列表计算下一个位置。
如果你的类移动是一个值类型,那么这个列表可以使用扁平的内存布局。
如果你的类移动同时也是一个记录可以使用模式匹配,而不需要定义显式解构模式。
您的代码可以看起来像这样:
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);
}
当然,还有许多其他方法可以实现相同的行为。然而,记录和值类型为实现提供了更多的选项,这可能非常有用。
记录和基元类(值类型的新名称)有很多共同点——它们是隐式的最终类,并且是不可变的。因此,可以理解的是,两者可能被视为同一件事。事实上,它们是不同的,它们有共存的空间,但它们也可以一起工作。
这两种新类别都涉及某种限制,以换取某些利益。(就像enum
一样,在这里您放弃了对实例化的控制,并获得了更简化的声明、对Switch
的支持等。)
记录
要求您放弃扩展性、易变性以及将表示与API解耦的能力。作为回报,您可以得到构造函数、访问器、equals
、hashCode
等的实现。
一个基本类
要求您放弃标识,包括放弃扩展和可变性,以及其他一些东西(例如同步)。作为回报,您可以获得一系列不同的好处——扁平化表示、优化调用序列以及基于状态的equals
和hashCode
。
如果您愿意做出这两种妥协,那么您可以同时获得这两组好处--这将是一个原始记录
。原始记录有很多用例,所以今天是记录的类明天可能是原始记录,而且速度会更快。
但是,我们不想强制所有记录都是原始的,或者所有原始都是记录。有些原始类想要使用封装,有些记录想要标识(这样它们就可以组织成树或图),这很好。
在可能的副本上: 此线程不是在询问如何扩展类。它问为什么一个声明为的类可能会扩展另一个类。 从该线程: <code>final</code>类只是一个不能扩展的类。 但是,我有一个帮助程序类,我声明它是,并了另一个类: Eclipse没有检测到任何错误。我已经测试了这个类,并且PDF是成功生成的,没有错误。 为什么我能够课程,而理论上我不应该延长? (如果重要的话,我正在使用Java7。)
问题内容: 是否有任何条件最终可能无法在Java中运行?谢谢。 问题答案: 注意:如果在执行try或catch代码时JVM退出,则finally块可能不会执行。同样,如果执行try或catch代码的线程被中断或杀死,即使整个应用程序继续运行,finally块也可能不会执行。 我不知道finally块无法执行任何其他方式…
问题内容: 可以从许多线程访问类。在这种情况下,必须是记录器还是最终的和静态的?谢谢。 问题答案: 所有主要的Java日志记录程序包(等)都是同步的并且是线程安全的。即使从多个线程调用该类,每个类的记录器的标准模式也可以。
从…起http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html,我发现JVM记录生成依赖于对象: 在下一个小GC,同样的事情发生在伊甸园空间。将删除未引用的对象,并将引用的对象移动到幸存者空间。然而,在这种情况下,它们被移动到第二个幸存者空间(S1)。此外,来自第一幸存者空间(S0)上最后一个次要G
我有一个类似以下的Java类: 我已经学会了如何通过反射应用编程接口改变“最终”字段的可访问性,但是这对类也是可能的吗?我可以在运行时将最终类变成非最终类吗?
问题内容: 在Java中,静态最终变量是常量,并且约定应使用大写形式。但是,我已经看到大多数人以小写形式声明记录器,这在PMD中是违反的。 例如: 只需在Google或SO中搜索“静态最终记录器”,您便会自己看到它。 我们应该改用LOGGER吗? 问题答案: 记录器引用不是常量,而是最终引用,并且不应大写。常数VALUE应该为大写。