Вышла Java 26

Вышла общедоступная версия Java 26. В этот релиз попало более 2700 закрытых задач и 10 JEP'ов. Release Notes можно посмотреть здесь. Полный список изменений API – здесь.

Java 26 не является LTS-релизом, и у него будут выходить обновления только полгода (до сентября 2026 года).

Скачать JDK 26 можно по этим ссылкам:

Рассмотрим все JEP'ы, которые попали в Java 26.

Язык

Prepare to Make Final Mean Final (JEP 500)

При использовании глубокой рефлексии для изменения final полей (с помощью методов Field::setAccessible и Field::set) теперь выдаются предупреждения в консоль:

WARNING: Final field f in class p.C has been mutated reflectively by class com.foo.Bar.caller in module N (file:/path/to/foo.jar)
WARNING: Use --enable-final-field-mutation=N to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled

Чтобы подавить такие предупреждения, теперь нужно будет использовать новую опцию командной строки --enable-final-field-mutation. С её помощью перечисляются модули, которым разрешено изменение final полей:

$ java --enable-final-field-mutation=ALL-UNNAMED ...

Одной лишь опции --enable-final-field-mutation может быть недостаточно для подавления предупреждений. Если класс, в котором находится final поле, находится в другом модуле, то также необходима гарантия того, что пакет класса открыт для глубокой рефлексии модулю (--add-opens), который собирается изменить поле.

Также можно контролировать поведение Java при несанкционированном изменении final полей. Для этого появилась ещё одна опция --illegal-final-field-mutation, которая может принимать одно из четырёх значений:

  • --illegal-final-field-mutation=allow разрешает изменений final полей без всяких предупреждений. Это означает откат к старому поведению (как в Java 25 и более старых релизах).
  • --illegal-final-field-mutation=warn разрешает изменений final полей, но выдаётся предупреждение при первом выполнении модуля такой нелегальной операции. Это значение по умолчанию в Java 26.
  • --illegal-final-field-mutation=debug – это то же, что и warn, но только предупреждение выдаётся при каждой нелегальной операции.
  • --illegal-final-field-mutation=deny запрещает изменений final полей. При выполнении нелегальной операции выбрасывается IllegalAccessException.

--illegal-final-field-mutation – это временная опция, которая необходима для более безболезненного перехода к ситуации, когда разрешать изменение final полей придётся только через перечисление модулей с помощью --enable-final-field-mutation (подобно тому, как с Java 9 по 16 существовала опция --illegal-access). В одном из будущих релизов значением по умолчанию станет deny, а allow перестанет поддерживаться. В конечном счёте опция исчезнет полностью.

Возможность изменения final полей долгое время была костью в горле для разработчиков JVM. Если final поле в любой момент может измениться, то значит его значению нельзя доверять, а значит к нему сложно применить оптимизации, например, constant folding. Когда изменение final полей станет невозможным, это развяжет руки разработчикам JVM и они смогут применить к final полям те же оптимизации, что и к полям записей (в которых изначально был запрет на изменение полей).

JEP 500 – это очередной шаг постепенного перехода Java к философии integrity by default (целостность по умолчанию). JVM по умолчанию не должна делать ничего кроме безопасных вещей. Если требуется что-то опасное, то это должно быть возможно только при явном разрешении от пользователя. Изменение final полей – это небезопасная операция, поэтому она должна быть по умолчанию запрещена.

Primitive Types in Patterns, instanceof, and switch (Fourth Preview) (JEP 530)

Примитивные типы в паттернах, instanceof и switch, которые были в режиме preview в Java 23, Java 24 и Java 25, остаются на четвёртое preview.

В Java 26 есть два изменения.

Во-первых, введено понятие безусловно точного приведения, основанного на значениях (value-based unconditionally exact conversion). Оно противопоставляется безусловно точному приведению, основанного на типах (type-based unconditionally exact conversion). Например, приведение int'ового константного значения 42 в short – это безусловно точное приведение, основанное на значениях, потому что 42 приводится к short во время компиляции без потерь.

