Java не стоит на месте и продолжает активно развиваться. Скоро выйдет уже 25-я версия языка. В этом релизе изменили работу с boilerplate-кодом и конструкторами, а также отказались от поддержки устаревших систем. Обо всех этих и других нововведениях расскажем в статье.

Новые API

JEP 506: Scoped Values

Ссылка на JEP

Как часто вы использовали ThreadLocal? Даже не так: слышали ли вы о нём? Если коротко, ThreadLocal — это контейнер, хранящий уникальное значение переменной для каждого потока.

С ходу тут и не скажешь, какие проблемы могут возникнуть. Но они есть и довольно серьёзные:

  • неограниченное время жизни;

  • неконтролируемая изменяемость;

  • затратное (по производительности) наследование значения дочерними потоками.

В этом релизе появился новый механизм ScopedValue, решающий перечисленные проблемы.

Пример:

ScopedValue<FrameworkContext> CONTEXT = ScopedValue.newInstance();  

void serve(Request request, Response response) {  
    var context = createContext(request);  
    ScopedValue.where(CONTEXT, context)  
            .run(() -> handle(request, response));  
}  

void handle(Request request, Response response) {  
    // ....
    readKey(key);  
    // ....
}  

Key readKey(String key) {  
    var context = CONTEXT.get();  
    var connection = getConnection(context);  
    return connection.readKey(key);  
}

Здесь с помощью ScopedValue#where настраивается окружение, а #run запускает Application#handle с этим контекстом. Сразу после выхода из метода контекст чистится.

JEP 510: Key Derivation Function API

Ссылка на JEP

Теперь в Java есть единый встроенный способ для работы с KDF-алгоритмами.

KDF-алгоритмы — алгоритмы, безопасно генерирующие криптографические ключи из исходных секретов (паролей/ключей). Более подробно тут.

Раньше для разных алгоритмов требовались разные подходы, теперь всё просто:

KDF hkdf = KDF.getInstance("HKDF-SHA256");  

AlgorithmParameterSpec params =  
        HKDFParameterSpec.ofExtract()  
                .addIKM(initialKeyMaterial)  
                .addSalt(salt)
                .thenExpand(info, 32);  
SecretKey key = hkdf.deriveKey("AES", params);

Все танцы с бубном с SecretKeyFactory и PBEKeySpec для PBKDF2 или возня с HKDFParameterSpec теперь в прошлом. Один API, чтобы управлять всеми.

Нововведения в синтаксисе

JEP 511: Module Import Declarations

Ссылка на JEP

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

Вот как это выглядит в комбинации с компактными main-методами:

import module org.slf4j;

Logger log = LoggerFactory.getLogger("MyAppLogger");

void main() {
    Stream<String> stringStream = ThreadLocalRandom.current().longs(10)
            .mapToObj(String::valueOf);

    for (var string : (Iterable<String>) (stringStream::iterator)) {
        log.info(string);
    }
}

JEP 512: Compact Source Files and Instance Main Methods

Ссылка на JEP

Пожалуй, одно из самых заметных изменений для каждого разработчика. Борьба с boilerplate вышла на новый уровень.

Наконец-то можно забыть про обязательные public static void main(String[] args) для простых скриптов и утилит. Язык становится более доступным для новичков и приятным для опытных разработчиков, которые ценят лаконичность. Под капотом это работает за счёт неявного объявления класса и автоматического импорта модуля java.base. А для ещё большего удобства в java.lang появился новый класс IO с базовыми методами ввода-вывода.

Boilerplate-код — это повторяющийся шаблонный код, который приходится писать вручную для решения стандартных задач, но при этом он несёт мало уникальной логики.

Достаточно лишь два раза в день...

void main() {
  IO.println("I'm still java. For now");
}

... и голова болеть не будет.

JEP 513: Flexible Constructor Bodies

Ссылка на JEP

Это то, о чём многие мечтали годами. Возможность писать код до вызова super() или this() — это не просто синтаксический сахар, а фундаментальное упрощение логики инициализации объектов. После preview в Java 22 фича доработана и позволяет не только валидировать аргументы, но и безопасно инициализировать поля, решая классические проблемы с порядком выполнения кода, которые находила одна из диагностик PVS-Studio :)

Не будем указывать пальцем, но это всегда было ограничение языка, а не байт-кода, JVM или чего-то ещё.

