Вышла общедоступная версия Java 27. В этот релиз попало около 2500 закрытых задач и 9 JEP’ов. Release Notes можно посмотреть здесь. Полный список изменений API – здесь.
Java 27 не является LTS-релизом, и у него будут выходить обновления только полгода (до марта 2027 года).
Скачать JDK 27 можно по этим ссылкам:
-
Oracle JDK (лицензия NFTC)
-
OpenJDK (лицензия GPLv2 with Classpath Exception)
Рассмотрим все JEP’ы, которые попали в Java 27.
Primitive Types in Patterns, instanceof, and switch (Fifth Preview) (JEP 532)
(Ссылка)
Примитивные типы в паттернах, instanceof и switch, которые были в preview в Java 23, Java 24, Java 25 и Java 26, остаются на пятое preview без изменений.
Смысл новой фичи – это поддержка примитивных типов в паттернах и операторах instanceof / switch:
// --enable-preview --source 27Object 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-27 типы выражений-селекторов в 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 (Third Preview) (JEP 531)
(Ссылка)
API для ленивых констант, которое было в preview в Java 25 и Java 26, остаётся в preview в третий раз. В этом релизе были сделаны следующие изменения:
-
Удалены методы
LazyConstant::isInitializedиLazyConstant::orElse. -
Добавлен статический метод
Set::ofLazy.
Напомним смысл новой фичи. Для начала вспомним, как в Java можно реализовать отложенную инициализацию классическими средствами:
class OrderController { private Logger logger = null; Logger getLogger() { if (logger == null) { logger = Logger.create(OrderController.class); } return logger; } void submitOrder(User user, List 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 27class OrderController { private final LazyConstant logger = LazyConstant.of(() -> Logger.create(OrderController.class)); void submitOrder(User user, List 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 27class Application { private static final List 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 – это ещё и хороший вариант для написания кэшей.
В Java 28 выйдет четвёртое preview ленивых констант.
Structured Concurrency (Seventh Preview) (JEP 533)
(Ссылка)
Structured Concurrency, которое было в режиме preview в Java 21, Java 22, Java 23, Java 24, Java 25 и Java 26, остаётся в режиме preview в седьмой раз. В этом релизе есть изменения API:
-
У интерфейсов
StructuredTaskScopeиJoinerпоявился ещё один параметрR_Xдля типа исключения, который методjoin()может выбросить. -
Появилась новая перегрузка статического метода
StructuredTaskScope::openс одним параметромUnaryOperator(до этого была только версия метода с двумя параметрами:JoinerиUnaryOperator). -
Методы
Joiner::allSuccessfulOrThrow,Joiner::anySuccessfulOrThrowиJoiner::awaitAllSuccessfulOrThrowтеперь создаютJoiner’ы, которые выбрасываютExecutionExceptionв случае исключения. Также к ним добавились перегрузки, принимающиеFunction, которые позволяют указать другой тип исключения. -
Статический метод
Joiner::awaitAllбыл удалён. -
voidметодJoiner::onTimeoutбыл заменён на методJoiner:timeout, который позволяет указать результат в случае таймаута.
Напомним, что Structured Concurrency – это подход многопоточного программирования, который заимствует принципы из однопоточного структурного программирования. Главная идея такого подхода заключается в следующем: если задача расщепляется на несколько конкурентных подзадач, то эти подзадачи воссоединяются в блоке кода главной задачи. Все подзадачи логически сгруппированы и организованы в иерархию. Каждая подзадача ограничена по времени жизни областью видимости блока кода главной задачи.
В центре нового API класс StructuredTaskScope, у которого есть два главных метода:
-
fork()– создаёт подзадачу и запускает её в новом виртуальном потоке, -
join()– ждёт, пока не завершатся все подзадачи или пока scope не будет закрыт.
Пример использования StructuredTaskScope, где показана задача, которая параллельно запускает две подзадачи и дожидается результата их выполнения:
// --enable-preview --source 27try (var scope = StructuredTaskScope.open()) { Subtask user = scope.fork(() -> findUser()); Subtask 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()завершается ошибкой, то другая операция отменяется автоматически, если ещё не завершена (в случае использования дефолтногоJoiner’аawaitAllSuccessfulOrThrow(), но возможны другие с другим поведением). -
Если главный поток прерывается в процессе ожидания
join(), то обе операцииfindUser()иfetchOrder()отменяются при выходе из блока. -
В дампе потоков будет видна иерархия: потоки, выполняющие
findUser()иfetchOrder(), будут отображаться как дочерние для главного потока.
Structured Concurrency должно облегчить написание безопасных многопоточных программ благодаря знакомому структурному подходу.
В Java 28 Structured Concurrency станет постоянным API.
Compact Object Headers by Default (JEP 534)
(Ссылка)
Компактные заголовки объектов, которые появились в Java 25 (в Java 24 – в экспериментальном режиме), теперь включены по умолчанию. Таким образом, опция -XX:+UseCompactObjectHeaders больше не нужна. Также компактные заголовки можно выключить, если запустить Java с противоположным ключом:
$ java -XX:-UseCompactObjectHeaders ...
Компактные заголовки являются результатом работы в проекте Lilliput. Теперь размер заголовков объектов в JVM уменьшился с 96/128 бит до 64 бит на 64-битных платформах. Компактные заголовки не только уменьшают размер кучи, но и могут улучшить производительность благодаря более высокой скорости выделения новых объектов, более низкой нагрузки на GC и лучшей локальности данных.
Сжатие заголовков достигается за счёт объединения mark-слова (64 бит) и class-слова (64 или 32 бит, если включены сжатые указатели на классы) в одно 64-битное слово. В новой схеме указатели на классы всегда являются сжатыми, и количество бит для них уменьшается с 32 до 22. Identity хеш-код остаётся неизменным: 31 бит. Количество тег-битов становится на один больше (для GC self forwarding). Битов для возраста GC остаётся 4, как и было. Также 4 бита резервируются для Valhalla.
Работа в проекте Lilliput не закончена. В дальнейшем возможно ещё большое сжатие заголовков до 32 бит, что сократит потребление памяти ещё больше.
Make G1 the Default Garbage Collector in All Environments (JEP 523)
(Ссылка)
Сборщик мусора G1 стал включен по умолчанию во всех окружениях. Ранее в окружениях с одним процессором или небольшим количеством оперативной памяти (< 1792 MB) автоматически включался Serial GC. Так было потому, что в таких условиях Serial GC имел преимущества в пропускной способности и количеству потребляемой памяти. Однако за последние годы G1 достиг хорошего уровня производительности, поэтому такое переключение уже практически не имеет смысла. Если в вашем конкретном случае сборщик мусора Serial GC всё ещё имеет лучшую производительность, то его можно включить с помощью ключа -XX:+UseSerialGC.
JFR In-Process Data Redaction (JEP 536)
(Ссылка)
В JDK Flight Recorder появилась возможность редактирования чувствительных данных, чтобы они не попадали в итоговые файлы записи в открытом виде. К таким данным относятся пароли, токены, секретные ключи и прочие подобные конфиденциальные данные. Редактировать можно три вида атрибутов: аргументы командной строки, переменные окружения и системные свойства. Для возможности указания конкретных атрибутов для редактирования, появились две новые подопции у опции -XX:FlightRecorderOptions: redact-argument (для аргументов командной строки) и redact-key (для переменных окружения и системных свойств). Например, вот так может выглядеть запуск JFR, если нужно отредактировать переменные и свойства с именем confidential или CONFIDENTIAL, а также URL, содержащие имя пользователя и пароль:
$ export CONFIDENTIAL=SOME_SECRET$ java -XX:FlightRecorderOptions:'redact-key=confidential,redact-argument=https://*:*@*' -XX:StartFlightRecording:filename=dump.jfr -Dconfidential=ANOTHER_SECRET -jar application.jar https://john:YET_ANOTHER_SECRET@example.com/login --verbose
После запуска в файле JFR чувствительные данные будут заменяться на [REDACTED], например, событие jdk.JVMInformation будет выглядеть так:
jdk.JVMInformation { startTime = 17:39:02.196 (2026-02-15) jvmVersion = "Java HotSpot(TM) 64-Bit Server VM" jvmArguments = "-Dconfidential=[REDACTED] -XX:FlightRecorderOptions:redact-key=confidential,redact-argument=[REDACTED] -XX:StartFlightRecording:filename=dump.jfr" jvmFlags = "N/A" javaArguments = "-jar application.jar [REDACTED] --verbose" jvmStartTime = 17:39:02.050 (2026-02-15) pid = 43671}
Если явно не указать redact-argument или redact-argument, то используются фильтры по умолчанию. В их список входят распространённые паттерны вроде *passwd*, *password*, *credential*, *secret*, *token* и другие.
PEM Encodings of Cryptographic Objects (Third Preview) (JEP 538)
(Ссылка)
API для кодирования криптографических объектов в формат PEM и декодирования обратно, которое было в preview в Java 25 и Java 26, остаётся на третье preview. В этом релизе есть несколько изменений API
-
PEMтеперь является обычным классом, а не записью, и у него появились конструкторы, принимающие Base64 в виде массивов байт. -
Интерфейс
DEREncodableпереименован вBinaryEncodable. -
Методы
EncryptedPrivateKeyInfo::getKeyиEncryptedPrivateKeyInfo::getKeyPair, которые принималиProviderвторым аргументом, теперь принимают только один аргументKey. -
Метод
PEMDecoder::withFactoryпереименован вPEMDecoder::withFactoriesOf. -
Появился новый тип исключения
CryptoException.
Напомним, что новое API для кодирования в формат PEM позволяет кодировать самые разные криптографические сущности: открытые ключи, закрытые ключи, сертификаты и т.д.
Вот пример открытого ключа, закодированного в формате PEM:
-----BEGIN PUBLIC KEY-----MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/kRGOL7wCPTN4KJ2ppeSt5UYB6ucPjjuKDtFTXbguOIFDdZ65O/8HTUqS/sVzRF+dg7H3/tkQ/36KdtuADbwQ==-----END PUBLIC KEY-----
Такой ключ можно декодировать с помощью нового класса java.security.PEMDecoder:
// --enable-preview --source 27PEMDecoder decoder = PEMDecoder.of();PublicKey key = (PublicKey) decoder.decode(data);IO.println(key);
Кодирование происходит с помощью класса java.security.PEMEncoder:
// --enable-preview --source 27PEMEncoder encoder = PEMEncoder.of();String data = encoder.encodeToString(key);IO.println(data);
Список всех криптографических объектов, которые можно кодировать/декодировать, лимитирован наследниками нового sealed интерфейса java.security.BinaryEncodable:
public sealed interface BinaryEncodable permits AsymmetricKey, KeyPair, PKCS8EncodedKeySpec, X509EncodedKeySpec, EncryptedPrivateKeyInfo, X509Certificate, X509CRL, PEM, InternalBinaryEncodable {}
Среди наследников выделяется особенный класс java.security.PEM. Этот класс может содержать в себе любые PEM-данные. Он может пригодиться, когда для криптографического объекта в Java нет соответствующего API (например, запрос сертификата PKCS #10):
public final class PEM implements BinaryEncodable { String type(); // Cryptographic object type, from the header text // (e.g., "PRIVATE KEY") byte[] content(); // Base64-encoded PEM content byte[] leadingData(); // Any content preceding the PEM header byte[] decode(); // Decode Base64 content ...}
PEM pr = PEMDecoder.of().decode(pem, PEM.class);
В Java 28 PEM Encodings станут постоянным API.
Post-Quantum Hybrid Key Exchange for TLS 1.3 (JEP 527)
(Ссылка)
В реализации TLS 1.3 в JDK появилась поддержка пост-квантовых гибридных схем обмена ключей. Эти схемы комбинируют ML-KEM с традиционным ECDHE (Ephemeral Elliptic-Curve Diffie-Hellman). Всего появилось 3 гибридных схемы:
-
X25519MLKEM768 (комбинация ECDHE c X25519 и ML-KEM-768)
-
SecP256r1MLKEM768 (комбинация ECDHE c кривой secp256r1 и ML-KEM-768)
-
SecP384r1MLKEM1024 (комбинация ECDHE c кривой secp384r1 и ML-KEM-1024)
Для включения новых схем от разработчика ничего не требуется: во время рукопожатия X25519MLKEM768 будет иметь наивысший приоритет и будет использоваться TLS-клиентом, если сервер его поддерживает.
Появление новых пост-квантовых гибридных схем является очередным шагом в поддержке в JDK алгоритмов, устойчивых к потенциальным будущим атакам с помощью квантовых компьютеров. Ранее в Java 24 появилась поддержка ML-KEM и ML-DSA.
Vector API (Twelfth Incubator) (JEP 537)
(Ссылка)
Векторное API в модуле jdk.incubator.vector, которое появилось ещё аж в Java 16, остаётся в инкубационном статусе в двенадцатый раз.
Векторное API остаётся так долго в инкубаторе, потому что зависит от некоторых фич проекта Valhalla (главным образом, от value-классов), который появится только в Java 28. Поэтому вероятно в Java 28 векторное API перейдёт из инкубатора в статус preview.
Источник: habr.com