Всего в релиз вошло 10 JEP-ов. Несколько я объединил в один блок, одним намеренно приберёг, чтобы рассказать о нём чуть позже. Будет немного практики — прямо в статье посмотрим, как перевести существующий проект на Java 26 и с чем придётся столкнуться. Статья также доступна в формате видео.
Как устроен путь фичи в JDK
Прежде чем начать, небольшой экскурс для тех, кто не следит за структурой релизов внимательно: сначала фича появляется в инкубаторе, потом переходит в превью, потом может войти в финальный состав JDK. Инкубатор и превью включаются отдельными флагами — это сделано специально, чтобы собирать фидбэк, не давая гарантий стабильности API. В продакшн фичи из инкубатора и превью тащить не стоит: API может поменяться, и весь написанный под него код устареет. Думаю, так будет понятнее, что значит «N-е превью» или «N-й инкубатор» с практической точки зрения по ходу чтения статьи.
Два способа переехать на Java 26
Прежде чем начать рассказывать про новые фичи, хочу ответить на самый главный вопрос: а как собственно получить эти фичи в своём проекте? На самом деле, нововведения можно поделить на два типа: те, которые мы используем напрямую (например, изменения в синтаксисе), и те, которые используем косвенно (например, улучшение сборщика мусора). Так вот, чтобы получить вторые — в случае с прекрасной экосистемой Java можно вообще практически ничего не делать.
Вариант 1: просто меняем рантайм
Код собирается под Java 24, запускается на Java 26. Синтаксис и зависимости не трогаем. Такой подход даёт бесплатный прирост от улучшений GC и других рантайм-оптимизаций без единого изменения в коде.

Пересобираем, запускаем, подключаемся к контейнеру, выполняем java -version — видим Java 26. Переезд занял буквально 2 строчки в Dockerfile.
Пытливый читатель может задаться вопросом, а чего это у меня такой навороченный Dockerfile? К сожалению, кратко ответить на этот вопрос я не смогу. Рекомендую прочитать статью от ребят из Spring АйО: «Как должен выглядеть правильный Docker Image для Spring Boot приложения?«.
Вариант 2: полноценный переезд — меняем и рантайм, и language level
Для этого потребуется обновить зависимости и сам Gradle/Maven файл сборки. На практике переезд простенького проекта у меня прошёл практически бесшовно — приложение стартует, в логах видна Java 26, варнингов по Spring или зависимых библиотек нет.

Structured Concurrency — шестое превью
JEP 525. Первый инкубатор появился в Java 19, первое превью — в Java 21. Шесть превью и два инкубатора позади. Прошло практически 4 года с того момента, как мы увидели её в изначальном исполнении.

Суть проблемы: классические инструменты параллелизма —
ExecutorService,Future,CompletableFuture— не знают ничего о связях между задачами. Если ты запустил три подзадачи для обработки одного запроса, они выполняются в разных потоках без общего «родителя» и без удобного способа координации.
Разберём на примере. Нужно параллельно загрузить профиль, настройки и историю пользователя, и нам нужны все три результата — если хоть один упал, остальные нерелевантны.
Ниже пример без использования параллелизма. Попробуем написать его с существующим API языка, различными его вариациями.
-
Вариант с ExecutorService + Future.
.get()вызывается последовательно: сначала ждём профиль, потом настройки, потом историю. Если настройки упали через секунду, а профиль отрабатывает 10 секунд — об ошибке узнаём только через 10 секунд. ПриInterruptedExceptionоставшиеся задачи не отменяются.
-
Вариант с ручной отменой. Можно добавить
cancel()вcatch— тогда при падении одной задачи явно отменяем остальные. Но узнаём об ошибке по-прежнему в самый последний момент, когда.get()доходит до упавшей задачи. Бойлерплейт растёт, проблема с поздней диагностикой провала никуда не делась.
-
Вариант с CompletableFuture.allOf.
cancel(true)отменяетCompletableFuture-обёртку, но не прерывает поток, который выполняет задачу — он продолжит работу до конца. Исключение приходит завёрнутым вCompletionException → ExecutionException. Вручную отменять задачи всё равно нужно.
-
Structured Concurrency. Наконец, вишенка на торте. Прошу любить и жаловать приятный, лаконичный и удобный API для решения нашей задачи:

