Предполагается ли, что записи Java в конечном итоге станут типами значений?

В recordфункция предварительного просмотра (JEP 384), представленная в JDK 14, является большим нововведением. Они значительно упрощают создание простых неизменяемых классов, которые представляют собой чистую коллекцию значений без потери контекста, присущего универсальным классам кортежей в различных библиотеках.

Описание JEP, написанное Брайаном Гетцем (https://openjdk.java.net/jeps/384), очень хорошо объясняет цель. Однако я ожидал более тесной связи с возможным введением типов значений. Первоначальные цели типов значений были довольно обширными: по существу, обеспечение потенциально значительных улучшений производительности для объектов, значение которых - это все, что имеет значение, путем устранения всех накладных расходов, не требуемых для этих типов объектов (например, косвенное обращение к ссылкам, синхронизация). Кроме того, он может предоставлять синтаксические тонкости, такие какmyPosition != yourPosition вместо того !myPosition.equals(yourPosition).

Кажется, что ограничения записей очень близки к типам ограничений, которые потребуются для потенциального типа значения. Тем не менее, JEP не ссылается на эти цели в мотивации. Я безуспешно пытался найти какие-либо публичные записи об этих обсуждениях.

Итак, мой вопрос: предназначены ли записи для того, чтобы стать частью возможного перехода к типам значений, или эти совершенно не связанные концепции и будущие типы значений могут выглядеть совершенно иначе?

Моя мотивация задать вопрос: если записи станут постоянной частью языка, это будет дополнительным стимулом для принятия их в код, если есть возможность значительного повышения производительности в будущем выпуске.

3 ответа

Решение

Записи и встроенные классы (новое имя для типов значений) имеют много общего - они неявно окончательные и поверхностно неизменяемые. Итак, понятно, что эти два могут рассматриваться как одно и то же. На самом деле они разные, и им обоим есть место сосуществовать, но они также могут работать вместе.

Оба этих новых вида классов предполагают определенные ограничения в обмен на определенные преимущества. (Какenum, где вы отказываетесь от контроля над созданием экземпляров и получаете более оптимизированное объявление, поддержку в switch, так далее.)

А recordтребует, чтобы вы отказались от расширений, изменчивости и возможности отделить представление от API. Взамен вы получаете реализации конструкторов, средств доступа,equals, hashCode, и более.

An inline classтребует, чтобы вы отказались от идентичности, что включает отказ от расширений и изменчивости, а также от некоторых других вещей (например, от синхронизации). Взамен вы получаете другой набор преимуществ - плоское представление, оптимизированные последовательности вызовов и основанные на состоянииequals а также hashCode.

Если вы готовы пойти на оба компромисса, вы можете получить оба набора преимуществ - это было быinline record. Существует множество вариантов использования встроенных записей, поэтому классы, которые являются записями сегодня, завтра могут стать встроенными записями, и они будут только быстрее.

Но мы не хотим, чтобы все записи были встроенными или чтобы все встроенные были записями. Есть встроенные классы, которые хотят использовать инкапсуляцию, и записи, которым нужна идентичность (чтобы их можно было организовать в деревья или графы), и это нормально.

Отказ от ответственности: этот ответ только расширяет другие ответы, суммируя некоторые последствия и приводя несколько примеров. Вы не должны принимать никаких решений, полагаясь на эту информацию, потому что сопоставление с образцом и типы значений все еще могут изменяться.

Есть два интересных документа о классах данных, известных как записи и типы значений: Старая версия от февраля 2018 г.
http://cr.openjdk.java.net/~briangoetz/amber/datum_2.html#are-data-classes-the-same-as-value-types
и более новая версия от февраля 2019 г.
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)))».

Например, вы также можете использовать записи для определения класса 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 . И он всегда будет вычислять (и пересчитывать) одно и то же значение с плавающей запятой при каждом вызове. (Типы записей и значений по умолчанию неизменяемы). Однако вы не можете предварительно вычислить и сохранить значение с плавающей запятой внутри этой записи скрытым от пользователя способом. И вы не захотите явно объявлять значение с плавающей запятой в качестве третьего параметра в заголовке класса. К счастью, типы значений могут это сделать:

      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);
}

Конечно, есть много других способов реализовать такое же поведение. Однако записи и типы значений дают вам еще несколько вариантов реализации, которые могут быть очень полезными.

Примечание. Возможно, я не настолько прав, поскольку речь идет о будущих мотивах Java или о намерениях сообщества в отношении типов значений. Ответ основан на моих личных знаниях и информации, доступной в открытом доступе в Интернете.

Все мы знаем, что сообщество Java настолько велико и достаточно зрелое, что они не добавляют (и не могут) добавлять какие-либо случайные функции для экспериментов до тех пор, пока не будет указано иное. Помня об этом, я помню эту статью на веб-сайте OpenJdk, в которой кратко описывается идеяvalue typesв Java. Следует отметить, что он написан / обновлен в апреле 2014 года, аrecord впервые вышла на Java 14 в марте 2020 года.

Но в самой статье выше был приведен пример recordпри объяснении типов значений. Большая часть его описания соответствует текущемуrecord также.

Система типов JVM почти полностью номинальная, а не структурная. Точно так же компоненты типов значений следует идентифицировать по именам, а не только по номерам их элементов. (Это делает типы значений больше похожими на записи, чем на кортежи.)

И неудивительно, что Брайан Гетц также был соавтором этой статьи.

Но есть и другие места во Вселенной, где record также представлен как data classes. См. Эту статью, она также написана / обновлена ​​Brain. Самое интересное здесь.

Значения Виктор скажет, что "класс данных - это просто более прозрачный тип значения".

Теперь, если рассматривать все эти шаги вместе, это выглядит как record - это функция, мотивированная (или для) кортежами, классами данных, типами значений и т. д.... и т. д. в Java, которая имеет смысл иметь только ОДНУ функцию, которую можно было бы воспринимать одновременно.

Что касается вашего беспокойства по поводу повышения производительности, вот статья, в которой сравнивается производительность записей Java 14 (предварительная версия) и традиционного класса. Возможно, вам это будет интересно. Я не увидел каких-либо значительных улучшений производительности по результатам приведенной выше ссылки.

Насколько мне известно, стек значительно быстрее, чем куча. Так в связи с тем, чтоrecord на самом деле только специальный класс, который затем попадает в кучу, а не в стек (тип значения / примитивный тип должен находиться в стеке, как int, помните Брайана: "Кодирует как класс, работает как int!"). Кстати, это мое личное мнение, я могу ошибаться в своих утверждениях о стеке и куче здесь. Я буду более чем счастлив увидеть, поправит ли меня кто-нибудь или поддержит меня в этом вопросе.

Другие вопросы по тегам