Вышла Java 26

15 мин
Вышла 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
}

программный интерфейс

Lazy Constants (Second Preview) (JEP 526)

(Гиперссылка)

программный оболочку для ленивых констант, которые в 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 программный интерфейс вряд ли на что-то негативно повлияет, так как современные браузеры уже давно не поддерживают аплеты, а средство appletviewer был удалён ещё в Java 11.

HTTP/3 for the HTTP Client программный интерфейс (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 в шестой раз.
В этом релизе есть небольшие изменения программный интерфейс:

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

В центре нового программный интерфейс класс 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.

Напомним, что новое программный оболочку для кодирования в формат 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 программный оболочку (Eleventh Incubator) (JEP 529)

(Гиперссылка)

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

Векторное программный интерфейс остаётся так долго в инкубаторе, потому что зависит от некоторых фич проекта Valhalla (главным образом, от value-классов), который пока что находится в разработке. Как только эти фичи станут доступны в виде preview, векторное программный интерфейс перейдёт из инкубатора в статус 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 понимает, что сканирование первой таблицы карт превышает по времени целевое значение пауз, он атомарно переключает таблицы. После этого приложение начинает обновлять вторую таблицу, а поток-оптимизатор начинает оптимизировать первую (заполненную) таблицу.

Читают сейчас

В Steam одновременно вышли две инди-игры с одинаковым названием

16 минут назад

В Steam одновременно вышли две инди-игры с одинаковым названием

В Steam практически одновременно появилось две никак не связанные друг с другом инди-игры с идентичным названием — Piece by Piece. Одновременно сами разработчики не стали конфликтовать, а решили извле

Microsoft может отказаться от обязательных учётных записей в Windows 11

36 минут назад

Microsoft может отказаться от обязательных учётных записей в Windows 11

Некоторые высокопоставленные инженеры Microsoft выступили за функция отказа от обязательного использования учётных записей при входе в систему в Windows 11. Читать далее

Opensophy Hub — начальный open-source инициатива от Opensophy: гибридная система для документации с богатым Markdown

1 час назад

Opensophy Hub — начальный open-source инициатива от Opensophy: гибридная система для документации с богатым Markdown

Opensophy выпустила свой начальный открытый open-source проект — Hub, гибридную SSG+SPA платформу для публикации технических знаний, статей и документации. Ознакомиться с проектом

НИЯУ МИФИ в партнёрстве с Яндекс Практикумом проводит День открытых дверей онлайн-магистратуры для DE, ML, CV и NLP

1 час назад

НИЯУ МИФИ в партнёрстве с Яндекс Практикумом проводит День открытых дверей онлайн-магистратуры для DE, ML, CV и NLP

Привет, это команда Яндекс Практикума! Приглашаем вас на День открытых дверей онлайн-магистратуры «Специалист по работе с данными и применению ИИ» НИЯУ МИФИ в партнёрстве с Практикумом. На встрече мы

Инициатива Crypto.com заявил о сокращении приблизительно 12% сотрудников на фоне масштабного внедрения ИИ в бизнес-процессы

1 час назад

Инициатива Crypto.com заявил о сокращении приблизительно 12% сотрудников на фоне масштабного внедрения ИИ в бизнес-процессы

Криптовалютная система Crypto.com сократила приблизительно 12% своего персонала в связи с масштабного внедрения искусственного интеллекта в бизнес-процессы, сообщил основатель и генеральный директор п