В Java наконец появляется ответ на старую проблему: полноценные классы часто слишком дорогие для памяти и процессора.

Пример - массив из миллиона, например, точек Point. Сейчас это обычно не миллион точек подряд, а миллион ссылок на объекты в куче. У каждого объекта есть служебные данные, его нужно создать, потом убрать сборщиком мусора. Плюс процессор постоянно прыгает по памяти, а это медленно.

Project Valhalla добавляет value class. Это обычный на вид класс с полями, конструктором и методами, но без идентичности объекта. JVM сможет хранить такие данные плотнее: например, прямо внутри массива, без отдельного объекта для каждого значения.

JEP 401 планируют включить в JDK 28 как preview. Это еще не финал: value class пока может быть null, а полная поддержка быстрых generics и плотных коллекций появится позже. Но первый рабочий шаг Valhalla уже близко.

15 июня инженер Oracle Луис Фолтан подтвердил то, во что значительная часть индустрии уже перестала верить: JEP 401: Value Classes and Objects будет интегрирован в основной репозиторий OpenJDK и нацелен на JDK 28.

Изменение настолько масштабное, что остальных коммиттеров попросили воздержаться от крупных коммитов на время интеграции. Один только pull request добавляет свыше 197 тысяч строк кода в 1816 файлах.

Однако прежде чем открывать шампанское: это preview версия, по умолчанию отключённая, и, как быстро остудил общий пыл Брайан Гетц, «лишь первая часть Valhalla». Гетц добавил отличное наблюдение: лагерь тех, кто говорил «они никогда это не выпустят», теперь плавно переключится на «но они не выпустили самую важную часть» (а в сообществе уже много лет ходит шутка, что мы скорее сами окажемся в Вальгалле — той самой, скандинавской загробной, — чем проект будет выпущен).

Так что сейчас самое время рассказать всю историю. Эта статья — один большой глубокий разбор, написанный с расчётом на то, что вы раньше никогда не следили за работой над Valhalla: от проблемы 2014 года, через эволюцию идей (изрядная часть которых в итоге отправилась в корзину), и до того, что именно мы получим в JDK 28.

1. Введение — о чём вообще речь

Девиз, который Valhalla несёт с самого начала: «пишется как класс, работает как int». В одной фразе — вся суть проекта: мы хотим писать нормальные, читаемые классы с методами, валидацией в конструкторе и осмысленными именами полей, но при этом хотим, чтобы JVM обращалась с ними столь же эффективно, как с примитивами.

Чтобы понять, в чём проблема, нужно вернуться к основам Java. В этом языке, если не считать восьми примитивов (int, long, double, boolean и остальных), всё является ссылочным типом. Когда вы пишете Point p = new Point(1, 2), переменная p — это не сам Point. Переменная p — это указатель, номерок из гардероба: где-то в куче лежит объект, а вы держите в руках бумажку с его адресом. Каждый раз, когда нужно прочитать поле, JVM должна «сходить в гардероб», совершив переход по указателю (pointer indirection).

Для одного объекта это ничто. Проблема начинается при масштабировании. У каждого объекта в куче есть собственный заголовок (десяток-другой байт метаданных: в том числе для того, чтобы JVM знала тип объекта и ведётся ли по нему синхронизация). Кстати, именно эту проблему в последнее время решает Project Lilliput, сокращая размер объектных заголовков. Но размер заголовка — не главное. Каждый объект нужно выделить, а потом собрать сборщиком мусора. И поскольку объекты разбросаны по куче, массив из миллиона точек — это на практике миллион бумажек, указывающих на миллион коробок, рассыпанных по всему складу.

Брайан Гётц в своих документах «State of Valhalla» называет такое расположение данных в памяти «рыхлым» (fluffy): раздутым, бесформенным. В идеале хочется плотный layout, при котором данные лежат вплотную друг к другу.