Во-вторых, в ветках switch усиливаются проверки доминирования, и ранее корректные для компилятора Java 25 ветки switch могут перестать компилироваться в Java 26. Это включает в себя новые проверки, основанные на вышеупомянутой безусловной точности, основанной на значениях:

// --enable-preview --source 26
byte x = ...;
switch (x) {
    case short s -> {}
    case 42      -> {} // error: dominated since 42 can be
                       // converted unconditionally exactly to short
}

В Java 25 этот код компилировался:

// --enable-preview --source 25
byte x = ...;
switch (x) {
    case short s -> {}
    case 42      -> {} // ok, no error
}

Также теперь безусловный паттерн всегда доминирует над любыми другими следующими паттернами:

// --enable-preview --source 26
int x = ...;
switch (x) {
    case int _ -> {} // unconditional pattern
    case float _ -> {} // error: dominated
}

В Java 25 этот код также компилировался:

// --enable-preview --source 25
int x = ...;
switch (x) {
    case int _ -> {} // unconditional pattern
    case float _ -> {} // ok, no error
}

Напомним, что смысл всей фичи – это поддержка примитивных типов в паттернах и операторах instanceof / switch:

// --enable-preview --source 26

Object obj = 42;
if (obj instanceof int i) { // matches
    System.out.println("int: " + i);
}

switch (obj) {
    case int i -> System.out.println("int: " + i); // matches
    case double d -> System.out.println("double: " + d);
    default -> System.out.println("other");
}

Проверять можно также и то, попадают ли значения в диапазон типа:

int i = 42;
if (i instanceof byte b) { // matches
    System.out.println("byte: " + b);
}
double d = 3.0;
switch (d) {
    case int i -> System.out.println("int: " + i); // matches
    case float f -> System.out.println("float: " + f);
    default -> System.out.println("other");
}

В примерах выше 42 попадает в диапазон byte ([-128; 127]), а 3.0 без потери точности приводится к int. Таким образом, это позволит более безопасно приводить одни числовые типы к другим, не прибегая к ручным проверкам диапазонов.

Подобные проверки могут быть полезны и в паттернах записей:

record JsonNumber(double d) {}

var json = new JsonNumber(3.0);
if (json instanceof JsonNumber(int i)) { // matches
    // ...
}

Если до Java 23-26 типы выражений-селекторов в switch могли быть только int, short, byte и char и для них поддерживались только константные ветки (case 3 и т.п.), то сейчас поддерживаются все примитивные типы и ветки могут быть паттернами:

float f = 1.0f;
switch (f) {
    case 0f -> System.out.println("0");
    case float x when x == 1f -> System.out.println("1"); // matches
    case float x -> System.out.println("other");
}

boolean b = "hello".isEmpty();
switch (b) {
    case true -> System.out.println("empty");
    case false -> System.out.println("non-empty"); // matches
}

API

Lazy Constants (Second Preview) (JEP 526)

API для ленивых констант, которые в Java 25 назывались стабильными значениями, переходит во второе preview со следующими изменениями:

Напомним смысл новой фичи. Для начала вспомним, как в Java можно реализовать отложенную инициализацию классическими средствами:

class OrderController {
    private Logger logger = null;

    Logger getLogger() {
        if (logger == null) {
            logger = Logger.create(OrderController.class);
        }
        return logger;
    }

    void submitOrder(User user, List<Product> products) {
        getLogger().info("order started");
        ...
        getLogger().info("order submitted");
    }
}

В примере выше объект logger инициализируется в момент первого обращения. У такого подхода есть несколько проблем:

  • Любой доступ к полю logger должен происходить через метод getLogger(). Это можно забыть сделать.
  • Код не является потокобезопасным: объект logger может инициализироваться несколько раз.
  • Компилятор не может применить оптимизацию constant folding, так как поле logger не является final.

