当前位置: 首页 > 知识库问答 >
问题:

Java记录是否打算最终成为值类型?

熊嘉茂
2023-03-14

JDK 14中引入的记录预览功能(JEP 384)是一个伟大的创新。它们使得创建简单的不可变类变得更加容易,这些类是纯值集合,而不会丢失各种库中泛型元组类固有的上下文。

Brian Goetz写的对JEP的描述(https://openjdk.java.net/jeps/384)很好地解释了意图。然而,我期望与值类型的最终引入有更密切的联系。值类型的最初目标非常广泛:通过消除这些类型对象(例如,引用间接寻址、同步)不需要的所有开销,本质上允许对其值至关重要的对象进行潜在的重大性能改进。此外,它还可以提供语法细节,如myPosition!=你的位置而不是!我的位置。等于(你的位置)

似乎记录的限制与潜在值类型所需的限制类型非常接近。然而,JEP在动机中没有提到这些目标。我试图找到有关这些审议的任何公开记录,但没有成功。

所以我的问题是:记录是打算成为向价值类型转变的一部分,还是这些完全不相关的概念和未来的价值类型看起来完全不同?

我提出这个问题的动机是:如果记录成为语言的一个永久部分,那么如果在未来的版本中有可能获得显著的性能优势,那么在代码中采用它们将是一个额外的动机。

共有3个答案

蔚和安
2023-03-14

注意:我可能不太正确,因为这是关于Java的未来动机或社区对价值类型的意图。答案是基于我的个人知识和互联网上公开提供的信息。

我们都知道Java社区是如此的庞大和成熟,以至于他们直到现在才(也不能)为实验添加任何随机特性

但在上面的文章中,他们在解释值类型时给出了record的示例。它的大部分描述也与当前的记录匹配。

JVM类型系统几乎完全是名义上的,而不是结构上的。同样,值类型的组件应该通过名称来标识,而不仅仅是它们的元素号。(这使得值类型更像记录而不是元组。)

毫无疑问,布莱恩·戈茨也是这篇文章的合著者。

但是在宇宙中的其他地方,记录也表示为数据类。看到这篇文章,它也是由大脑编写/更新的。有趣的部分在这里。

值维克多会说“数据类实际上只是一种更透明的值类型。”

现在,综合考虑所有这些步骤,它看起来确实像是一个由元组、数据类、值类型等驱动的特性。。。等但它们不是彼此的替代品。

正如Brain在评论中提到的:-

更好的解释这里引用的文档的方法是,类元组类型是值类型的一种可能用途,但到目前为止不是唯一的用途。而且可能有需要标识的记录类型。因此,两者通常会一起工作,但两者都不包含对方——每一个都会带来一些独特的东西。

关于性能提高的问题,这里有一篇文章比较了Java14记录(预览)和传统类的性能。你可能会觉得很有趣。从上述链接的结果中,我没有看到任何显著的性能改进。

据我所知,堆栈的速度明显快于堆。因此,由于record实际上只是一个特殊的类,然后它进入堆而不是堆栈(值类型/基元类型应该像int一样存在于堆栈中,请记住Brian“像类一样的代码,像int一样工作!”)。顺便说一句,这是我个人的观点,我在这里对stack和heap的陈述可能是错误的。如果有人在这方面纠正我或支持我,我将非常高兴。

韦棋
2023-03-14

免责声明:此答案仅通过总结一些含义和给出一些示例来扩展其他答案。您不应该依靠这些信息做出任何决策,因为模式匹配和值类型仍然是变化的主题。

关于数据类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);
}

当然,还有许多其他方法可以实现相同的行为。然而,记录和值类型为实现提供了更多的选项,这可能非常有用。

薛利
2023-03-14

记录和基元类(值类型的新名称)有很多共同点——它们是隐式的最终类,并且是不可变的。因此,可以理解的是,两者可能被视为同一件事。事实上,它们是不同的,它们有共存的空间,但它们也可以一起工作。

这两种新类别都涉及某种限制,以换取某些利益。(就像enum一样,在这里您放弃了对实例化的控制,并获得了更简化的声明、对Switch的支持等。)

记录要求您放弃扩展性、易变性以及将表示与API解耦的能力。作为回报,您可以得到构造函数、访问器、equalshashCode等的实现。

一个基本类要求您放弃标识,包括放弃扩展和可变性,以及其他一些东西(例如同步)。作为回报,您可以获得一系列不同的好处——扁平化表示、优化调用序列以及基于状态的equalshashCode

如果您愿意做出这两种妥协,那么您可以同时获得这两组好处--这将是一个原始记录。原始记录有很多用例,所以今天是记录的类明天可能是原始记录,而且速度会更快。

但是,我们不想强制所有记录都是原始的,或者所有原始都是记录。有些原始类想要使用封装,有些记录想要标识(这样它们就可以组织成树或图),这很好。

 类似资料:
  • 在可能的副本上: 此线程不是在询问如何扩展类。它问为什么一个声明为的类可能会扩展另一个类。 从该线程: <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应该为大写。