Почему плотность так важна? Потому что железо развивалось быстрее, чем Java. В 1995 году обращение к памяти стоило примерно столько же, сколько операция процессора. Сегодня CPU быстрее оперативной памяти на два порядка, и весь этот разрыв закрывает кэш. Процессор читает память блоками, которые называются кэш-линиями (обычно 64 байта). Если данные лежат плотно и последовательно, один такой блок сразу несёт массу полезных значений. Если же мы скачем по указателям, каждое обращение рискует обернуться промахом кэша (cache miss), а это может быть в сотню раз медленнее, чем попадание. Это и есть локальность обращений (locality of reference) — главная ставка во всей этой игре.

Комментарий от Михаила Поливаха

Кстати, это был один из многих тезисов Кейси Муратори в его нашумевшей работе "Clean Code Horrible Performance" 

Вот текстовая версия, почитайте, кому интересно: 

https://www.computerenhance.com/p/clean-code-horrible-performance

«Но ведь в JVM есть escape analysis», — заметит самый внимательный. Верно: виртуальная машина умеет распознавать объекты, которые никогда не «вырываются» за пределы локального фрагмента кода, и тогда вообще не выделяет их в куче. С точки зрения программиста объект как будто существует, но на самом деле его поля разносятся по обычным переменным или регистрам процессора. В лучшем случае стоимость выделения и последующей сборки мусора падает практически до нуля.

Беда в том, что эта оптимизация непредсказуема и хрупка. Она работает только тогда, когда JIT-компилятор с высокой уверенностью может проследить весь путь объекта. Но достаточно, чтобы объект попал в поле другого класса, был сохранён в массиве, передан в более сложный метод или оказался за границей кода, доступного JIT для анализа, — вот тут-то и ничего не сработает. Исходный код остаётся идентичным, но поведение с точки зрения производительности может измениться кардинально.

Именно поэтому опытные JVM-разработчики воспринимают escape analysis как приятный бонус, но не как фундамент проекта. Если производительность приложения зависит от того, сумеет ли конкретная версия JIT применить эту оптимизацию, очень легко угодить в ловушку труднопредсказуемых регрессий. Небольшой рефакторинг, обновление JDK или изменение структуры кода могут вернуть объекты обратно в кучу — и затраты на выделение памяти и работу сборщика мусора вернутся в полном объёме.

Комментарий от Михаила Поливаха

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

А далее это уже задача рантайма оптимизировать то, что написал разработик, то есть разработчик не должен думать за рантайм. Это часто довольно контрпродуктивно.

Остаётся брать силой: отказаться от объектов и кодировать данные вручную. Вместо класса Color — три байта r, g, b. Это не академический пример. Подобный подход годами применяется в игровых движках, графических библиотеках, системах обработки изображений, базах данных, аналитических движках и HPC-коде, где важен каждый байт памяти и каждое выделение. Только вот скорость достигается ценой безопасности и читаемости. Мы теряем имена, инкапсуляцию состояния, валидацию и методы. JEP 401 приводит простой пример: разработчик, работающий с «сырыми» байтами цвета, может случайно интерпретировать их как BGR вместо RGB, перепутать красный с синим и просто испортить всё изображение. Класс бы этого не допустил. Голый int? Пожалуйста.

И именно эту дихотомию — либо удобные классы, либо быстрые примитивы — Valhalla и пытается стереть.

2. Истоки — 2014 год, «шесть докторских степеней» и пять прототипов

Официально Project Valhalla стартовал в 2014 году. Джеймс Гослинг описал его тогда как «шесть докторских степеней, завязанных в один узел» — и это было не преувеличением. Интересно, что идея старше самого проекта: создатели Java хотели реализовать value types ещё в первой версии языка, но в 1995 году отказались от этой затеи — задача оказалась слишком сложной.

Цель была сформулирована амбициозно: восстановить соответствие между моделью программирования и характеристиками производительности современного железа. Иными словами, дать программистам возможность объявлять собственные типы, которые плоские и плотные в памяти, как примитивы, но выглядят и ведут себя как обычные классы.

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

