Вот список JEP'ов, которые попали в Java 21. Паттерн-матчинг для switch наконец-то был финализирован и стал стабильной конструкцией языка. Напомним, что он появился в Java 17 и был в состоянии preview четыре релиза: 17, 18, 19 и 20. Новый паттерн-матчинг существенно расширяет возможности оператора switch. Начиная с Java 1.0, switch поддерживал только сравнение с примитивными константами. Позже список типов был расширен (Java 5 – перечисления, Java 7 – строки), но в ветках case всё ещё могли быть только константы. Теперь же switch поддерживает в ветках case так называемые паттерны: Паттерны могут снабжаться условиями с использованием нового ключевого слова when: Также добавлена поддержка матчинга null. Сделать это можно с помощью явной отдельной ветки case null: Если ветка case null отсутствует, то switch с переданным в него null всегда будет выбрасывать NullPointerException (даже если есть ветка default): Ветки null и default можно объединять друг с другом: Новый паттерн-матчинг обладает рядом ограничений. Во-первых, все switch (кроме тех, что были корректными до Java 21) должны быть исчерпывающими. Т.е. в ветках должны покрываться все возможные случаи: Пример выше можно исправить, добавив ветку Object o или default. Во-вторых, все ветки case должны располагаться в таком порядке, что ни перед одной веткой нет доминирующей ветки: Отдельным видом паттернов являются паттерны записей. Они появились в Java 19 в режиме preview и стали стабильными в Java 21. Паттерны записей позволяют осуществлять деконструкцию значений записей чрезвычайно компактно: Или через оператор switch: Особая мощь паттернов записей состоит в том, что они могут быть вложенными: Используя var, можно сократить код ещё сильнее: Паттерны записей отлично сочетаются с паттернами по типу: Поддерживается вывод типов записей-дженериков: К сожалению, паттерны записей могут использоваться только в instanceof и switch, но не могут использоваться сами по себе: Будем надеяться, что когда-нибудь добавят и такую возможность. Строковые шаблоны – новая синтаксическая возможность, позволяющая встраивать в строки выражения: Таким образом, в Java появилась строковая интерполяция, которая уже давно есть во многих других известных языках программирования. Однако в Java она работает только в режиме preview, т.е. использовать в Java 21 её можно только с включенным флагом --enable-preview. Реализация строковых шаблонов в Java отличается от большинства реализаций в других языках: в Java строковый шаблон на самом деле сначала превращается в объект java.lang.StringTemplate, а затем процессор, реализующий java.lang.StringTemplate.Processor, конвертирует этот объект в строку (или объект другого класса). В примере выше STR."…" есть ничто иное, как сокращённый вариант следующего кода: STR – это стандартный и наиболее часто используемый процессор, который выполняет простую подстановку значений в шаблон и возвращает сконкатенированную строку. STR неявно импортируется в любой исходный файл, поэтому его можно использовать без import. RAW – это процессор, который ничего не делает со StringTemplate и просто возвращает его. Обычно он не используется, т.к. на практике мало кому нужны сырые представления шаблонов, а нужны результаты интерполяции в виде готовых объектов. Процессоры были введены для того, чтобы была возможность кастомизировать процесс интерполяции. Например, ещё один стандартный процессор FMT поддерживает форматирование с использованием спецификаторов, определённых в java.util.Formatter: Процессоры необязательно должны возвращать String. Вот общая сигнатура метода process() интерфейса Processor: Это значит, что можно реализовать процессор, который будет делать практически всё что угодно и возвращать что угодно. Например, гипотетический процессор JSON будет создавать напрямую объекты JSON (без промежуточного объекта String) и при этом поддерживать экранирование кавычек: Если в name, phone или address будут содержаться кавычки, то они не испортят объект, т.к. процессор заменит " на \". Или, например, процессор SQL будет создавать PreparedStatement'ы, защищая от атак SQL Injection: Таким образом, строковые шаблоны гораздо более мощный инструмент, нежели простая конкатенирующая строковая интерполяция. Они решают не только проблему простого внедрения выражений в строки и увеличивают читабельность, но и улучшают безопасность и гибкость программ. Ещё одно новшество в режиме preview: теперь можно объявлять так называемые безымянные переменные и паттерны. Делается это с помощью символа подчеркивания (_). Это часто необходимо, когда переменная или паттерн не используются: В примере выше важен факт наличия элемента, но сама переменная не нужна. Поэтому, чтобы не придумывать этой переменной название, было использовано подчеркивание вместо имени. Довольно частый пример нужности безымянных переменных – блок catch с неиспользуемым исключением: Полный список случаев, в которых можно использовать безымянные переменные: Здесь разработчику понадобились только координаты точки, но не её цвет. Без безымянного паттерна ему пришлось бы объявлять неиспользуемую переменную типа Color и придумывать ей имя: Такой код менее читабелен и хуже позволяет сфокусироваться на главном (координатах). Кроме того, некоторые IDE подсветили бы неиспользуемую переменную c, что ещё одно дополнительное неудобство. Есть также возможность объявлять безымянные переменные паттернов: Безымянные паттерны и переменные паттернов прекрасно сочетаются и со switch: В целом, паттерн-матчинг и безымянные паттерны вместе обладают большой синергией и позволяют писать действительно мощные, компактные и выразительные конструкции. Теперь в режиме preview можно запускать программы с методами main(), которые не являются public static и у которых нет параметра String[] args: В таком случае JVM сама создаст экземпляр класса (у него должен быть не-private конструктор без параметров) и вызовет у него метод main(). Протокол запуска будет выбирать метод main() согласно следующему приоритету: Кроме того, можно писать программы и без объявления класса вовсе: В таком случае будет создан неявный безымянный класс (не путать с анонимным классом), которому будут принадлежать метод main() и другие верхнеуровневые объявления в файле: Безымянный класс является синтетическим и final. Его simple name является пустой строкой: Виртуальные потоки, которые много лет разрабатывались в рамках проекта Loom и появились в Java 19 в режиме preview, теперь наконец-то стали стабильными. Виртуальные потоки, в отличие от потоков операционной системы, являются легковесными и могут создаваться в огромном количестве (миллионы экземпляров). Это свойство должно значительно облегчить написание конкурентных программ, поскольку позволит применять простой подход «один запрос – один поток» (или «одна задача – один поток») и не прибегать к более сложным асинхронному или реактивному программированию. При этом миграция на виртуальные потоки уже существующего кода должна быть максимально простой, потому что виртуальные потоки являются экземплярами существующего класса java.lang.Thread и практически полностью совместимы с классическими потоками: поддерживают стек-трейсы, interrupt(), ThreadLocal и т.д. Виртуальные потоки реализованы поверх обычных потоков и существуют только для JVM, но не для операционной системы (отсюда и название «виртуальные»). Поток, на котором в данный момент выполняется виртуальный поток, называется потоком-носителем. Если потоки платформы полагаются на планировщик операционной системы, то планировщиком для виртуальных потоков является ForkJoinPool. Когда виртуальный поток блокируется на некоторой блокирующей операции, то он размонтируется от своего потока-носителя, что позволяет потоку-носителю примонтировать другой виртуальный поток и продолжить работу. Такой режим работы и дешевизна виртуальных потоков позволяет им очень хорошо масштабироваться. Однако на данный момент есть два исключения: synchronized блоки и JNI. При их выполнении виртуальный поток не может быть размонтирован, поскольку он привязан к своему потоку-носителю. Такое ограничение может препятствовать масштабированию. Поэтому при желании максимально использовать потенциал виртуальных потоков рекомендуется избегать synchronized блоков и операции JNI, которые выполняются часто или занимают длительное время. Несмотря на привлекательность виртуальных потоков, вовсе необязательно предпочитать только их и всегда избегать классических потоков. Например, для задач, интенсивно и долго использующих CPU, лучше подойдут обычные потоки. Или если нужен поток, не являющийся демоном, то также придётся использовать обычный поток, потому что виртуальный поток всегда является демоном. Для создания виртуальных потоков и работы с ними появилось следующее API: Для виртуальных потоков также добавилась поддержка в инструментарии JDK (дебаггер, JVM TI, Java Flight Recorder). Появились три новых интерфейса SequencedCollection, SequencedSet и SequencedMap. SequencedCollection является наследником Collection и представляет собой коллекцию с установленным порядком элементов. Такими коллекциями являются LinkedHashSet и все реализации List, SortedSet и Deque. У этих коллекций есть общее свойство последовательности элементов, но до Java 21 их общим родителем был Collection, который является слишком общим интерфейсом и не содержит многих методов, характерных для последовательностей (getFirst(), getLast(), addFirst(), addLast(), reversed() и т.д). При этом у самих вышеописанных коллекций такие методы были несогласованны друг с другом (например, list.get(0) против sortedSet.first() против deque.getFirst()), либо вовсе отсутствовали (например, linkedHashSet.getLast()). SequencedCollection закрыла эту дыру в иерархии и привела API к общему знаменателю: Теперь больше не надо думать, как для конкретной коллекции получить последний элемент, потому что есть универсальный метод getLast(), который есть и у ArrayList, и у TreeSet, и у ArrayDeque. Особый интерес представляет метод reversed(), который возвращает view коллекции с обратным порядком. Это делает обратный обход коллекции гораздо более лаконичным: Для LinkedHashSet эффективного способа обратного обхода и вовсе не было. Для последовательных множеств ввели интерфейс SequencedSet: Его реализациями являются LinkedHashSet и наследники SortedSet. Также ввели интерфейс SequencedMap: Его реализациями являются LinkedHashMap и наследники SortedMap. Scoped Values, которые появились в Java 20 в инкубационном статусе, теперь стали Preview API. Новый класс ScopedValue позволяет обмениваться иммутабельными данными без их передачи через аргументы методов. Он является альтернативой существующему классу ThreadLocal. Классы ThreadLocal и ScopedValue похожи тем, что решают одну и ту же задачу: передать значение переменной в рамках одного потока (или дерева потоков) из одного места в другое без использования явного параметра. В случае ThreadLocal для этого вызывается метод set(), который кладёт значение переменной для данного потока, а потом метод get() вызывается из другого места для получения значения переменной. У данного подхода есть ряд недостатков: Эти проблемы усугубляются с появлением виртуальных потоков, которые могут создаваться в гораздо большем количестве, чем обычные. ScopedValue лишён вышеперечисленных недостатков. В отличие от ThreadLocal, ScopedValue не имеет метода set(). Значение ассоциируется с объектом ScopedValue путём вызова другого метода where(). Далее вызывается метод run(), на протяжении которого это значение можно получить (через метод get()), но нельзя изменить. Как только исполнение метода run() заканчивается, значение отвязывается от объекта ScopedValue. Поскольку значение не меняется, решается и проблема дорогого наследования: дочерним потокам не надо копировать значение, которое остаётся постоянным в течение периода жизни. Пример использования ScopedValue: В целом ScopedValue является предпочтительной заменой ThreadLocal, т.к. навязывает разработчику безопасную однонаправленную модель работы с неизменяемыми данными. Однако такой подход не всегда применим для некоторых задач, и для них ThreadLocal может быть единственно возможным решением. Ещё одно API, которое ранее было в инкубационном статусе (Java 19 и 20), а теперь стало Preview API – это Structured Concurrency. Structured Concurrency – это подход многопоточного программирования, который заимствует принципы из однопоточного структурного программирования. Главная идея такого подхода заключается в следующем: если задача расщепляется на несколько конкурентных подзадач, то эти подзадачи воссоединяются в блоке кода главной задачи. Все подзадачи логически сгруппированы и организованы в иерархию. Каждая подзадача ограничена по времени жизни областью видимости блока кода главной задачи. В центре нового API класс StructuredTaskScope, у которого есть два главных метода: Пример использования StructuredTaskScope, где показана задача, которая параллельно запускает две подзадачи и дожидается результата их выполнения: Может показаться, что в точности аналогичный код можно было бы написать с использованием классического ExecutorService и submit(), но у StructuredTaskScope есть несколько принципиальных отличий, которые делают код безопаснее: Structured Concurrency должно облегчить написание безопасных многопоточных программ благодаря знакомому структурному подходу. Foreign Function & Memory API, ставшее preview в Java 19, продолжает находиться в этом статусе. API находится в пакете java.lang.foreign. Напомним, что FFM API много лет разрабатывается в проекте Panama с целью заменить JNI. В Java 22 API выйдет из состояния preview. Векторное API в модуле jdk.incubator.vector, которое появилось ещё аж в Java 16, остаётся в инкубационном статусе в шестой раз. В этом релизе лишь небольшие изменения API, исправления багов и улучшения производительности. Векторное API останется в инкубаторе, пока необходимые фичи проекта Valhalla не станут preview. В пакете javax.crypto появилось новое API, реализующее механизм инкапсуляции ключей. Механизм инкапсуляции ключей (KEM) – это современная криптографическая техника, позволяющая обмениваться симметричными ключами, используя асимметричное шифрование. Если в традиционной технике симметричный ключ генерируется случайным образом и шифруется с помощью открытого ключа (что требует паддинга), то в KEM симметричный ключ выводится из самого открытого ключа. В Java KEM API состоит из трёх главных классов. KEM – входная точка API. У него есть метод getInstance(), возвращающий объект KEM для указанного алгоритма. Encapsulator – представляет собой функцию инкапсуляции, которая вызывается отправителем. У этого класса есть метод encapsulate(), который принимает открытый ключ и возвращает секретный ключ, а также key encapsulation message (которое шлётся принимающей стороне). Decapsulator – функция декапсуляции, которая вызывается принимающей стороной. У класса есть метод decapsulate(), который принимает key encapsulation message и возвращает секретный ключ. Таким образом, у обеих сторон теперь есть одинаковый симметричный ключ, с помощью которого можно дальше обмениваться данными с помощью обычного симметричного шифрования. Пример генерации симметричного ключа и его передачи: Для KEM также добавлен интерфейс KEMSpi, позволяющий предоставлять пользовательские реализации алгоритмов KEM. В сборщик мусора ZGC, который появился в Java 15, добавили поддержку поколений. Поколения в ZGC пока что отключены по умолчанию, и для их включения требуется ключ -XX:+ZGenerational: В будущих версиях Java режим работы с поколениями будет по умолчанию, и ключ -XX:+ZGenerational уже требоваться не будет. Поколения в ZGC должны улучшить производительность Java-программ, т.к. молодые объекты, которые склонны умирать рано согласно слабой гипотезе о поколениях, будут собираться чаще, а старые объекты – более редко. При этом характеристики ZGC не должны от этого пострадать: время отклика по-прежнему должно быть сверхнизким (< 1ms) и кучи гигантских размеров (несколько терабайт) должны продолжать поддерживаться. Напомним, что также ведётся работа над поддержкой поколений в другом сборщике мусора Shenandoah, похожем по характеристикам на ZGC. Однако в Java 21 Generational Shenandoah попасть не успел. Сборщиком мусора по умолчанию по-прежнему остаётся G1. Он стал дефолтным сборщиком мусора в Java 9 (до него дефолтным был Parallel GC) При динамической загрузке агентов теперь выдаётся предупреждение: Агент – это компонент, который может изменять (инструментировать) код Java-приложения во время работы. Поддержка агентов появилась в Java 5, чтобы была возможность писать продвинутые инструменты вроде профилировщиков, которым необходимо добавлять эмиссию событий в классы, или AOP-библиотек. Для включения агентов требовались опции командной строки -javaagent или -agentlib, поэтому все агенты тогда могли включаться только явно при старте приложения. Однако в Java 6 появился Attach API, который, кроме всего прочего, позволил загружать агенты динамически прямо в работающий JVM. Благодаря этому библиотеки получили возможность подключаться к приложению и по-тихому изменять классы, не имея на то согласия от владельца приложения. Причём изменяться могут не только классы приложения, но и классы JDK. Таким образом, подвергается риску строгая инкапсуляция, которая является одним из краеугольных камней Java. Чтобы закрыть такую потенциально опасную дыру, в Java 9 вместе с появлением модулей было предложено запретить динамическую загрузку агентов по умолчанию. Однако тогда было решено отложить на неопределённое время такое радикальное решение, чтобы дать авторам инструментов время подготовиться. В итоге, изменение дожило до наших дней, и было реализовано лишь в Java 21, но в виде предупреждения. Чтобы подавить предупреждение, необходимо запускать JVM с опцией -XX:+EnableDynamicAgentLoading, либо загружать агенты при старте JVM, явно перечисляя их с помощью опций -javaagent или -agentlib. В будущих версиях Java планируется полностью отключить динамическую загрузку по умолчанию, и она уже не будет работать без -XX:+EnableDynamicAgentLoading.