Частично проблем выше можно избежать, прибегнув к другим более сложным идиомам, например, double-checked locking или class holder. Однако с double-checked locking код становится невероятно громоздким и хрупким (например, можно забыть вставить ключевое слово volatile), а так же отсутствует constant folding. С class holder код становится более-менее простым и надёжным (и есть constant folding), но у этой идиомы есть серьёзные ограничения: она применима только к статическим полям и для каждого поля приходится объявлять свой собственный класс. Также можно использовать ConcurrentHashMap, однако и у неё есть недостатки: отсутствует constant folding и есть проблемы, если функция возвращает null.

Теперь посмотрим, как код будет выглядеть с новым интерфейсом LazyConstant:

// --enable-preview --source 26
class OrderController {
    private final LazyConstant<Logger> logger
            = LazyConstant.of(() -> Logger.create(OrderController.class));

    void submitOrder(User user, List<Product> products) {
        logger.get().info("order started");
        ...
        logger.get().info("order submitted");
    }
}

Теперь для получения логгера нужно использовать объект LazyConstant, который инкапсулирует в себе ленивую логику вычисления логгера. При первом вызове метода get() содержимое вычисляется путём вызова переданного Supplier'а. Если же содержимое уже вычислено, то оно просто возвращается. LazyConstant гарантирует, что Supplier вызовется не более одного раза, тем самым обеспечивая потокобезопасность.

Под капотом LazyConstant реализован таким образом, что использует внутреннюю для JDK аннотацию @Stable для хранения содержимого в поле, не являющегося final. Эта аннотация даёт сигнал виртуальной машине, что поле не будет меняться более одного раза, а значит виртуальная машина после установки может считать его константным значением, что открывает возможность для constant folding. Таким образом, LazyConstant позволяет добиваться одновременно гибкости инициализации и хорошей производительности.

API также позволяет создавать не только значения с единичным содержимым, но и ленивые списки и словари. Приведём пример ленивого списка:

// --enable-preview --source 26
class Application {
    private static final List<OrderController> ORDERS
        = List.ofLazy(POOL_SIZE, _ -> new OrderController());

    public static OrderController orders() {
        long index = Thread.currentThread().threadId() % POOL_SIZE;
        return ORDERS.get((int)index);
    }
}

В примере выше список ORDERS – это список, который для каждого индекса вычисляет значение в момент обращения и не более одного раза. Таким образом, LazyConstant – это ещё и хороший вариант для написания кэшей.

Remove the Applet API (JEP 504)

API аплетов, которое стало deprecated в Java 9 и стало deprecated for removal в Java 17, было окончательно удалено. Удалению подлежал весь пакет java.applet, а также классы java.beans.AppletInitializer и javax.swing.JApplet.

Окончательное удаление Applet API вряд ли на что-то негативно повлияет, так как современные браузеры уже давно не поддерживают аплеты, а инструмент appletviewer был удалён ещё в Java 11.

HTTP/3 for the HTTP Client API (JEP 517)

HTTP-клиент теперь поддерживает третью версию протокола HTTP.

HTTP/3 – это последняя версия протокола HTTP, которая использует протокол QUIC в качестве транспорта вместо TCP. У HTTP/3 потенциального более быстрые рукопожатия, чем в предыдущих версиях, меньше проблем с застоями и более надёжный транспорт, особенно в окружениях с большими потерями пакетов. HTTP/3 был стандартизован в 2022 году.

Чтобы переключиться на HTTP/3 в HTTP-клиенте, нужно явно указать версию протокола:

var client = HttpClient.newBuilder()
                       .version(HttpClient.Version.HTTP_3)
                       .build();

Также можно указать версию конкретно для запроса:

var request = HttpRequest.newBuilder(URI.create("https://openjdk.org/"))
                         .version(HttpClient.Version.HTTP_3)
                         .GET().build();

Метод version() указывает на предпочитаемую версию протокола. Если сервер не поддерживает HTTP/3, то запросы не падают с ошибкой, а незаметно переключаются на HTTP/2 или HTTP/1.1.

Structured Concurrency (Sixth Preview) (JEP 525)

Structured Concurrency, которое было в режиме preview в Java 21, Java 22, Java 23, Java 24 и Java 25, остаётся в режиме preview в шестой раз.

