Команда проекта Valhalla выпустила early-access сборку JDK с полной реализацией JEP 401 — value-классы и объекты теперь можно попробовать в действии! В новом переводе от команды Spring АйО — примеры использования, объяснение концепции, сравнение производительности с обычными объектами и практические советы для разработчиков.


Получение Early-Access сборок


Чтобы начать, перейдите на сайт jdk.java.net/valhalla и скачайте EA сборку JDK. Вы также можете ознакомиться с release notes`ами, чтобы понять, что включено в релиз.

Комментарий от Евгения Сулейманова

EA-сборки предназначены для экспериментов, API/семантика могут меняться; производственные нагрузки не рекомендуются

Чтобы начать пользоваться добавьте явные команды запуска с превью:

java --enable-preview

Распакуйте архив, поместите его в удобное место и используйте каталог bin для запуска таких команд, как java и javac. На своем Mac я установлю переменную окружения для удобного доступа к этим командам в приведённых ниже примерах:

% -> export jdk401="$PWD/jdk-26.jdk/Contents/Home/bin"

% -> "$jdk401"/java --version
openjdk 26-jep401ea2 2026-03-17
OpenJDK Runtime Environment (build 26-jep401ea2+1-1)
OpenJDK 64-Bit Server VM (build 26-jep401ea2+1-1, mixed mode, sharing)

Эксперименты с value-объектами


Как объясняется в JEP, value-объекты — это экземпляры value-классов, которые содержат только final поля и не обладают объектной идентичностью (object identity).

Комментарий от экспертов Михаила Поливахи и Федора Сазонова

Речь о том, что оператор double equals (т.е. ==) для  value объектов, конечно, работает, но по другому, отличному от обычных объектов принципу. На данный момент в англоязыной литературе используют термин "statewise equivalence", но терминология пока довольно шаткая и может меняться. Statewise equivalence декларирует, что оператор == будет проевалюирован в true в случае если:

  • Эти value объекты имеют один тип

  • Все примитивные поля этих value объектов содержат одинаковые значения

  • Все референсные поля при сравнении на == также евалюируются в true (т.е. идёт некая рекурсивная проверка)

Ряд классов JDK, включая Integer и LocalDate, становятся value-классами при запуске Java в режиме preview.

В JShell метод Objects.hasIdentity позволяет легко определить, какие объекты являются  value-объектами, а какие — обычными identity-объектами:

% -> "$jdk401"/jshell --enable-preview
|  Welcome to JShell -- Version 26-jep401ea2
|  For an introduction type: /help intro

jshell> Objects.hasIdentity(Integer.valueOf(123))
$1 ==> false

jshell> Objects.hasIdentity("abc")
$2 ==> true

jshell> Objects.hasIdentity(LocalDate.now())
$3 ==> false

jshell> Objects.hasIdentity(new ArrayList<>())
$4 ==> true

Value-объекты во многом ведут себя так же, как и identity-объекты. Однако есть одно отличие: оператор == не может определить, являются ли два value-объекта «одним и тем же объектом» — у них просто нет identity для сравнения. Вместо этого == проверяет, эквивалентны ли объекты по состоянию: принадлежат ли они к одному классу и содержат ли одинаковые значения полей.

jshell> LocalDate d1 = LocalDate.now()
d1 ==> 2025-10-23

jshell> LocalDate d2 = d1.plusDays(365)
d2 ==> 2026-10-23

jshell> LocalDate d3 = d2.minusDays(365)
d3 ==> 2025-10-23

jshell> d1 == d3
$8 ==> true

Эквивалентность по состоянию не заменяет хорошо продуманный метод equals, созданный автором класса.

Комментарий от Александра Шустанова

Например, два экземпляра BigDecimal с разными представлениями (new BigDecimal("1.0") и new BigDecimal("1.00")) имеют различное внутреннее состояние, но при этом логически равны.Поэтому, даже с появлением value-классов, всё ещё рекомендуется явно определять метод equals(), если у вашего типа есть особая семантика равенства, выходящая за рамки простого сравнения состояния.

В некоторых случаях два экземпляра value-класса с разными внутренними состояниями всё же должны считаться еквивалентными. Поэтому, как и прежде, рекомендуется избегать оператора == и предпочитать метод equals для сравнения объектов.

Вы можете объявлять собственные value-классы с помощью ключевого слова value. Многие record`ы хорошо подходят для преобразования в value-классы:

jshell> value record Point(int x, int y) {}
|  created record Point

jshell> Point p = new Point(17, 3)
p ==> Point[x=17, y=3]

jshell> Objects.hasIdentity(p)
$11 ==> false

jshell> new Point(17, 3) == p
$12 ==> true

Производительность value-объектов

Зачем вообще объявлять value-класс, если можно использовать обычный класс с  нормальным и всем привычным поведением?

Одна из причин — семантическая. Если ваш класс представляет неизменяемые значения предметной области, которые взаимозаменяемы при одинаковом состоянии, то предоставление таким объектам всех признаков идентичности лишь усложняет систему без необходимости. Лучше объявить value-класс и полностью отказаться от identity.

Но самая веская причина — в том, что JVM может очень агрессивно оптимизировать value-объекты таким образом, какой невозможен для обычных объектов. Например, ссылка на value-объект не обязана указывать на уникальное место в памяти. Вместо этого состояние объекта может быть “заинлайнено” в хипе, то есть минуя непосредственно сам pointer.

Комментарий от Федора Сазонова и Михаила Поливахи

Flattening значений в хипе для value объектов визуально можно хорошо представить таким образом:

Как вы можете видеть, в комплексных объектах (например, как указано выше - в массиве), в heap-е отсутствуют ссылки как таковые. В базовом сценарии, для обычных объектов, layout бы выглядел таким образом:  

