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

2022-08-31 15:42:10

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

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

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

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

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


答案 1

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

这两种新的类都涉及某种限制,以换取某些好处。(就像 ,您放弃了对实例化的控制,并获得了更简化的声明、支持等奖励。enumswitch

A 要求您放弃扩展、可变性以及将表示形式与 API 分离的能力。作为回报,您将获得构造函数、访问器、、 等的实现。recordequalshashCode

A 要求您放弃身份,包括放弃扩展和可变性,以及其他一些事情(例如,同步)。作为回报,您将获得一组不同的好处 - 扁平化表示,优化的调用序列以及基于状态的和。primitive classequalshashCode

如果您愿意同时做出这两种妥协,则可以同时获得两组好处 - 这将是一个.原始记录有很多用例,因此今天是记录的类明天可能是原始记录,并且会变得更快。primitive record

但是,我们不希望强制所有记录都是原始记录,或者所有基元都是记录。有些基元类想要使用封装,有些记录需要标识(因此它们可以组织成树或图形),这很好。


答案 2

免責聲明:这个答案只是通过总结一些含义并给出一些例子来扩展其他答案。您不应根据此信息做出任何决策,因为模式匹配和值类型仍然是更改的对象。

关于数据类(即记录与值类型)有两个有趣的文档: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);
}

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


推荐