В этом релизе есть небольшие изменения API:

Напомним, что Structured Concurrency – это подход многопоточного программирования, который заимствует принципы из однопоточного структурного программирования. Главная идея такого подхода заключается в следующем: если задача расщепляется на несколько конкурентных подзадач, то эти подзадачи воссоединяются в блоке кода главной задачи. Все подзадачи логически сгруппированы и организованы в иерархию. Каждая подзадача ограничена по времени жизни областью видимости блока кода главной задачи.

В центре нового API класс StructuredTaskScope, у которого есть два главных метода:

  • fork() – создаёт подзадачу и запускает её в новом виртуальном потоке,
  • join() – ждёт, пока не завершатся все подзадачи или пока scope не будет закрыт.

Пример использования StructuredTaskScope, где показана задача, которая параллельно запускает две подзадачи и дожидается результата их выполнения:

// --enable-preview --source 26
try (var scope = StructuredTaskScope.open()) {
    Subtask<String> user = scope.fork(() -> findUser());
    Subtask<Integer> order = scope.fork(() -> fetchOrder());

    scope.join(); // Join subtasks, propagating exceptions

    // Both subtasks have succeeded, so compose their results
    return new Response(user.get(), order.get());
}

Может показаться, что в точности аналогичный код можно было бы написать с использованием классического ExecutorService и submit(), но у StructuredTaskScope есть несколько принципиальных отличий, которые делают код безопаснее:

  • Время жизни всех потоков подзадач ограничено областью видимости блока try-with-resources. Метод close() гарантированно не завершится, пока не завершатся все подзадачи.
  • Если одна из операций findUser() и fetchOrder() завершается ошибкой, то другая операция отменяется автоматически, если ещё не завершена (в случае использования дефолтного JoinerawaitAllSuccessfulOrThrow(), но возможны другие с другим поведением).
  • Если главный поток прерывается в процессе ожидания join(), то обе операции findUser() и fetchOrder() отменяются при выходе из блока.
  • В дампе потоков будет видна иерархия: потоки, выполняющие findUser() и fetchOrder(), будут отображаться как дочерние для главного потока.

Structured Concurrency должно облегчить написание безопасных многопоточных программ благодаря знакомому структурному подходу.

PEM Encodings of Cryptographic Objects (Second Preview) (JEP 524)

API для кодирования криптографических объектов в формат PEM и декодирования обратно, которое появилось в Java 25, остаётся на второе preview.

Есть несколько изменений в API:

  • Класс PEMRecord переименован в PEM, и теперь в нём содержится метод decode(), который возвращает содержимое в формате Base64.
  • Методы encryptKey() в классе EncryptedPrivateKeyInfo переименованы в encrypt() и теперь принимают объекты DEREncodable, а не PrivateKey. Это позволяет шифровать объекты KeyPair и PKCS8EncodedKeySpec.
  • Класс EncryptedPrivateKeyInfo теперь содержит методы getKeyPair(), которые расшифровывают текст в формате PKCS#8, содержащий PublicKey.
  • Исключения, которые выбрасываются методами getKey() в классе EncryptedPrivateKeyInfo, теперь согласуются с исключениями, которые выбрасываются методами getKeySpec() в том же классе.
  • Классы PEMEncoder и PEMDecoder теперь поддерживают шифрофание и расшифровку объектов KeyPair и PKCS8EncodedKeySpec.

Напомним, что новое API для кодирования в формат PEM позволяет кодировать самые разные криптографические сущности: открытые ключи, закрытые ключи, сертификаты и т.д.

Вот пример открытого ключа, закодированного в формате PEM:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/kRGOL7wCPTN4KJ2ppeSt5UYB6u
cPjjuKDtFTXbguOIFDdZ65O/8HTUqS/sVzRF+dg7H3/tkQ/36KdtuADbwQ==
-----END PUBLIC KEY-----

Такой ключ можно декодировать с помощью нового класса java.security.PEMDecoder:

// --enable-preview --source 26
PEMDecoder decoder = PEMDecoder.of();
PublicKey key = (PublicKey) decoder.decode(data);
System.out.println(key);