Ранние прототипы двигались в направлении, которое теперь называют «Q World». Оно исходило из того, что новые value types — принципиально иная сущность по сравнению с объектами: с отдельными дескрипторами типов, отдельными байткодами и отдельными корневыми типами — в точности как у примитивов. Звучит логично: если они должны работать как int, пусть и представляются как int. Проблема в том, что такое разделение усложняло всю систему типов JVM дополнительной сложностью: всё приходилось делать в двух вариантах.

Прорыв произошёл с появлением прототипа под названием «L World» (примерно в 2019 году). Название происходит от того, что value types начали разделять тот же «L-носитель» (дескриптор L — тот самый, который JVM использует для обычных ссылок) с объектными ссылками. Команда ожидала, что такое объединение окажется слишком сложным, однако, к собственному удивлению, оно сработало без существенных компромиссов и заодно решило целый ворох проблем из предыдущих итераций.

L World принёс ещё одно фундаментальное озарение, определившее всё последующее: языковая модель и модель JVM не обязаны совпадать на сто процентов. L World — правильная модель для виртуальной машины, но её можно рассматривать как целевое представление трансляции и предложить программисту нечто более удобное на уровне языка. Это разделение слоёв оказалось ключом ко всему остальному проекту.

Тогда же окончательно оформился план разбить работу на два этапа : сначала value classes (тогда называвшиеся иначе — об этом чуть ниже), и лишь затем — специализированные generics. К generics мы вернёмся в разделе 6: это отдельная, более длинная история.

3. Эволюция идей — американские горки с названиями

Если вы когда-нибудь пытались читать о Valhalla и натыкались на стену противоречивых терминов — это не ваша вина. Названия менялись несколько раз, и не косметически: за каждым переименованием стояло изменение модели. Давайте проследим этот путь, потому что он лучше всего показывает, как проектировалась эта возможность.

Этап 1: value types: Самый ранний термин. Расплывчатый, потому что тогда ещё не было понимания, чем именно должны быть эти сущности.

Этап 2: inline classes: Примерно в 2019–2020 годах устоялось разграничение, которое в своей сути сохранилось до сегодняшнего дня: классы разделились на identity classes (с идентичностью — то есть всё, что мы знали до сих пор) и новые inline classes (без идентичности). Тогда же родился слоган «codes like a class, works like an int», и были установлены базовые ограничения: inline classes по умолчанию final, их поля final, на них нельзя синхронизироваться.

Этап 3: «primitive classes» и модель двух проекций. Вот здесь становится интересно, потому что именно эта идея была существенно урезана. В документах «State of Valhalla» за 2021 год Valhalla обещала три вещи: value objects, primitive classes и specialized generics. Идея «primitive class» состояла в том, что один тип имеет две проекции: value-вариант (плоский, никогда не null, ведёт себя как примитив) и reference-вариант (обёртка, допускающая null). В разных итерациях это записывалось как Point.val/Point.ref, затем экспериментировали с синтаксисом Point! и Point?.

Модель была мощной, но и концептуально тяжёлой. Программисту пришлось бы ежедневно жонглировать двумя формами одного типа и понимать, когда между ними происходит преобразование. Команда, верная принципу «упрощай модель для пользователя, даже ценой потолка производительности», в итоге отказалась от этого дуализма.

Этап 4 (сегодня): «value classes» и «value objects». Действующий JEP 401, автор — Dan Smith (рецензент: Brian Goetz), формулирует всё просто. Есть одна новая сущность: value class, объявляемый с модификатором value. Его экземпляры — value objects: объекты без идентичности. И (это ключевое) value class по-прежнему является ссылочным типом. Вся непростая тема non-nullability была вынесена в отдельный, самостоятельный JEP (Null-Restricted Value Class Types), к которому мы ещё вернёмся. Таким образом, вместо одной сложной концепции появились две простые, ортогональные: «есть ли у него идентичность?» и, отдельно, на потом: «допускает ли он null?»