Sandbox(String key) {
    if (!key.startsWith("sandbox:"))
        throw new IllegalArgumentException(key);
    super(key);
}

Изменения в платформе Java

JEP 503: Remove the 32-bit x86 Port

Ссылка на JEP

Официально прощаемся с 32-битными x86-системами.

Напомним: в прошлом релизе отправили в утиль поддержку 32-битной Windows, а теперь добили и остальные платформы (Linux x86, macOS). Причина проста: поддержка древнего железа требует немалых усилий, а пользователей таких систем совсем-совсем мало.

Если вы ещё используете 32-битную JVM (вау!), пора задуматься об обновлении.

JEP 519: Compact Object Headers

Ссылка на JEP

Сжать нельзя выделить.

Каждый объект в Java-куче имеет скрытый заголовок — как паспорт с метаданными. До Java 25 в 64-битных JVM он весил 12 байт. Теперь его можно ужать до 8 байт флагом -XX:+UseCompactObjectHeaders.

JEP 521: Generational Shenandoah

Ссылка на JEP

Shenandoah — низкопаузный параллельный сборщик мусора, работающий конкурентно с приложением. Основная задача: минимизировать stop-the-world паузы (<10 мс) даже на терабайтных хипах. Вместо привычных поколений использует регионы фиксированного размера.

Теперь Shenandoah официально поддерживает поколения (ранее это было экспериментальной фичей): регионы могут быть молодыми или старыми. Зачем?

  • снижение потребление памяти без увеличения пауз GC;

  • уменьшение энергопотребления и нагрузки на CPU.

Ahead-of-Time оптимизации

JEP 514: Ahead-of-Time Command-Line Ergonomics

Ссылка на JEP

Java понемногу старается упростить себя. В том числе и CLI. Теперь для создания новомодного AOT-кэша нужен только один флаг: -XX:AOTCacheOutput=<file>.

Подробнее о Ahead-of-Time кэше можете прочитать в нашей предыдущей статье о нововведениях в Java.

JEP 515: Ahead-of-Time Method Profiling

Ссылка на JEP

AOT-кэш развивается. В этом релизе в AOT добавили возможность сохранять профили выполнения методов. Теперь JVM может применить оптимизации JIT сразу, используя кэш, не дожидаясь сбора статистики выполнения. Соответственно, приложения в продакшене быстрее запускаются и достигают пиковой производительности.

Улучшения JFR

JEP 518: JFR Cooperative Sampling

Ссылка на JEP

Профилирование производительности становится точнее! Раньше JFR использовал прерывания потоков для сбора данных, что могло искажать реальную картину при высокой нагрузке.

Теперь же реализована кооперативная выборка: потоки приостанавливаются в безопасных точках (safepoints), чтобы отдать статистику. Данные остаются консистентными, а влияние на производительность минимально.

JEP 520: JFR Method Timing & Tracing

Ссылка на JEP

Хотите не только знать, где тормозит код, но и почему? Новые события в JFR дают ответ:

  • MethodTiming фиксирует время выполнения методов с наносекундной точностью;

  • MethodTrace строит цепочки вызовов для критичных участков.

In Preview

В качестве предварительного просмотра в релиз включили следующие JEP'ы:

Заключение

Полный список JEP можно найти на сайте OpenJDK.