Кодирование происходит с помощью класса java.security.PEMEncoder:

// --enable-preview --source 26
PEMEncoder encoder = PEMEncoder.of();
String data = encoder.encodeToString(key);
System.out.println(data);

Список всех криптографических объектов, которые можно кодировать/декодировать, лимитирован наследниками нового sealed интерфейса java.security.DEREncodable:

public sealed interface DEREncodable permits AsymmetricKey, KeyPair,
    PKCS8EncodedKeySpec, X509EncodedKeySpec, EncryptedPrivateKeyInfo,
    X509Certificate, X509CRL, PEM {
}

Среди наследников выделяется особенный класс java.security.PEM. Этот класс может содержать в себе любые PEM-данные. Он может пригодиться, когда для криптографического объекта в Java нет соответствующего API (например, запрос сертификата PKCS #10):

public record PEM(String type, String content, byte[] leadingData) implements DEREncodable {
    ...
}
PEM pr = PEMDecoder.of().decode(pem, PEM.class);
Vector API (Eleventh Incubator) (JEP 529)

Векторное API в модуле jdk.incubator.vector, которое появилось ещё аж в Java 16, остаётся в инкубационном статусе в одиннадцатый раз.

Векторное API остаётся так долго в инкубаторе, потому что зависит от некоторых фич проекта Valhalla (главным образом, от value-классов), который пока что находится в разработке. Как только эти фичи станут доступны в виде preview, векторное API перейдёт из инкубатора в статус preview.

JVM

Ahead-of-Time Object Caching with Any GC (JEP 516)

Ahead-of-time кэш, который появился в Java 24, теперь поддерживает все сборщики мусора, включая ZGC.

Ранее ZGC не поддерживался из-за того, что в нём использовался GC-specific формат, совместимый только со сборщиками мусора Serial, Parallel и G1. Сейчас же реализован новый GC-agnostic формат, который не зависит от сборщика мусора.

GC-agnostic формат имеет важное отличие: он является потоковым, то есть JVM не отображает его целиком в память (как в случае с GC-specific), а подгружает объекты последовательно в фоновом потоке.

Новый формат является опциональным и включается во время тренировочного запуска в следующих случаях:

  • Если использовать GC-agnostic формат указано явно через опцию -XX:+AOTStreamableObjects.
  • При использовании сборщика мусора ZGC (-XX:+UseZGC).
  • Если отключены сжатые указатели (-XX:-CompressedOops).
  • Если размер кучи больше чем 32 GB.

В остальным случаях используется старый отображаемый в память формат.

G1 GC: Improve Throughput by Reducing Synchronization (JEP 522)

В сборщике мусора G1 было уменьшено количество синхронизации между потоками приложения и потоками сборщика мусора. Благодаря этому производительность приложений, в которых интенсивно модифицируются ссылки в объектах, выросла на 5-15%. Кроме того, были упрощены барьеры записи (write barriers): фрагменты кода, которые внедряет GC и JIT в инструкции присвоения ссылок в объектах. Это упрощение привело к тому, что барьеры стали быстрее и стали меньше по размеру, в результате чего производительность выросла до 5% и в приложениях, в которых ссылки в объектах модифицируются не так интенсивно.

Достичь уменьшения синхронизации (и упрощения барьеров) удалось с помощью введения второй таблицы карт (card table), которую модифицирует только поток-оптимизатор G1, но не трогают потоки приложения, т.к. они обновляют в это время первую таблицу. Как только G1 понимает, что сканирование первой таблицы карт превышает по времени целевое значение пауз, он атомарно переключает таблицы. После этого приложение начинает обновлять вторую таблицу, а поток-оптимизатор начинает оптимизировать первую (заполненную) таблицу.

Подписывайтесь на канал в Telegram, чтобы не пропускать новости.

2026
  • Вышла Java 26
Все материалы на этом сайте выложены под лицензией CC BY-SA 4.0
© Евгений Козлов, 2017-2026
Feed
Table of JEPs