Вебинар: SAST как Quality Gate - 13.03
Уже скоро, 18 марта, выйдет новая версия Java. Поэтому предлагаю посмотреть, какие в ней будут новшества, включая финализацию давно ожидаемых Stream Gatherers!
Порядок нововведений (JEP — JDK Enhancement Proposal) выбран по собственной оценке их "интересности", а не по их нумерации.
Как известно, операции Stream API разделяются на промежуточные (порождающие новый Stream
) и терминирующие (создающие результат или имеющие побочный эффект). Однако у терминирующих есть collect(Collector)
, позволяющий создавать свои операции посредством реализации Collector
. А вот список промежуточных расширить нельзя: ограничиваемся map
, flatMap
, filter
, distinct
, sorted
, peek
, sorted
. Во всяком случае, так было до Java 24, ведь в ней введены Stream Gatherers.
Ключевые моменты нововведения состоят в следующем:
1. В java.util.stream.Stream
добавлен метод gather(Gatherer)
.
2. Добавлен интерфейс java.util.stream.Gatherer
. Его спецификация заключается в четырёх методах:
initializer
— создание изначального промежуточного состояния. Использует Supplier
;integrator
— обработка элементов, опциональное использование промежуточного состояния, отправка результата далее в поток. Использует новый функциональный интерфейс Integrator
;combiner
— объединение состояний. Использует BinaryOperator
;finisher
— последняя операция использования промежуточного состояния и отправки результата далее в поток (после потребления всех элементов). Использует BiConsumer
.3. Добавлен класс java.util.stream.Gatherers
, который содержит несколько стандартных реализаций Gatherer
:
fold
— подобная reduce
операция;mapConcurrent
— map
с использованием Virtual Threads;scan
— инкрементальная аккумулирующая операция;windowFixed
— стандартная реализация Fixed Window;windowSliding
— стандартная реализация Sliding Window.Sliding Window, или метод скользящего окна, подразумевает создание окна или диапазона размера N на входных данных и затем движение этого окна (смещение). Это легко показать на простой коллекции чисел (окно размера 3):
Результатом такой операции станет Stream
, содержащий все выделенные синей рамкой подколлекции.
Теперь с использованием Gatherers мы можем вывести все подколлекции следующим образом:
public static void main(String[] args) {
var list = List.of(
"1", "2", "3", "4", "5", "6", "7",
"8", "9", "10", "11", "12", "13", "14"
);
int k = 3;
list.stream()
.gather(Gatherers.windowSliding(k))
.forEach(sublist -> System.out.printf("%s ", sublist));
System.out.println();
}
Получаем вывод:
[1, 2, 3] [2, 3, 4] [3, 4, 5] [4, 5, 6] [5, 6, 7] [6, 7, 8] [7, 8, 9] [8, 9, 10] [9, 10, 11] [10, 11, 12] [11, 12, 13] [12, 13, 14]
Fixed Window имеет крайне схожую реализацию, но теперь смещение равно размеру окна:
Использование идентичное:
public static void main(String[] args) {
var list = List.of(
"1", "2", "3", "4", "5", "6", "7",
"8", "9", "10", "11", "12", "13", "14"
);
int k = 3;
list.stream()
.gather(Gatherers.windowFixed(k))
.forEach(sublist -> System.out.printf("%s ", sublist));
System.out.println();
}
Вывод:
[1, 2, 3] [4, 5, 6] [7, 8, 9] [10, 11, 12] [13, 14]
Помимо создания собственных классов, реализующих Gatherer
, есть статические фабричные методы:
Gatherer.of(integrator)
Gatherer.ofSequential(integrator)
Оба метода имеют вариации с различными дополнительными аргументами в виде функциональных интерфейсов, список которых был упомянут ранее (initializer
, integrator
, combiner
, finisher
).
Создание собственного API для работы с class-файлами было предложено ещё в JDK 22. Теперь в версии JDK 24 этот API финализируется.
Активная разработка Java в последние годы привела к появлению частых и регулярных изменений в байткоде, с которым взаимодействуют стандартные инструменты jlink
, jar
и другие. Для этого взаимодействия они используют библиотеки вроде ASM. Из-за этого для поддержки новых версий байткода инструменты должны дожидаться поддержки библиотеками, которые, в свою очередь, ждут финализации версии JDK. Подобные зависимости сильно замедляют процесс разработки и добавления новых возможностей в class-файлы.
Этот API едва ли найдёт применение для большинства разработчиков, но различные фреймворки и библиотеки (в том числе Spring, Hibernate) работают с байткодом и используют ASM. А старая версия ASM не может работать с новыми версиями JDK. При необходимости обновить в проекте версию JDK придётся обновить и ASM. А для обновления ASM нужно апдейтить всё, что от него зависит. Вот и получается, что обновить придётся практически всё... а хотелось просто поднять версию JDK.
Посмотрим, как выглядит новый API. Немного покопавшись в нём, получилось сделать простой пример чтения статических константных примитивных полей (потребуется базовое понимание структуры class-файлов):
public class ClassFileExample {
public static void main(String[] args) throws IOException {
var classFile = ClassFile.of().parse(Path.of("./Main.class"));
for (var field : classFile.fields()) {
var flags = field.flags();
if (flags.has(AccessFlag.STATIC) && flags.has(AccessFlag.FINAL)) {
System.out.printf("static final field %s = ", field.fieldName());
var value = field.attributes().stream()
.filter(ConstantValueAttribute.class::isInstance)
.map(ConstantValueAttribute.class::cast)
.findFirst()
.map(constant -> constant.constant().constantValue().toString())
.orElse("null");
System.out.printf("%s%n", value);
}
}
}
}
Решение подобной задачи, внезапно, имеет практическое применение, ведь использование рефлексии приводит к инициализации класса. Возможно, однажды я расскажу о последствиях такой непредумышленной инициализации.
А знакомые с ASM разработчики могут заметить, что авторы решили не использовать паттерн Visitor. Это обосновывается нововведениями Java, в частности pattern matching.
Целью нововведения является улучшение времени запуска приложений. Для этого добавляется возможность сохранения кэша загруженных классов. Генерация и использование кэша осуществляются в три шага:
1. Генерация конфигурации AOT. Для этого необходимо запустить приложение с флагом -XX:AOTMode=record
, и указать путь до итогового файла через -XX:AOTConfiguration=PATH
:
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -jar app.jar
2. Генерация самого кэша с использованием конфигурации. Режим AOT
меняется на create
, а также добавляется флаг с указанием пути вывода кэша -XX:AOTCache=PATH
:
java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot -jar app.jar
3. Запуск приложения с использованием кэша. Для этого оставляем лишь флаг -XX:AOTCache=PATH
:
java -XX:AOTCache=app.aot -jar app.jar
В тексте JEP указывают, что время запуска простой программы с использованием Stream API
уменьшилось с 0.031 секунд до 0.018 (разница 42%). Время запуска проекта на Spring (Spring PetClinic) уменьшилось с 4.486 до 2.604 секунд.
Я посмотрел разницу во времени запуска простого приложения с использованием Quarkus на примере проекта из недавно опубликованной книги "Quarkus in Action" (GitHub). Время запуска снизилось с 3.480 до 2.328 секунд, т.е. на 39.67%.
Этот JEP решает проблему блокировки платформенных потоков при использовании виртуальных потоков в synchronized
блоках. Для понимания изменения необходимо ознакомиться с Project Loom, а конкретно — с виртуальными потоками.
Ещё при введении виртуальных потоков (JEP 444) указывалось два случая, при которых они не освобождают несущий их платформенный поток в случае блокировки:
synchronized
блоке;Отныне первый случай не является действительным. Теперь разработчики могут свободно выбирать между использованием ключевого слова synchronized
и пакета java.util.concurrent.locks
, ориентируясь лишь на требования решаемой задачи.
Z Garbage Collector (ZGC) поддерживал два режима: Generational и Non-Generational. Поскольку Generational ZGC является лучшей опцией в большинстве случаев, было принято решение упростить дальнейшую поддержку ZGC, отключив один из режимов — Non-Generational. Теперь флаг ZGenerational
является устаревшим, и при его использовании будет выведено соответствующее сообщение:
При использовании методов из sun.misc.Unsafe
, связанных с памятью, будет выдаваться предупреждение. Этот функционал был заменён VarHandle API
и Foreign Function & Memory API
, а само изменение приближает удаление связанных с памятью методов из sun.misc.Unsafe
(которые уже были помечены как Deprecated for Removal) и мотивирует разработчиков библиотек переходить на использование новых альтернатив.
Использование Java Native Interface
(JNI) и Foreign Function & Memory
(FFM) теперь приводит к предупреждению:
Это первый шаг к ограничению использования JNI и FFM: в будущем планируется выбрасывать исключение. Это делается не для удаления возможности использования JNI или FFM (что было бы очень иронично, ведь последний вышел в релиз в Java 22), а чтобы соблюсти политику целостности по умолчанию (the policy of integrity by default). По факту это означает лишь то, что разработчики должны явно указывать, что согласны на использование небезопасных возможностей JDK (нативный код).
Использование флага ‑‑enable-linkable-runtime
при сборке JDK позволяет jlink
создавать образы без использования JMOD
файлов из JDK. Это сокращает итоговый размер образа на 25%.
JMOD-файлы появились ещё в Project Jigsaw, Java 9. Они используются в опциональной фазе линкования при использовании jlink
, т.е. при создании оптимизированного по занимаемому месту JRE.
Эти файлы выступают как альтернатива jar, позволяют хранить не только .class
файлы и ресурсы, но и нативные библиотеки, лицензии, исполняемые файлы. Всё это затем попадает в итоговый JRE. Поскольку при разработке стандартного приложения файлов JAR более чем достаточно, то использовать JMOD и не приходится. А ещё для этого формата катастрофически мало документации.
Хотя это и не затрагивает непосредственно разработчиков, но однозначно имеет эффект при работе с контейнерами или при необходимости создания минимальных образов. Однако такая сборка не включена изначально, и отдельные провайдеры JDK сами принимают решение об использовании этой оптимизации.
Например, в Eclipse Temurin уже начали использовать этот флаг. Поддержку такой сборки затем добавили и в GraalVM.
Подготовка к отключению java.lang.SecurityManager
началась ещё в Java 17, когда он был помечен как Deprecated for Removal.
Это связано с крайне редким использованием этого класса при высоких затратах на его поддержание. Теперь к изменениям.
Флаг -Djava.security.manager
в любых вариациях более не поддерживается и выдаёт ошибку:
Исключением является -Djava.security.manager=disallow
, а использование System::setSecurityManager
приведёт к исключению UnsupportedOperationException
.
Помимо этого, системные свойства, связанные с SecurityManager
отныне игнорируются, а также удалён файл conf/security/java.policy
.
Прочие изменения связаны с документацией: удаление упоминаний SecurityManager
и SecurityException
.
Стоит отметить, что сами классы и методы не удалены, а деградированы до "пустышек ", т.е. они либо возвращают null
, false
, либо сразу исполняют запрос, либо бросают SecurityException
или UnsupportedOperationException
.
Поддержка Windows 32-bit x86 наконец прекращается. Это упрощает инфраструктуру сборки и тестирования, а также позволяет перестать направлять ресурсы на поддержание этой платформы.
Одной из причин удаления этого порта является отсутствие поддержки virtual threads
, которые откатываются к классическим kernel threads
. Помимо этого, поддержка последней 32-битной версии Windows 10 завершается в октябре 2025 года.
Судьба остальных 32-битных платформ также очевидна: их удалят, но не в этом релизе.
Так случилось, что последней 32-битной поддерживаемой платформой является Linux. Для сборки 32-битной версии сейчас потребуется добавление флага ‑‑enable-deprecated-ports=yes
:
bash ./configure –enable-deprecated-ports=yes
Однако полное удаление этого порта ожидается уже в Java 25.
Этот и следующий JEP относятся к постквантовой криптографии.
Постквантовая криптография относится к созданию криптографических алгоритмов, которые будут эффективны даже после появления квантовых компьютеров.
Добавлена реализация ML-KEM
для KeyPairGenerator
, KEM
, KeyFactory
APIs, а именно ML-KEM-512
, ML-KEM-768
, ML-KEM-1024
согласно стандарту FIPS 203. Так, создание пары ключей можно осуществить следующим образом:
KeyPairGenerator generator = KeyPairGenerator.getInstance("ML-KEM-1024");
KeyPair keyPair = generator.generateKeyPair();
В продолжение предыдущего JEP.
Добавлена реализация ML-DSA
для KeyPairGenerator
, Signature
, KeyFactory
APIs, а именно ML-DSA-44
, ML-DSA-65
, ML-DSA-87
согласно стандарту FIPS 204. Подобно предыдущему пункту, посмотрим пример получения соответствующей подписи:
Signature signature = Signature.getInstance("ML-DSA");
Напоследок JEP, который не затрагивает напрямую разработчиков Java, это изменения в Garbage-first (G1) сборщике мусора, переносящие реализацию его барьеров на более поздний этап C2 JIT компиляции. Это должно помочь упростить понимание этих барьеров для будущих разработчиков, а также уменьшить время исполнения компиляции C2.
Помимо этого списка нововведений часть изменений все ещё находятся состоянии Preview или Experimental:
Рассчитываем, что в одном из будущих (и скорых) релизов получится рассмотреть и показать на практике эти нововведения.
Полный список ссылок JEP для подробного изучения можно прочитать по ссылке.
Java продолжает развиваться активными темпами, а появление Class-File API
уменьшает количество зависимостей, что ещё сильнее увеличивает скорость обновлений на всей платформе. Но на этом пока завершается релиз Java 24. Значит, можно возвращаться к долгому ожиданию Project Valhalla.
0