Однако, и это ещё не все. Возможны и ещё более агрессивные оптимизации. Например, довольно часто, в случае, если объект является довольно компактным, HotSpot сможет не просто не аллоцировать объект непосредственно в heap (escape analysis), а ещё и раскладывать объект на регистры того или иного ядра процессора (за счет отсутствия identity и соот-но изменения семантики double equals). Это, очевидно, позволяет совершать крайне быстрые доступы к полям объекта.

Этот приём называется уплощением кучи (heap flattening) и может значительно снизить затраты на загрузку объектов из памяти.

Комментарий от Евгения Сулейманова

Уплощение - право VM, а не гарантия. Зависит от профиля, структуры памяти и null-ов.

Для теста создадим очень большой массив value-объектов LocalDate и просуммируем значения их годов. Чтобы смоделировать реалистичное распределение объектов в памяти, заполним массив из неотсортированного HashSet с объектами LocalDate. Выполним простейшее профилирование, измерив время выполнения итерации по массиву (Примечание: для более точного профилирования следует использовать JMH).

void main(String... args) {
    int size = 50_000_000;
    if (args.length > 0) size = Integer.parseInt(args[0]);
    LocalDate[] arr = makeArray(size);
    for (int i = 1; i <= 5; i++) {
        double t = time(() -> sumYears(arr));
        IO.println("Attempt " + i + ": " + t);
    }
}

/// Expensive task to be timed
long sumYears(LocalDate[] dates) {
    long result = 0;
    for (var d : dates) result += d.getYear();
    return result;
}

/// Make an array of LocalDates, unpredictably ordered
LocalDate[] makeArray(int size) {
    HashSet<LocalDate> set = new HashSet<>();
    for (int i = 0; i < size; i++) set.add(LocalDate.ofEpochDay(i));
    return set.toArray(new LocalDate[0]);
}

/// Run a task and report the elapsed wall-clock time in ms
double time(Runnable r) {
    var start = Instant.now();
    r.run();
    var end = Instant.now();
    return Duration.between(start, end).toNanos() / 1_000_000.0;
}

В качестве базовой линии я поместил следующий код в файл DateTest.java и запустил его на своём MacBook Pro без включённых preview фич. Полученные результаты:

% -> "$jdk401"/java DateTest.java
Attempt 1: 82.703
Attempt 2: 77.716
Attempt 3: 74.959
Attempt 4: 71.962
Attempt 5: 71.915

Когда я включаю preview-фичи, LocalDate становится value-классом, и его экземпляры могут быть напрямую уплощены в массиве. Благодаря сокращению количества загрузок из памяти JVM удаётся добиться почти трёхкратного прироста производительности:

% -> "$jdk401"/java --enable-preview DateTest.java
Attempt 1: 41.959
Attempt 2: 38.992
Attempt 3: 25.466
Attempt 4: 28.404
Attempt 5: 25.027

Результаты могут варьироваться в зависимости от машины и размера массива. Однако главное здесь — использование value-объектов в вычислениях, критичных к производительности, позволяет JVM применять новые, значительные оптимизации, невозможные для объектов с identity.

Следующие шаги

Это бета-версия программного обеспечения, и в ней, безусловно, могут быть ошибки и неожиданные проблемы с производительностью.

Разумеется, простое добавление ключевого слова value в код не устранит автоматически все узкие места в производительности программы. Пользователям рекомендуется внимательно ознакомиться с JEP 401, чтобы лучше понять, какие именно оптимизации возможны, и использовать инструменты профилирования, такие как JDK Flight Recorder, чтобы увидеть, как value-объекты влияют на работу их приложений.

Мини-FAQ ("что это и зачем") от Евгения Сулейманова
  1. Что такое value-классы/объекты простыми словами?

    Это обычные классы без объектной идентичности: все поля “final”, “==” сравнивает состояние, синхронизация/мониторы - недоступны. Их можно оптимальнее хранить/перемещать в памяти, чем identity-объекты.

  2. Зачем мне это как разработчику?

    Чтобы честно моделировать "значения домена" (деньги, дата, точка, диапазон) без накладных расходов идентичности, и чтобы JVM могла применять уплотнение/уплощение и сокращать лишние разыменования (особенно в массивах/дженериках). Это дает шанс на заметный выигрыш в latency/throughput - где это уместно.

  3. Меняется ли поведение “==”/”equals”?

    Для value-объектов “==” сравнивает поля (состояние), а у identity-объектов - как раньше, ссылки. В прикладном коде по-прежнему предпочитайте “equals”, т.к. бизнес-семантика равенства может отличаться от полного равенства полей.

  4. Можно ли просто добавить “value” "везде" и стать быстрее?

    Нет. Это превью-функциональность, оптимизации - право JVM, а не контракт. Есть ограничения совместимости (мониторы, weak-refs, прокси). Профилируйте (JFR/JMH), а мигрируйте только те типы, у которых равенство-по-состоянию естественно и идентичность не нужна.

  5. Что с экосистемой и как безопасно начинать?

    - Не трогайте Entity-классы и все, что ожидает identity.
    - Для DTO/встраиваемых типов - можно пробовать “record”/”value record”/value-класс.
    - Проверяйте “Objects.hasIdentity”, ловите “IdentityException” в тестах.


Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Комментарии (2)


  1. Bifurcated
    28.10.2025 18:15

    А как работает присвоение null таким value классам? Помню хотели от null отказаться, но проблема, что не понятно как инициализировать


    1. Yami-no-Ryuu
      28.10.2025 18:15

      Пустой объект типа? Наверное. Похоже в HotSpot не тегированные указатели, иначе можно было помечать в младших битах.