← Заметки

Java Project Valhalla: объекты-значения, record и предварительная версия JDK 28

На этой странице

Project Valhalla — проект OpenJDK, который позволяет данным, не нуждающимся в идентичности объекта, использовать объекты-значения, предоставляя JVM больше свободы для эффективного хранения и передачи. По состоянию на 31 октября 2026 года страница проекта и JEP 401 показывают, что первая предварительная версия объектов-значений интегрирована в JDK 28. Это ещё не окончательная возможность. Пример ниже запускался на JDK 28 EA Build 21.

Зачем классы-значения, если в Java уже есть record?

Обычные объекты имеют идентичность даже при совпадении полей. Изменяемым счетам и объектам блокировки это различие нужно, но таким данным, как координата (3, 4), может требоваться только значение.

record объявляет компоненты данных и предоставляет методы доступа, конструкторы и методы вроде equals(). В существующих обычных record оператор == по-прежнему различает два отдельно созданных объекта с совпадающими компонентами. Модификатор value делает отдельный выбор: удаляет идентичность объекта. Его можно сочетать с record как value record.

Объявление Значение в этом примере
record IdentityPoint(int x, int y) Сохраняет разные идентичности объектов даже при совпадающих компонентах
value record ValuePoint(int x, int y) Экземпляры одного класса с одинаковым состоянием полей не различаются по идентичности
value record Label(String text) Не имеет идентичности, но результаты == и equals() всё равно могут различаться

Поля экземпляров класса-значения являются final. Это не делает объекты по ссылкам автоматически глубоко неизменяемыми: изменяемая коллекция в поле всё ещё может менять своё содержимое. Объекты-значения по-прежнему обрабатываются через ссылки на объекты; они не становятся примитивными типами.

Попробуйте предварительную версию JDK 28

Создание обычных record и record-значений, затем последовательная проверка равенства, идентичности, ссылочных полей, null в массиве и ограничения монитора

Схема соответствует коду ниже. Label содержит разные объекты String с одинаковым содержимым, чтобы показать сравнение ссылочных полей. В конце программа проверяет, что помещение объекта-значения в переменную Object не делает его допустимым монитором, и обрабатывает ожидаемое исключение.

Используйте отдельный каталог для эксперимента, где javac и java указывают на одну сборку JDK 28 EA. Сравните загрузку с её официальным SHA-256 и подтвердите сборку через java --version. JDK 17 и 21 не могут скомпилировать этот синтаксис. Флаги предварительных возможностей нужны и при компиляции, и при запуске.

ValhallaExample.java

import java.util.Objects;

public class ValhallaExample {
    record IdentityPoint(int x, int y) {}
    value record ValuePoint(int x, int y) {}
    value record Label(String text) {}

    public static void main(String[] args) {
        IdentityPoint a = new IdentityPoint(3, 4);
        IdentityPoint b = new IdentityPoint(3, 4);
        ValuePoint p = new ValuePoint(3, 4);
        ValuePoint q = new ValuePoint(3, 4);
        System.out.println("record == " + (a == b));
        System.out.println("record equals " + a.equals(b));
        System.out.println("value == " + (p == q));
        System.out.println("value equals " + p.equals(q));
        System.out.println("record identity " + Objects.hasIdentity(a));
        System.out.println("value identity " + Objects.hasIdentity(p));

        Label first = new Label(new String(new char[] {'x'}));
        Label second = new Label(new String(new char[] {'x'}));
        System.out.println("label == " + (first == second));
        System.out.println("label equals " + first.equals(second));

        ValuePoint[] points = new ValuePoint[1];
        System.out.println("array slot null " + (points[0] == null));
        Object monitor = p;
        try {
            synchronized (monitor) {
                throw new AssertionError("A value object must not be a monitor");
            }
        } catch (IdentityException expected) {
            System.out.println("monitor " + expected.getClass().getSimpleName());
        }
    }
}
javac --enable-preview --release 28 ValhallaExample.java
java --enable-preview ValhallaExample

Этот вывод наблюдался на OpenJDK 28-ea+21-1707 для macOS AArch64. Сообщение компилятора о предварительных возможностях не входит в этот стандартный вывод.

record == false
record equals true
value == true
value equals true
record identity true
value identity false
label == false
label equals true
array slot null true
monitor IdentityException

Обычные record возвращают true для equals(), но false для ==. Record-значения имеют только целочисленные поля, поэтому при совпадающем состоянии полей оба сравнения возвращают true. Objects.hasIdentity() также показывает это различие.

Следует ли сравнивать объекты-значения через ==?

Продолжайте использовать equals() для равенства в предметной области. Для объектов-значений == сравнивает состояние полей в пределах одного класса-значения и рекурсивно применяет == к ссылочным полям. Объекты с идентичностью, на которые ведут ссылки, остаются различимыми, если это отдельные объекты, даже при совпадающем содержимом. Примитивные поля сравниваются по битовому представлению, что также может отличаться от семантического равенства чисел с плавающей точкой. Точные правила см. в правилах сравнения объектов-значений JEP 401.

Два объекта Label содержат строки с одинаковым содержимым, поэтому сгенерированный equals() record возвращает true. Их строки — разные объекты с идентичностью, поэтому == возвращает false. Не трактуйте value как требование сравнивать каждое ссылочное поле по содержимому.

Ячейка нового массива значений равна null, а динамическое использование объекта-значения как монитора вызывает IdentityException. Прямая блокировка выражения, тип которого явно является классом-значением, вместо этого может быть отклонена при компиляции. Перед миграцией проверьте операции, зависящие от идентичности, такие как блокировки и кеши на основе идентичности.

Даёт ли это сразу экономию памяти или List<int>?

Удаление идентичности даёт JVM возможность применять flattening — представление ссылки через состояние полей вместо указателя — и scalarization — передачу значений отдельными компонентами в регистрах и подобных местах. Язык не гарантирует конкретную компоновку, устранение выделений памяти или ускорение. Вывод выше проверяет семантику; он не измеряет использование кучи или пропускную способность.

Valhalla охватывает несколько выпусков JDK. Официальный план отделяет объекты-значения от хранения с запретом null, улучшений массивов, объединения примитивов и классов и Parametric JVM для специализации обобщений. Один JEP 401 не завершает реализацию List<int> и не гарантирует непрерывное хранение каждого массива. Сопутствующий JEP 539 добавляет поддержку строгой инициализации полей в JVM; он не вводит произвольный модификатор полей strict в исходном коде Java.

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

Обсуждение

Посмотреть на GitHub ↗