Если любая задача упала — остальные отменяются автоматически. Поток-владелец всегда переживает все дочерние. Стектрейсы отражают реальную иерархию вызовов. Принцип тот же, что у try-with-resources: время жизни задач привязано к лексическому блоку.
На самом деле, мы уже могли написать нечто похожее и в версии Java 25. А вот что изменилось именно в Java 26:
-
Скоуп теперь создаётся через статический
StructuredTaskScope.open()вместоnew. -
Метод
join()возвращаетListвместоStream— результаты материализованы сразу, не нужно думать о ленивых вычислениях после закрытия скоупа. -
Добавился
joinUntil(deadline)— если задачи не успели к дедлайну, скоуп их отменяет.
Изменений с прошлого превью немного, API явно стабилизируется. Финализация, скорее всего, произойдёт довольно скоро. Но фича по прежнему в превью, поэтому тащить в продакшен я бы вам не советовал.
Final-поля: конец долгой истории
JEP 502. Этот JEP важен для всех, кто пишет на Spring, Hibernate — не потому что мы сами меняем final-поля через рефлексию, а потому что именно так работают большинство популярных фреймворков под капотом.

Как так получилось?
Когда появилась сериализация/десериализация, встала задача: как восстановить объект из байтов, у которого есть final поля? Вместо того чтобы сделать узкий механизм именно для этого случая, в Java 5 расширили общий Field.set() так, чтобы он мог писать в final-поля вообще для всех. Это было проще в реализации, но открыло лазейку всем желающим.
Ей воспользовались все: Jackson, Hibernate, Spring, библиотеки мокирования и тестирования. Технически это работало. Семантически — нарушало контракт final. А у JIT не было и по прежнему нет возможности доверять final-полям и делать на их основе оптимизации, потому что кто угодно теоретически может такое поле изменить через рефлексию.
Двадцать лет спустя это начинают исправлять.
Что изменилось в Java 26?
JVM начинает выдавать предупреждения, когда код меняет final-поле через рефлексию после завершения работы конструктора:
WARNING: Final field com.example.MyClass.value mutated after construction
Пока только предупреждения. Используя специальный флаг эти предупреждения сейчас можно заглушить. Следующие шаги по плану: невозможность заглушить предупреждения, исключения, которые можно заглушить и, наконец, полный запрет подобного изменения значения final-поля.
Правильное решение — использовать sun.reflect.ReflectionFactory.newConstructorForSerialization(). Этот API позволяет создать объект и установить final-поля в момент его инициализации, что JVM считает допустимым. Именно так сейчас работают Jackson 2.x и Hibernate, поэтому на современных проектах варнингов вы уже скорее всего и не встретите.
// Старый способ — то, что будет запрещеноField field = MyClass.class.getDeclaredField("value");field.setAccessible(true);field.set(instance, 42); // WARNING в Java 26, исключение позже// Новый способ через ReflectionFactoryReflectionFactory rf = ReflectionFactory.getReflectionFactory();Constructor<?> objCtor = Object.class.getDeclaredConstructor();Constructor<?> serCtor = rf.newConstructorForSerialization(MyClass.class, objCtor);serCtor.setAccessible(true);MyClass instance = (MyClass) serCtor.newInstance(); // final-поля устанавливаются здесь
Что это означает на практике, для нас, простых работяг?
-
Spring 6.x и Hibernate давно перешли на новый подход и внутренние механизмы JDK. На современных проектах предупреждений не будет.
-
Spring 5.x и старый Jackson — при запуске на Java 26 в логах появятся варнинги. Приложение не упадёт, но это сигнал к обновлению.
Я проверял на своём проекте с телеграм-ботом на Spring Boot 3.5. Никаких ворнингов. А вот Connekt (HTTP-клиент в Amplicode/OpenIDE) пока не обновился, и во время запросов через него в логах видны варнинги — что-то связанное с cookie storage использует старый подход.