Это стоит запомнить: если вы встретите старую статью (или Baeldung , описывающий «primitive classes» как отдельный механизм), то знайте, что вы читаете про устаревшую модель. В каноне OpenJDK «primitive classes» в том смысле больше не существуют.

По дороге отпало и многое другое. Исходный драфт «Value Objects» JEP был отозван и заменён JEP 401. Исходный драфт «Universal Generics» также ушёл на доработку. JEP 401 сопровождается JEP 402: Enhanced Primitive Boxing (тоже в статусе preview), а также целой серией early-access сборок (LW1, LW2, LW3…) и докладами с JVM Language Summit, в том числе Frédéric Parainо heap flattening и Daniel Smithо новой модели инициализации объектов.

Мораль этого раздела такова: двенадцать лет — это не двенадцать лет написания кода. Это двенадцать лет отказа от идей, пока не осталась та, которую действительно можно поддерживать.

4. Как Valhalla работает сегодня — модель value class в JDK 28

Перейдём к конкретике. Вот что на самом деле мы получаем.

Объявление. Value class создаётся добавлением модификатора value:

value class USDCurrency implements Comparable<USDCurrency> {
    private int cents; // implicitly final
    public USDCurrency(int dollars, int cents) {
        this.cents = dollars * 100 + cents;
    }

    public USDCurrency plus(USDCurrency that) {
        return new USDCurrency(0, this.cents + that.cents);
    }

    // dollars(), cents(), compareTo(), toString()...
}

Это может быть и value record. Правила: все поля экземпляра неявно final, методы не могут быть synchronized, класс по умолчанию final (либо может образовывать иерархию из value classes и абстрактных value classes), он не может наследоваться от класса с идентичностью, но спокойно реализует интерфейсы. За вычетом этих ограничений — обычный класс.

Определяющее свойство: отсутствие идентичности. Вот в чём суть. Обычный объект обладает идентичностью: два независимо созданных new Point(1,2) — это два разных объекта, даже если их содержимое одинаково. Value object идентичности не имеет — точно так же, как не существует двух «разных» четвёрок типа int. Из этого вытекают все следствия:

  • == меняет смысл. До сих пор == сравнивал идентичность (один ли это адрес). Для value objects == проверяет взаимозаменяемость: одного ли класса оба значения и совпадают ли их поля — рекурсивно (примитивные поля побитово, поля-объекты снова через ==). Поэтому new USDCurrency(3,95) == new USDCurrency(3,95) возвращает true. Это хорошая новость: она устраняет давнюю путаницу с == на Integer.

    Комментарий от Михаила Поливаха: Это как раз речь про внутренний кеш Integer. Тут можно почитать подробнее: https://dev.to/dev_tips/when-numbers-lie-the-java-equality-bug-every-dev-hits-at-least-once-3nja

    Но осторожно: == смотрит на внутреннее состояние, а это не всегда то, что объект представляет, поэтому для сравнения того, что это те же данные, продолжайте использовать equals.

  • synchronized бросает исключение. Синхронизироваться не на чем. Попытка завершается IdentityException. Когда нужно явно потребовать идентичность, есть новые вспомогательные методы Objects.requireIdentity и Objects.hasIdentity.

И теперь самая важная концептуальная ловушка: value objects по-прежнему МОГУТ быть null. Это удивляет всех, кто думает: «value = как примитив = никогда не null». В JDK 28 value class является ссылочным типом, поэтому USDCurrency d = null; — совершенно допустимая запись. Non-nullable типы (с ограничением null) — это отдельный, будущий JEP. В JDK 28 их нет. Мы вернёмся к этому, потому что это не деталь: это ключ к полной производительности.

Как это устроено в памяти