Эволюция Java продолжается, и 25-й релиз отличное тому доказательство. Язык целенаправленно движется в сторону увеличения производительности (AOT, Shenandoah), уменьшения boilerplate (компактный синтаксис) и усиления безопасности (KDF). При этом команда разработки демонстрирует взвешенный подход, не ломая обратную совместимость. Всё как всегда: шаг за шагом, без революций, но с постоянным улучшением уже знакомого и любимого инструмента.

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


  1. mrprogre
    10.09.2025 10:43

    Шикарная статья! Максимально понятно написано в отличие от других подобных.
    Просто мысль: много народу на восьмой джаве пишет до сих пор и чаще закрывает подобные статьи, типа на потом. Было бы интересно почитать статьи именно от Вас со всеми важными различиями между восьмой и текущей. Это явно статей на 10 :)


  1. Lewigh
    10.09.2025 10:43

    Пожалуй, одно из самых заметных изменений для каждого разработчика. Борьба с boilerplate вышла на новый уровень.

    Наконец-то можно забыть про обязательные public static void main(String[] args) для простых скриптов и утилит. Язык становится более доступным для новичков и приятным для опытных разработчиков, которые ценят лаконичность.

    void main() {  IO.println("I'm still java. For now");}

    Просто революция не меньше. Теперь не нужно писать public static и String ...args ровно в одном месте программы. К черту неконсистентность языка ровно для одного места. К черту то, что Java настолько boilerplate что без сторонней утилиты (lombok) невозможно писать нормально. Главное что в методе main теперь на пару слов меньше писать. Борьба с boilerplate вышла на новый уровень однозначно.

    Официально прощаемся с 32-битными x86-системами.

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

    В одной и той же статье.


    1. urvanov
      10.09.2025 10:43

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


      1. Lewigh
        10.09.2025 10:43

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

        Придерживаюсь аналогичного мнения.


    1. grisha9
      10.09.2025 10:43

      del


    1. DenSigma
      10.09.2025 10:43

      Больше всего удар по бойлерплейт нанес record. С ним почти все фичи ломбока можно запретить.


  1. urvanov
    10.09.2025 10:43

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

    В статье, кстати, не указано, но 25-ая версия должна быть с длительным сроком поддержки (LTS), судя по этой статье. Мне кажется, стоит указать.


  1. AstarothAst
    10.09.2025 10:43

    Пожалуй, одно из самых заметных изменений для каждого разработчика

    Вы издеваетесь? Каждому разработчику давно плевать где там в коде public static void main(String[] args) - это одна единственная строка, которая генерируется за разработчика, и видит он ее примерно никогда. Изменение, ё-мое...


    1. ValeryIvanov
      10.09.2025 10:43

      Думаю это хороший подарок для тех кто обучают языку Java других людей. Объяснять новичку что это за class Main public static void main немного затруднительно, ведь в процессе обучения до классов и методов доходят далеко не сразу.


      1. polotenze
        10.09.2025 10:43

        А мне кажется, наоборот. main это точка входа в программу, и надо понимать, откуда начинается выполнение, и как вообще работает java. А его отсуствие это просто некоторое удобство для написания скриптов


        1. AstarothAst
          10.09.2025 10:43

          Вот нет других проблем при изучении языка - сигнатура метода main все портит...


      1. Lewigh
        10.09.2025 10:43

        Думаю это хороший подарок для тех кто обучают языку Java других людей. Объяснять новичку что это за class Main public static void main немного затруднительно, ведь в процессе обучения до классов и методов доходят далеко не сразу.

        Чуть позже очень удобно будет объяснять новичку:

        // публичный статический метод
        void main() {  
            System.out.println("Hello world");
        }
        // приватный пакетный нестатический метод
        void hello() {  
            System.out.println("Hello world");
        }
        // снова публичный статический метод
        public static void hello() {  
            System.out.println("Hello world");
        }

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


        1. supcheg Автор
          10.09.2025 10:43

          В первом случае #main не является ни публичным, ни статическим.
          Исходники:

          void main() { }
          void hello1() { }
          public static void hello2() { }
          

          Байт-код:

          $ javap Sandbox
          Compiled from "Sandbox.java"
          final class Sandbox {
            Sandbox();
            void main();
            void hello1();
            public static void hello2();
          }
          


  1. svn74
    10.09.2025 10:43

    Неужели поддержка 32 битных jvm так уж накладно?... :( Тут же ж вся прелесть, - возможность писать под древние ПК...


    1. x86chk
      10.09.2025 10:43

      С учётом доминирования 64-битных платформ, что в мобильных устройствах, что на десктопе, это больше похоже на естественный процесс. Если брать целевые операционные системы, то Windows 10 — последняя ОС, существовавшая в 32-битной редакции, и её поддержка без учёта ESU и корпоративных изданий заканчивается уже через месяц. Из macOS 32-битную подсистему удалили несколько лет назад.

      Не исключаю, что кто-то сделает новый JDK, где продолжат поддержку устаревшей архитектуры, всё зависит от реального спроса :)


  1. pkokoshnikov
    10.09.2025 10:43

    На самом деле в этом lts релизе много изменений по сравнению с предыдущим lts. Как будто не все понимают что этот релиз содержит фичи из 22, 23, 24 версии и теперь это lts.