G1 и ZGC: бесплатный прирост производительности
-
JEP 522 — улучшение пропускной способности G1. Не про latency, а именно про throughput — количество полезной работы в единицу времени.
-
JEP 519 — ZGC получил поддержку concurrent object pinning. Раньше при обращении к JNI отдельные объекты «пинились» и не двигались во время GC, что вызывало stop-the-world паузы. Теперь это происходит конкурентно. G1 — пока дефолтный сборщик, но всё движется к тому, что ZGC со временем займёт его место.
По результатам прошлогоднего опроса среди Java-разработчиков, лишь единицы занимаются тонкой настройкой GC. Большинству это не нужно — и в этом смысл: просто поменяй рантайм в Dockerfile, как показано выше, и получи несколько процентов прироста без единой строчки кода.
Кстати, чтобы мои заявления про «большинство» были менее голословными, пройди свежий опрос от 2026 года. В нём вопросы не только про Java, но и про AI-агентов, Spring, деплоймент, и так далее. Про всё, с чем Java-разработчикам приходится сталкиваться хотя-бы время от времени.
Рекомендую пройти, чтобы понять насколько текущий стек твоей команды или твой лично вообще релевантен для РФ-рынка. Ну и плюс можно выиграть бесплатные билеты на конференции, тоже приятно 🙂

Примитивы в паттернах
JEP 530. Четвертое превью.

До Java 26 паттерны в switch и instanceof работали только с reference-типами. Integer — ок, int — нет. Приходилось писать воркэраунды с явным приведением типа. Теперь:
switch (value) { case int i when i > 100 -> "big"; case int i when i > 0 -> "positive"; default -> "other";}
И в instanceof:
if (value instanceof int i && i > 0) { // безопасное использование i}
Почему так не сделали с самого начала? Ну, это на самом деле не так уж и тривиально: reference-типы наследуются от Object, у них есть иерархия, по которой компилятор строит паттерны. У примитивов её нет. При этом между примитивами существуют неявные конверсии: int в doubleбезопасно, double в short — нет, теряются данные. JEP вводит понятие safe conversions: компилятор знает, какие переходы разрешены без потери данных, и запрещает небезопасные статически.
Пока превью — в продакшн не тащим.
HTTP/3 в стандартном HttpClient
JEP 517. Финализированная фича!

HTTP/3 работает поверх QUIC (UDP) вместо TCP. Убирает проблему head-of-line blocking, лучше работает на нестабильных соединениях, быстрее устанавливает соединение.
По умолчанию HttpClient всё ещё использует HTTP/2. HTTP/3 нужно включать явно:
HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_3) .build();
Если сервер не поддерживает HTTP/3 — клиент автоматически откатывается до HTTP/2. Поэтому можно указывать HTTP_3 без страха: как только сервер дорастёт до поддержки, оно заработает само. Чтобы видеть в логах, какая версия протокола реально использовалась, нужен VM-флаг -Djdk.internal.httpclient.debug=true.
Как подключить HTTP/3 в Spring
В Spring Boot начиная с версии 6.1 RestClient и WebClient используют стандартный Java HttpClient под капотом. Это значит, можно переопределить factory-бин и получить HTTP/3 во всём приложении без низкоуровневого кода:
@Configurationpublic class Http3Config { @Bean JdkClientHttpRequestFactory jdkClientHttpRequestFactory() { HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_3) .build(); return new JdkClientHttpRequestFactory(client); } @Bean RestClient restClient(JdkClientHttpRequestFactory jdkClientHttpRequestFactory) { return RestClient.builder() .requestFactory(jdkClientHttpRequestFactory) .build(); }}
После этого везде, где заинжектирован RestClient, запросы пойдут по HTTP/3. Я проверял с Connekt: запрос к нашему приложению идёт по HTTP/1.1 (Connekt к нам так подключается), а запрос из приложения по RestClient к внешнему эндпоинту — по HTTP/3. В логах это видно явно при включённом debug-флаге -Djdk.httpclient.HttpClient.log=requests.

Vector API и что ещё не вошло в обзор
-
Vector API (JEP 529) — одиннадцатый инкубатор. Не опечатка. Инкубатор → превью → финал — и вот одиннадцатый инкубатор подряд. Технология интересна для LLM-вычислений, но временной горизонт финализации кажется слишком далеким лично для меня, поэтому не стал в это лезть. Без комментариев.
-
Lazy Constants (JEP 526) — интересная фича, но как то для неё не нашлось места в этой статье, разберу отдельно в телеграм-канале.
-
Удаление апплетов (JEP 523) — ну, что поделать, ушла эпоха.

Уже сейчас OpenIDE позволяет разрабатывать проекты на Java, Spring, Python, Go, JavaScript и TypeScript! А поддержка Docker и 300+ плагинов доступны абсолютно бесплатно в маркетплейсе. Пробуйте российскую IDE в деле и подписывайтесь на нас в Telegram или Max, чтобы не пропустить свежие обновления и полезные материалы.
ссылка на оригинал статьи https://habr.com/ru/articles/1022864/