JEP 401 предоставляет JVM свободу, благодаря которой value objects могут оптимизироваться двумя основными способами.

Скаляризация — техника JIT-компилятора. Ссылка на объект-значение «раскладывается на простейшие множители»: сводится к своей сути — набору полей — без какой-либо обёртки. Вместо того чтобы передавать указатель на Color, JIT просто передаёт три байта r, g, b (плюс один флаговый бит, указывающий, не является ли ссылка null). Такой объект на практике обходится бесплатно: никаких аллокаций, никакой нагрузки на GC. Это немного похоже на escape analysis, но значительно предсказуемее и шире по охвату: механизм работает даже через границы вызовов методов, которые JIT не встроил инлайном. Ограничение: скаляризация, как правило, не работает если переменная имеет тип, являющийся супертипом value-класса (например, Object или, что особенно важно, затёртый параметр обобщённого типа). В таком случае объект приходится материализовывать на куче.

Уплощение на куче (heap flattening) — второй механизм. Суть объекта кодируется в виде компактного битового вектора и записывается напрямую в поле или ячейку массива — без указателя куда-то ещё в памяти. Именно здесь рождаются плотность и локальность.

Здесь, однако, есть важная тонкость: уплощённые данные должны читаться и записываться атомарно (иначе при конкурентном доступе возникает риск «разрыва»). На типичных платформах «достаточно маленький» сегодня означает не более 64 бит, включая флаг null.

Комментарий от Михаила Поливаха

Т.е. размер машинного поинтера-а

Именно поэтому многие небольшие value-классы прекрасно уплощаются, а класс с, допустим, двумя полями int или одним double может не вписаться в атомарную запись и в итоге всё равно окажется обычным объектом на куче. В будущем появятся 128-битные кодировки, а упомянутый JEP о типах без null позволит уплощать более крупные классы в обмен на отказ от гарантии атомарности. Именно здесь ненулевость перестаёт быть косметикой и превращается в инструмент оптимизации производительности.

Боксинг и анбоксинг в новом свете

Помните извечную цену боксинга — оборачивания int в Integer? В новой модели сами классы-обёртки становятся value-классами (при включённом preview Integer, Long, Double и им подобные лишаются идентичности). Поскольку у бокса больше нет идентичности, JVM может его скаляризовать и уплощать. Эффект: Integer[] начинает приближаться по эффективности к int[], а накладные расходы на боксинг, цитируя JEP 401, резко сокращаются. Сопутствующий JEP 402 (Enhanced Primitive Boxing) идёт ещё дальше и сглаживает преобразования между примитивами и их обёртками, открывая путь к таким конструкциям, как List<int>. Но это отдельная, ещё не устоявшаяся часть, так что не стоит рассчитывать, что она появится одновременно с 401.

Массивы

Здесь эффект проявляется наиболее наглядно. Вместо того чтобы хранить миллион указателей на миллион разрозненных объектов, массив Color[] может хранить непосредственно уплощённые 32-битные представления последовательных цветов (опять же: плюс флаг null). С точки зрения памяти такой массив начинает вести себя как обычный int[]: непрерывный блок данных, который процессор обходит последовательно, строка кэша за строкой кэша.

Чтобы всё это заработало, пришлось сдвинуть действительно глубокие основания: новый модификатор value; строгие правила конструирования (все поля должны быть установлены прежде, чем кто-либо увидит новый объект, — на практике до вызова super(), чтобы мутация final-полей никогда не могла быть наблюдаема); переопределение == как проверки взаимозаменяемости; добавление проверки value-объекта в байткод сравнения ссылок (acmp); механизмы скаляризации и уплощения; IdentityException; и миграция существующих основанных на значениях классов. Короче говоря, это не синтаксический сахар. Это перестройка допущения, которое было верным в Java с 1995 года: что каждый объект имеет идентичность.

5. Практический пример — до и после, шаг за шагом

Возьмём максимально простой случай и разберём его так, чтобы всё было понятно даже без знания внутренностей JVM.

До Valhalla:

final class Point {       // an ordinary class with identity
    final int x;
    final int y;
    Point(int x, int y) { this.x = x; this.y = y; }
}

Point[] points = new Point[1_000_000];

Что здесь происходит в памяти? Массив points — это миллион указателей. Каждый указатель ведёт к отдельному объекту Point, лежащему где-то на куче. И каждый такой объект — это не просто два его int (8 байт), но ещё и заголовок (ещё десяток с лишним байт метаданных). Объекты разбросаны: аллокатор создавал их в разные моменты времени в разных местах. При итерации по массиву и суммировании координат для каждой точки процессору приходится: прочитать указатель из массива, перейти по указанному адресу (риск промаха кэша), прочитать поля. Миллион раз. Это именно то рыхлое расположение из раздела 1.

После Valhalla:

value class Point {       // a value class without identity
    final int x;
    final int y;
    Point(int x, int y) { this.x = x; this.y = y; }
}

Point[] points = new Point[1_000_000];

Разница в коде — ровно одно слово: value. Но разница в памяти — принципиальная. JVM теперь может хранить сами значения в массиве, плотно выложенными одно за другим: 8 байт на точку (плюс возможный флаг null), в непрерывном блоке. Никаких заголовков на элемент. Никаких указателей. Никаких прыжков по куче.

При итерации по массиву процессор теперь читает данные последовательно. Каждая 64-байтная строка кэша сразу захватывает несколько целых точек. Суммирование миллиона координат идёт на скорости пропускной способности памяти, а не задыхается от промахов кэша. На данных с высокой интенсивностью обращений это может давать разницу в разы, а не в проценты.

И, что самое важно для сопровождаемости кода, вы не заплатили за это абстракцией. Point по-прежнему класс: у него есть имя, конструктор, в нём может быть валидация (if (x < 0) throw ...), могут быть методы. Не нужно, как раньше, разбивать точки на два сырых массива int[] xs и int[] ys и надеяться, что индексы никогда не перепутаются. Вы получили плотность примитива и читаемость класса. Весь Project Valhalla — в одном примере.

6. Специализированные обобщённые типы — почему стирание типов причиняет боль (и что с этим делают)

Это вторая половина Valhalla, и, честно говоря, более сложная. Начнём с источника проблемы.

Java реализует обобщённые типы через стирание типов. На практике: List<String> и List<Integer> во время выполнения — один и тот же обычный List, а параметр типа T стирается до Object. Это часто высмеивают, но стоит знать, что это было осознанным и обоснованным решением — не проявлением лени. Стирание дало Java совместимость при постепенной миграции: можно было взять существующий класс без обобщений и сделать его обобщённым не сломав ни одного существующего исходного файла или скомпилированного класса — а клиенты могли мигрировать немедленно, позже или не мигрировать вовсе. В 2004 году, когда у Java уже была огромная кодовая база, альтернатива («вот обобщённые типы, но выбросьте все свои библиотеки») обошлась бы слишком дорого. Сегодня было бы ещё хуже.

Проблема в том, что стирание типов вступает в прямой конфликт с Valhalla именно там, где производительность важна больше всего. Поскольку T стирается до Object, объект-значение, помещённый в List<Point>, должен быть материализован как обычный объект на куче. Иными словами: ваш красивый, пригодный для уплощения Point в обобщённой коллекции теряет своё уплощение: контейнер хранит ссылки, а не плоские значения. Вся плотность, достигнутая в Point[], улетучивается в ArrayList<Point>.

План исправления, как и весь Valhalla, двухфазный:

Фаза 1: Universal Generics. Это изменение на уровне языка : оно позволяет переменным типа охватывать в том числе value-типы, то есть выражать нечто вроде ArrayList<Point> или List<int>. Пока — всё ещё через стирание. Программист ощутит это главным образом в виде новых предупреждений компилятора о загрязнении null, поскольку поле типа T по умолчанию инициализируется null, даже если T — value-тип. Устранение этих предупреждений делает API готовым к специализации.

Фаза 2: Specialized Generics. Это будущие расширения JVM , которые будут генерировать гетерогенные, специализированные схемы классов для конкретных аргументов типа (в терминологии проекта: species и type restrictions). Только тогда ArrayList<Point> действительно будет опираться на плоскую память. Эта часть пока в значительной мере остаётся исследовательской работой.

Последствия для библиотек и фреймворков огромны, и именно поэтому всё происходит постепенно. В конечном счёте коллекции, стримы и целые API смогут стать плоскими и не требующими аллокаций при работе с value-типами. Но авторам библиотек придётся устранять новые предупреждения и проектировать API с прицелом на специализацию. Будем честны: исходный черновик Universal Generics прошёл через переработку, а полная отдача от специализации — дело будущих релизов. JDK 28 этого не привносит.

7. Что конкретно означает вхождение в JDK 28

Соберём всё в одном месте, потому что легко запутаться между тем, что уже есть и тем, что ещё не готово.

Что принято: JEP 401 (Value Classes and Objects) в статусе preview фича, нацеленная на JDK 28 (релиз в марте 2027 года); интеграция в основную ветку запланирована примерно на июль 2026-го. 197 тысяч строк, 1816 файлов, координация на стороне Lois Foltan, просьба к остальным коммиттерам воздержаться от крупных изменений. По умолчанию отключено: чтобы поиграть с синтаксисом, нужно явно передать --enable-preview.

Что реально доходит до пользователей: возможность объявлять value class и value record; перевод существующих «value-based»-классов в JDK (в том числе примитивных обёрток вроде Integer) в value-классы под preview; скаляризация и выравнивание (flattening) для подходящих классов; более дешёвый boxing.

Что ещё может измениться и чего НЕТ в 28: null-restricted типы (ненулевые); полноценные специализированные дженерики; 128-битные кодировки; окончательно зрелый JEP 402. И сам синтаксис — потому что это preview , и именно это от него и ожидается: он может меняться от релиза к релизу в ответ на обратную связь. Отсюда и цитата Goetz про «только первую часть».

Как это может повлиять на экосистему: для высокопроизводительной Java (данные, векторные вычисления, ML, геймдев, финансы, кодеки) это путь к плотным данным без отказа от абстракций — именно то, чего часть этих областей ждала годами. Фреймворки и библиотеки начнут переводить свои value-based классы. Придется также учитывать возможные изменения в поведении == и synchronized в коде, который осознанно или нет опирался на идентичность объектов. И ещё один момент, который стоит держать в голове при планировании: JDK 28 — не LTS-релиз: следующим LTS, скорее всего, станет JDK 29 в сентябре 2027 года. Так что большинство компаний, скорее всего, познакомятся со стабилизированной Valhalla только на LTS-релизе. Но именно preview в JDK 28 запускает реальный цикл обратной связи на живом коде. Если вы работаете над проектом, который может выиграть от этой фичи, сейчас самое время экспериментировать и отправлять фидбэк.

8. Итоги

Почему я называю это одним из крупнейших изменений в истории платформы? Потому что Valhalla не добавляет еще одну фичу поверх Java — она меняет допущение, на котором язык стоял с 1995 года: у каждого объекта есть идентичность. Теперь программист сможет отказаться от этого свойства там, где оно не нужно, и сам решить, какие объекты должны иметь идентичность, а какие могут быть просто значениями. Это не косметическое улучшение и не рефакторинг, а смена фундамента. Именно поэтому за Valhalla тянется целое десятилетие будущей работы: объединение примитивов и объектов, специализация дженериков, более плотные коллекции и более быстрая арифметика.

В то же время честная версия заголовка звучит так: «Valhalla приходит в JDK 28» — это только часть правды. JDK 28 приносит первый preview-шаг большого многоэтапного релиза. Но именно дисциплина команды — упростить модель для разработчика и оставить сложные оптимизации на стороне JVM — объясняет, почему работа заняла двенадцать лет и почему ее вообще можно выпустить сейчас.

Для нас, программистов, важнее всего не синтаксис, а само различие между идентичностью и значением. Все остальное — ==, flattening, дженерики — лишь следствия этой идеи. А сборки раннего доступа уже доступны: можно попробовать Valhalla на своем коде раньше, чем это сделают ваши конкуренты.

И напоследок — вопросы, с которыми я постоянно сталкиваюсь 😊

  1. value class — это просто record?
    Нет, это два независимых решения. record означает: «я отказываюсь от отдельного внутреннего состояния, содержимое объекта определяется его компонентами». value означает: «я отказываюсь от идентичности». Возможны любые комбинации: обычный класс, record, value class и value record.

  2. Можно ли сравнивать value-объекты через ==?
    Да, но теперь == означает нечто другое: взаимозаменяемость, то есть рекурсивное сравнение всех полей, а не адресов в памяти. Если нужно понять, представляют ли объекты одни и те же данные, по-прежнему лучше использовать equals, потому что == смотрит на внутреннее состояние, а оно не всегда совпадает с представляемым значением.

  3. Может ли value class быть null?
    В модели JDK 28 — да. value class по-прежнему остается ссылочным типом. Non-nullable типы, то есть типы с ограничением на null, — это отдельный будущий JEP. Именно они разблокируют flattening для более крупных value-классов. В JDK 28 их нет.

  4. Integer становится value class — это сломает мой код?
    В большинстве случаев нет. Бинарная совместимость сохраняется, а новые ошибки компиляции появятся только при попытке синхронизироваться на таком типе. Изменения, которые вы можете заметить, касаются кода, зависящего от идентичности: == на Integer начнет сравнивать по значению, а synchronized (someInteger) перестанет работать. Если код опирался на любое из этих поведений, он и так был хрупким.

  5. Получу ли я быстрый, плоский ArrayList<Point>?
    Пока нет. Из-за стирания типов объекты в обобщенной коллекции материализуются в куче. Плоские обобщенные коллекции требуют универсальных и специализированных дженериков, а это дело будущего. В JDK 28 flattening работает напрямую для полей и массивов value-типа, например для Point[].

  6. Чем это отличается от struct в C#?
    struct в C# — тоже value type, но модель другая: у него есть четко заданная семантика копирования, он может быть изменяемым, а значит программисту приходится внимательнее думать о присваивании, передаче и мутации. Value-объекты в Valhalla не имеют идентичности, а способ их размещения в памяти остается на усмотрение JVM. Проще для человека — больше свободы для машины.

  7. Разве escape analysis уже не делал все это?
    Частично. Escape analysis может избежать аллокации объекта, если докажет, что тот не зависит от идентичности. Но эта оптимизация непредсказуема и не помогает, когда объект попадает в поле, массив или «ускользает» за пределы анализа. Скаляризация value-объектов работает предсказуемее и охватывает больше случаев, в том числе границы вызовов методов.

  8. Нужно ли переписывать код, чтобы получить выгоду?
    Для собственных классов обычно достаточно добавить модификатор value к тем, что представляют простые доменные значения и не опираются на идентичность. В целом такая миграция совместима. Часть выигрыша вы получите бесплатно — за счет того, что JDK сам переводит свои классы, например примитивные обертки.

  9. Когда я увижу полную Valhalla — с дженериками, non-null-типами и всем остальным?
    В будущих релизах. Команда выпускает Valhalla инкрементально: JDK 28 — это первый preview value-классов. Полная картина — специализированные дженерики, null-restricted типы и 128-битные кодировки — растянется на несколько релизов и, скорее всего, стабилизируется только ближе к следующему LTS.

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