- PVSM.RU - https://www.pvsm.ru -

Вышла Java 21

Вышла общедоступная версия Java 21 [1]. В этот релиз попало около 2500 закрытых задач и 15 JEP'ов [2]. Release Notes можно посмотреть здесь [3]. Изменения API – здесь [4].

Java 21 является LTS-релизом, а значит у него будут выходить обновления как минимум 5 лет [5] с момента выхода.

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

Вот список JEP'ов, которые попали в Java 21.

Язык

Pattern Matching for switch (JEP 441) [10]

Паттерн-матчинг для switch наконец-то был финализирован и стал стабильной конструкцией языка. Напомним, что он появился в Java 17 [11] и был в состоянии preview [12] четыре релиза: 17 [13], 18 [14], 19 [15] и 20 [16].

Новый паттерн-матчинг существенно расширяет возможности оператора switch. Начиная с Java 1.0, switch поддерживал только сравнение с примитивными константами. Позже список типов был расширен (Java 5 – перечисления, Java 7 – строки), но в ветках case всё ещё могли быть только константы.

Теперь же switch поддерживает в ветках case так называемые паттерны:

Object obj = …
return switch (obj) {
    case Integer i -> String.format("int %d", i);
    case Long l -> String.format("long %d", l);
    case Double d -> String.format("double %f", d);
    case String s -> String.format("String %s", s);
    default -> obj.toString();
};

Паттерны могут снабжаться условиями с использованием нового ключевого слова when:

Object obj = …
return switch (obj) {
    case Integer i when i > 0 -> String.format("positive int %d", i);
    case Integer i -> String.format("int %d", i);
    case String s -> String.format("String %s", s);
    default -> obj.toString();
};

Также добавлена поддержка матчинга null. Сделать это можно с помощью явной отдельной ветки case null:

Object obj = …
switch (obj) {
    case null -> System.out.println("Null");
    case String s -> System.out.println("String: " + s);
    default -> System.out.println("Other");
}

Если ветка case null отсутствует, то switch с переданным в него null всегда будет выбрасывать NullPointerException (даже если есть ветка default):

Object obj = null;
switch (obj) { // NullPointerException
    case String s -> System.out.println("String: " + s);
    default -> System.out.println("Other");
}

Ветки null и default можно объединять друг с другом:

String str = …
switch (str) {
    case "Foo", "Bar" -> System.out.println("Foo or Bar");
    case null, default -> System.out.println("Null or other");
}

Новый паттерн-матчинг обладает рядом ограничений.

Во-первых, все switch (кроме тех, что были корректными до Java 21) должны быть исчерпывающими. Т.е. в ветках должны покрываться все возможные случаи:

Object obj = …
switch (obj) { // error: the switch statement does not cover all possible input values
    case String s -> System.out.println(s.length());
    case Integer i -> System.out.println(i);
};

Пример выше можно исправить, добавив ветку Object o или default.

Во-вторых, все ветки case должны располагаться в таком порядке, что ни перед одной веткой нет доминирующей ветки:

return switch (obj) {
    case CharSequence cs ->
        "sequence of length " + cs.length();
    case String s -> // error: this case label is dominated by a preceding case label
        "string of length " + s.length();
    default -> "other";
 };

Так как CharSequence это более широкий тип, чем String, то его ветка должна быть расположена ниже.

В-третьих, несколько паттернов в одной ветке работать не будут:

return switch (obj) {
    case String s, Integer i -> "string or integer"; // error: illegal fall-through from a pattern
    default -> "other";
 };

Т.е. сделать тест по нескольким типам в одной ветке пока что нельзя (хотя грамматика языка это позволяет). Это можно обойти, только включив режим preview и заменив s и i на символы подчёркивания (см. JEP про безымянные переменные ниже).

В целом новый паттерн-матчинг значительно увеличивает выразительность языка. Особенно хорошо он сочетается с записями. Паттерны записей мы рассмотрим отдельно, поскольку про них есть свой собственный JEP (см. следующий раздел).

Record Patterns (JEP 440) [17]

Отдельным видом паттернов являются паттерны записей. Они появились в Java 19 [18] в режиме preview и стали стабильными в Java 21.

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

record Point(int x, int y) {}

static void printSum(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        System.out.println(x + y);
    }
}

Или через оператор switch:

static void printSum(Object obj) {
    switch (obj) {
        case Point(int x, int y) -> System.out.println(x + y);
        default -> System.out.println("Not a point");
    }
}

Особая мощь паттернов записей состоит в том, что они могут быть вложенными:

record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}

static void printColorOfUpperLeftPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint(Point p, Color c), ColoredPoint lr)) {
        System.out.println(c);
    }
}

Используя var, можно сократить код ещё сильнее:

static void printColorOfUpperLeftPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint(var p, var c), var lr)) {
        System.out.println(c);
    }
}

Паттерны записей отлично сочетаются с паттернами по типу:

record Box(Object obj) {}

static void test(Box box) {
    switch (box) {
        case Box(String s) -> System.out.println("string: " + s);
        case Box(Object o) -> System.out.println("other: " + o);
    }
}

Поддерживается вывод типов записей-дженериков:

record Box<T>(T t) {}

static void test(Box<Box<String>> box) {
    if (box instanceof Box(Box(var s))) { // Infers Box<Box<String>>(Box<String>(String s))
        System.out.println("String " + s);
    }
}

К сожалению, паттерны записей могут использоваться только в instanceof и switch, но не могут использоваться сами по себе:

static void usePoint(Point p) {
    Point(var x, var y) = p; // Не сработает
    // Use x and y
}

Будем надеяться, что когда-нибудь добавят и такую возможность.

String Templates (Preview) (JEP 430) [19]

Строковые шаблоны – новая синтаксическая возможность, позволяющая встраивать в строки выражения:

int x = 10;
int y = 20;
String str = STR."{x} plus {y} equals {x + y}";
// В str будет лежать "10 + 20 equals 30"

Таким образом, в Java появилась строковая интерполяция, которая уже давно есть во многих других известных языках программирования. Однако в Java она работает только в режиме preview [12], т.е. использовать в Java 21 её можно только с включенным флагом --enable-preview.

Реализация строковых шаблонов в Java отличается от большинства реализаций в других языках: в Java строковый шаблон на самом деле сначала превращается в объект java.lang.StringTemplate [20], а затем процессор, реализующий java.lang.StringTemplate.Processor [21], конвертирует этот объект в строку (или объект другого класса). В примере выше STR."…" есть ничто иное, как сокращённый вариант следующего кода:

StringTemplate template = RAW."{x} plus {y} equals {x + y}";
String str = STR.process(template);

STR [22] – это стандартный и наиболее часто используемый процессор, который выполняет простую подстановку значений в шаблон и возвращает сконкатенированную строку. STR неявно импортируется в любой исходный файл, поэтому его можно использовать без import.

RAW [23] – это процессор, который ничего не делает со StringTemplate и просто возвращает его. Обычно он не используется, т.к. на практике мало кому нужны сырые представления шаблонов, а нужны результаты интерполяции в виде готовых объектов.

Процессоры были введены для того, чтобы была возможность кастомизировать процесс интерполяции. Например, ещё один стандартный процессор FMT [24] поддерживает форматирование с использованием спецификаторов, определённых в java.util.Formatter [25]:

double length = 46;
System.out.println(FMT."The length is %.2f{length} cm");
// The length is 46.00 cm

Процессоры необязательно должны возвращать String. Вот общая сигнатура метода process() [26] интерфейса Processor:

public interface Processor<R, E extends Throwable> {
    R process(StringTemplate stringTemplate) throws E;
}

Это значит, что можно реализовать процессор, который будет делать практически всё что угодно и возвращать что угодно. Например, гипотетический процессор JSON будет создавать напрямую объекты JSON (без промежуточного объекта String) и при этом поддерживать экранирование кавычек:

JSONObject doc = JSON."""
    {
        "name":    "{name}",
        "phone":   "{phone}",
        "address": "{address}"
    };
    """;

Если в name, phone или address будут содержаться кавычки, то они не испортят объект, т.к. процессор заменит " на ".

Или, например, процессор SQL будет создавать PreparedStatement'ы, защищая от атак SQL Injection:

PreparedStatement ps = SQL."SELECT * FROM Person p WHERE p.name = {name}";

Таким образом, строковые шаблоны гораздо более мощный инструмент, нежели простая конкатенирующая строковая интерполяция. Они решают не только проблему простого внедрения выражений в строки и увеличивают читабельность, но и улучшают безопасность и гибкость программ.

Unnamed Patterns and Variables (Preview) (JEP 443) [27]

Ещё одно новшество в режиме preview: теперь можно объявлять так называемые безымянные переменные и паттерны. Делается это с помощью символа подчеркивания (_). Это часто необходимо, когда переменная или паттерн не используются:

int acc = 0;
for (Order _ : orders) {
    if (acc < LIMIT) {
        … acc++ …
    }
}

В примере выше важен факт наличия элемента, но сама переменная не нужна. Поэтому, чтобы не придумывать этой переменной название, было использовано подчеркивание вместо имени.

Довольно частый пример нужности безымянных переменных – блок catch с неиспользуемым исключением:

String s = …
try {
    int i = Integer.parseInt(s);
    …
} catch (NumberFormatException _) {
    System.out.println("Bad number: " + s);
}

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

  • Локальная переменная в блоке,
  • Объявление ресурса в try-with-resources,
  • Заголовок for statement,
  • Заголовок улучшенного цикла for,
  • Исключение в блоке catch,
  • Параметр лямбда-выражения,
  • Переменная паттерна (см. ниже).

Внимательный читатель заметит, что в списке выше отсутствуют параметры методов. Действительно, они не могут быть безымянными, и для любых методов (как интерфейсов, так и классов) по-прежнему всегда нужно указывать имена параметров.

Символы подчёркивания также можно использовать для указания безымянных паттернов:

if (r instanceof ColoredPoint(Point(int x, int y), _)) {
    // Используются только x и y
}

Здесь разработчику понадобились только координаты точки, но не её цвет. Без безымянного паттерна ему пришлось бы объявлять неиспользуемую переменную типа Color и придумывать ей имя:

if (r instanceof ColoredPoint(Point(int x, int y), Color c)) { // Warning: unused c
    // Используются только x и y
}

Такой код менее читабелен и хуже позволяет сфокусироваться на главном (координатах). Кроме того, некоторые IDE подсветили бы неиспользуемую переменную c, что ещё одно дополнительное неудобство.

Есть также возможность объявлять безымянные переменные паттернов:

if (r instanceof ColoredPoint(Point(int x, int y), Color _)) {
    …
}

Безымянные паттерны и переменные паттернов прекрасно сочетаются и со switch:

switch (box) {
    case Box(RedBall _), Box(BlueBall _) -> processBox(box);
    case Box(GreenBall _)                -> stopProcessing();
    case Box(_)                          -> pickAnotherBox();
}

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

Unnamed Classes and Instance Main Methods (Preview) (JEP 445) [28]

Теперь в режиме preview можно запускать программы с методами main(), которые не являются public static и у которых нет параметра String[] args:

class HelloWorld {
    void main() {
        System.out.println("Hello, World!");
    }
}

В таком случае JVM сама создаст экземпляр класса (у него должен быть не-private конструктор без параметров) и вызовет у него метод main().

Протокол запуска будет выбирать метод main() согласно следующему приоритету:

  1. static void main(String[] args)
  2. static void main()
  3. void main(String[] args)
  4. void main()

Кроме того, можно писать программы и без объявления класса вовсе:

String greeting = "Hello, World!";

void main() {
    System.out.println(greeting);
}

В таком случае будет создан неявный безымянный класс (не путать с анонимным классом), которому будут принадлежать метод main() и другие верхнеуровневые объявления в файле:

// class <some name> { ← неявно
String greeting = "Hello, World!";

void main() {
    System.out.println(greeting);
}
// }

Безымянный класс является синтетическим [29] и final. Его simple name [30] является пустой строкой:

void main() {
    System.out.println(getClass().isUnnamed()); // true
    System.out.println(getClass().isSynthetic()); // true
    System.out.println(getClass().getSimpleName()); // ""
    System.out.println(getClass().getCanonicalName()); // null
}

При этом имя [31] класса совпадает с именем файла, но такое поведение не гарантируется.

Такое упрощение запуска Java-программ было сделано с двумя целями:

  1. Облегчить процесс обучения языку. На новичка, только что начавшего изучение Java, не должно сваливаться всё сразу, а концепции должны вводятся постепенно, начиная с базовых (переменные, циклы, процедуры) и постепенно переходя к более продвинутым (классы, области видимости).
  2. Облегчить написание коротких программ и скриптов. Количество церемоний для них должно быть сведено к минимуму.

API

Virtual Threads (JEP 444) [32]

Виртуальные потоки, которые много лет разрабатывались в рамках проекта Loom [33] и появились в Java 19 [34] в режиме preview, теперь наконец-то стали стабильными.

Виртуальные потоки, в отличие от потоков операционной системы, являются легковесными и могут создаваться в огромном количестве (миллионы экземпляров). Это свойство должно значительно облегчить написание конкурентных программ, поскольку позволит применять простой подход «один запрос – один поток» (или «одна задача – один поток») и не прибегать к более сложным асинхронному или реактивному программированию. При этом миграция на виртуальные потоки уже существующего кода должна быть максимально простой, потому что виртуальные потоки являются экземплярами существующего класса java.lang.Thread [35] и практически полностью совместимы с классическими потоками: поддерживают стек-трейсы, interrupt() [36], ThreadLocal [37] и т.д.

Виртуальные потоки реализованы поверх обычных потоков и существуют только для JVM, но не для операционной системы (отсюда и название «виртуальные»). Поток, на котором в данный момент выполняется виртуальный поток, называется потоком-носителем. Если потоки платформы полагаются на планировщик операционной системы, то планировщиком для виртуальных потоков является ForkJoinPool [38]. Когда виртуальный поток блокируется на некоторой блокирующей операции, то он размонтируется от своего потока-носителя, что позволяет потоку-носителю примонтировать другой виртуальный поток и продолжить работу. Такой режим работы и дешевизна виртуальных потоков позволяет им очень хорошо масштабироваться. Однако на данный момент есть два исключения: synchronized блоки и JNI. При их выполнении виртуальный поток не может быть размонтирован, поскольку он привязан к своему потоку-носителю. Такое ограничение может препятствовать масштабированию. Поэтому при желании максимально использовать потенциал виртуальных потоков рекомендуется избегать synchronized блоков и операции JNI, которые выполняются часто или занимают длительное время.

Несмотря на привлекательность виртуальных потоков, вовсе необязательно предпочитать только их и всегда избегать классических потоков. Например, для задач, интенсивно и долго использующих CPU, лучше подойдут обычные потоки. Или если нужен поток, не являющийся демоном [39], то также придётся использовать обычный поток, потому что виртуальный поток всегда является демоном.

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

  • Thread.Builder [40] – билдер потоков. Например, виртуальный поток можно создать путём вызова Thread.ofVirtual().name("name").unstarted(runnable).
  • Thread.startVirtualThread(Runnable) [41] – создаёт и сразу же запускает виртуальный поток.
  • Thread.isVirtual() [42] – проверяет, является ли поток виртуальным.
  • Executors.newVirtualThreadPerTaskExecutor() [43] – возвращает исполнитель, который создаёт новый виртуальный поток на каждую задачу.

Для виртуальных потоков также добавилась поддержка в инструментарии JDK (дебаггер, JVM TI, Java Flight Recorder).

Sequenced Collections (JEP 431) [44]

Появились три новых интерфейса SequencedCollection [45], SequencedSet [46] и SequencedMap [47].

SequencedCollection является наследником Collection [48] и представляет собой коллекцию с установленным порядком элементов. Такими коллекциями являются LinkedHashSet [49] и все реализации List [50], SortedSet [51] и Deque [52]. У этих коллекций есть общее свойство последовательности элементов, но до Java 21 их общим родителем был Collection, который является слишком общим интерфейсом и не содержит многих методов, характерных для последовательностей (getFirst(), getLast(), addFirst(), addLast(), reversed() и т.д). При этом у самих вышеописанных коллекций такие методы были несогласованны друг с другом (например, list.get(0) против sortedSet.first() против deque.getFirst()), либо вовсе отсутствовали (например, linkedHashSet.getLast()).

SequencedCollection закрыла эту дыру в иерархии и привела API к общему знаменателю:

interface SequencedCollection<E> extends Collection<E> {
    E getFirst();
    E getLast();
    void addFirst(E);
    void addLast(E);
    E removeFirst();
    E removeLast();
    SequencedCollection<E> reversed();
}

Теперь больше не надо думать, как для конкретной коллекции получить последний элемент, потому что есть универсальный метод getLast() [53], который есть и у ArrayList, и у TreeSet, и у ArrayDeque.

Особый интерес представляет метод reversed() [54], который возвращает view коллекции с обратным порядком. Это делает обратный обход коллекции гораздо более лаконичным:

var linkedList = new LinkedList<>(…);

// До Java 21
for (var it = linkedList.descendingIterator(); it.hasNext();) {
    var e = it.next();
    …
}

// С Java 21
for (var element : linkedList.reversed()) {
    …
}

Для LinkedHashSet эффективного способа обратного обхода и вовсе не было.

Для последовательных множеств ввели интерфейс SequencedSet:

interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
    SequencedSet<E> reversed();
}

Его реализациями являются LinkedHashSet и наследники SortedSet.

Также ввели интерфейс SequencedMap:

interface SequencedMap<K,V> extends Map<K,V> {
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
    V putFirst(K, V);
    V putLast(K, V);
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    SequencedMap<K,V> reversed();
}

Его реализациями являются LinkedHashMap [55] и наследники SortedMap [56].

Scoped Values (Preview) (JEP 446) [57]

Scoped Values, которые появились в Java 20 [58] в инкубационном статусе [59], теперь стали Preview API.

Новый класс ScopedValue [60] позволяет обмениваться иммутабельными данными без их передачи через аргументы методов. Он является альтернативой существующему классу ThreadLocal [37].

Классы ThreadLocal и ScopedValue похожи тем, что решают одну и ту же задачу: передать значение переменной в рамках одного потока (или дерева потоков) из одного места в другое без использования явного параметра. В случае ThreadLocal для этого вызывается метод set() [61], который кладёт значение переменной для данного потока, а потом метод get() [62] вызывается из другого места для получения значения переменной. У данного подхода есть ряд недостатков:

  • Неконтролируемая мутабельность (set() можно вызвать когда угодно и откуда угодно).
  • Неограниченное время жизни (переменная очистится, только когда завершится исполнение потока или когда будет вызван ThreadLocal.remove(), но про него часто забывают).
  • Высокая цена наследования (дочерние потоки всегда вынуждены делать полную копию переменной, даже если родительский поток никогда не будет её изменять).

Эти проблемы усугубляются с появлением виртуальных потоков, которые могут создаваться в гораздо большем количестве, чем обычные.

ScopedValue лишён вышеперечисленных недостатков. В отличие от ThreadLocal, ScopedValue не имеет метода set(). Значение ассоциируется с объектом ScopedValue путём вызова другого метода where() [63]. Далее вызывается метод run() [64], на протяжении которого это значение можно получить (через метод get() [65]), но нельзя изменить. Как только исполнение метода run() заканчивается, значение отвязывается от объекта ScopedValue. Поскольку значение не меняется, решается и проблема дорогого наследования: дочерним потокам не надо копировать значение, которое остаётся постоянным в течение периода жизни.

Пример использования ScopedValue:

private static final ScopedValue<FrameworkContext> CONTEXT = ScopedValue.newInstance();

void serve(Request request, Response response) {
    var context = createContext(request);
    ScopedValue.where(CONTEXT, context)
               .run(() -> Application.handle(request, response));
}

public PersistedObject readKey(String key) {
    var context = CONTEXT.get();
    var db = getDBConnection(context);
    db.readKey(key);
}

В целом ScopedValue является предпочтительной заменой ThreadLocal, т.к. навязывает разработчику безопасную однонаправленную модель работы с неизменяемыми данными. Однако такой подход не всегда неприменим для некоторых задач, и для них ThreadLocal может быть единственно возможным решением.

Structured Concurrency (Preview) (JEP 453) [66]

Ещё одно API, которое ранее было в инкубационном статусе (Java 19 [67] и 20 [68]), а теперь стало Preview API – это Structured Concurrency.

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

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

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

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

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Supplier<String> user = scope.fork(() -> findUser());
    Supplier<Integer> order = scope.fork(() -> fetchOrder());

    scope.join()            // Join both forks
         .throwIfFailed();  // ... and propagate errors

    return new Response(user.get(), order.get());
}

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

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

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

Foreign Function & Memory API (Third Preview) (JEP 442) [76]

Foreign Function & Memory API, ставшее preview в Java 19 [77], продолжает находиться в этом статусе. API находится в пакете java.lang.foreign [78].

Напомним, что FFM API много лет разрабатывается в проекте Panama [79] с целью заменить JNI. В Java 22 [80] API выйдет из состояния preview.

Vector API (Sixth Incubator) (JEP 448) [81]

Векторное API в модуле jdk.incubator.vector [82], которое появилось ещё аж в Java 16 [83], остаётся в инкубационном статусе в шестой раз. В этом релизе лишь небольшие изменения API, исправления багов и улучшения производительности.

Векторное API останется в инкубаторе, пока необходимые фичи проекта Valhalla [84] не станут preview.

Key Encapsulation Mechanism API (JEP 452) [85]

В пакете javax.crypto появилось новое API, реализующее механизм инкапсуляции ключей [86].

Механизм инкапсуляции ключей (KEM) – это современная криптографическая техника, позволяющая обмениваться симметричными ключами, используя асимметричное шифрование. Если в традиционной технике симметричный ключ генерируется случайным образом и шифруется с помощью открытого ключа (что требует паддинга), то в KEM симметричный ключ выводится из самого открытого ключа.

В Java KEM API состоит из трёх главных классов.

KEM [87] – входная точка API. У него есть метод getInstance() [88], возвращающий объект KEM для указанного алгоритма.

Encapsulator [89] – представляет собой функцию инкапсуляции, которая вызывается отправителем. У этого класса есть метод encapsulate() [90], который принимает открытый ключ и возвращает секретный ключ, а также key encapsulation message (которое шлётся принимающей стороне).

Decapsulator [91] – функция декапсуляции, которая вызывается принимающей стороной. У класса есть метод decapsulate() [92], который принимает key encapsulation message и возвращает секретный ключ. Таким образом, у обеих сторон теперь есть одинаковый симметричный ключ, с помощью которого можно дальше обмениваться данными с помощью обычного симметричного шифрования.

Пример генерации симметричного ключа и его передачи:

// Receiver side
var kpg = KeyPairGenerator.getInstance("X25519");
var kp = kpg.generateKeyPair();

// Sender side
var kem1 = KEM.getInstance("DHKEM");
var sender = kem1.newEncapsulator(kp.getPublic());
var encapsulated = sender.encapsulate();
var k1 = encapsulated.key();

// Receiver side
var kem2 = KEM.getInstance("DHKEM");
var receiver = kem2.newDecapsulator(kp.getPrivate());
var k2 = receiver.decapsulate(encapsulated.encapsulation());

assert Arrays.equals(k1.getEncoded(), k2.getEncoded());

Для KEM также добавлен интерфейс KEMSpi [93], позволяющий предоставлять пользовательские реализации алгоритмов KEM.

JVM

Generational ZGC (JEP 439) [94]

В сборщик мусора ZGC, который появился в Java 15 [95], добавили поддержку поколений. Поколения в ZGC пока что отключены по умолчанию, и для их включения требуется ключ -XX:+ZGenerational:

java -XX:+UseZGC -XX:+ZGenerational ...

В будущих версиях Java режим работы с поколениями будет по умолчанию, и ключ -XX:+ZGenerational уже требоваться не будет.

Поколения в ZGC должны улучшить производительность Java-программ, т.к. молодые объекты, которые склонны умирать рано согласно слабой гипотезе о поколениях, будут собираться чаще, а старые объекты – более редко. При этом характеристики ZGC не должны от этого пострадать: время отклика по-прежнему должно быть сверхнизким (< 1ms) и кучи гигантских размеров (несколько терабайт) должны продолжать поддерживаться.

Напомним, что также ведётся работа [96] над поддержкой поколений в другом сборщике мусора Shenandoah [97], похожем по характеристикам на ZGC. Однако в Java 21 Generational Shenandoah попасть не успел.

Сборщиком мусора по умолчанию по-прежнему остаётся G1. Он стал дефолтным сборщиком мусора в Java 9 [98] (до него дефолтным был Parallel GC)

Prepare to Disallow the Dynamic Loading of Agents (JEP 451) [99]

При динамической загрузке агентов теперь выдаётся предупреждение:

WARNING: A {Java,JVM TI} agent has been loaded dynamically (file:/u/bob/agent.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release

Агент – это компонент, который может изменять (инструментировать) код Java-приложения во время работы. Поддержка агентов появилась в Java 5, чтобы была возможность писать продвинутые инструменты вроде профилировщиков, которым необходимо добавлять эмиссию событий в классы, или AOP-библиотек. Для включения агентов требовались опции командной строки -javaagent или -agentlib, поэтому все агенты тогда могли включаться только явно при старте приложения.

Однако в Java 6 появился Attach API [100], который, кроме всего прочего, позволил загружать агенты динамически прямо в работающий JVM. Благодаря этому библиотеки получили возможность подключаться к приложению и по-тихому изменять классы, не имея на то согласия от владельца приложения. Причём изменяться могут не только классы приложения, но и классы JDK. Таким образом, подвергается риску строгая инкапсуляция, которая является одним из краеугольных камней Java.

Чтобы закрыть такую потенциально опасную дыру, в Java 9 вместе с появлением модулей было предложено запретить динамическую загрузку агентов по умолчанию. Однако тогда было решено отложить на неопределённое время такое радикальное решение, чтобы дать авторам инструментов время подготовиться. В итоге, изменение дожило до наших дней, и было реализовано лишь в Java 21, но в виде предупреждения.

Чтобы подавить предупреждение, необходимо запускать JVM с опцией -XX:+EnableDynamicAgentLoading, либо загружать агенты при старте JVM, явно перечисляя их с помощью опций -javaagent или -agentlib.

В будущих версиях Java планируется полностью отключить динамическую загрузку по умолчанию, и она уже не будет работать без -XX:+EnableDynamicAgentLoading.

Deprecate the Windows 32-bit x86 Port for Removal (JEP 449) [101]

32-битный порт OpenJDK под Windows стал deprecated for removal. В будущем планируется избавиться от него полностью.

Удаление порта позволит ускорить разработку платформы. Также причиной стало отсутствие нативной реализации виртуальных потоков на 32-битной версии JDK 21 под Windows: виртуальные потоки в этой версии реализованы через платформенные потоки.

Полный список JEP'ов, попавших в JDK 21, начиная с JDK 17: ссылка [102].

Автор: Zheka Kozlov

Источник [103]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/oracle/387167

Ссылки в тексте:

[1] Java 21: https://openjdk.org/projects/jdk/21/

[2] 2500 закрытых задач и 15 JEP'ов: https://builds.shipilev.net/backports-monitor/release-notes-21.html

[3] здесь: http://jdk.java.net/21/release-notes

[4] здесь: https://javaalmanac.io/jdk/21/apidiff/20/

[5] как минимум 5 лет: https://www.oracle.com/java/technologies/java-se-support-roadmap.html

[6] Oracle JDK: https://www.oracle.com/java/technologies/downloads/

[7] NFTC: https://www.oracle.com/downloads/licenses/no-fee-license.html

[8] OpenJDK: http://jdk.java.net/21/

[9] GPLv2 with Classpath Exception: https://openjdk.org/legal/gplv2+ce.html

[10] Pattern Matching for switch (JEP 441): https://openjdk.org/jeps/441

[11] Java 17: https://habr.com/ru/articles/577924/

[12] preview: https://openjdk.org/jeps/12

[13] 17: https://openjdk.org/jeps/406

[14] 18: https://openjdk.org/jeps/420

[15] 19: https://openjdk.org/jeps/427

[16] 20: https://openjdk.org/jeps/433

[17] Record Patterns (JEP 440): https://openjdk.org/jeps/440

[18] Java 19: https://openjdk.org/jeps/405

[19] String Templates (Preview) (JEP 430): https://openjdk.org/jeps/430

[20] java.lang.StringTemplate: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/StringTemplate.html

[21] java.lang.StringTemplate.Processor: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/StringTemplate.Processor.html

[22] STR: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/StringTemplate.html#STR

[23] RAW: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/StringTemplate.html#RAW

[24] FMT: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/FormatProcessor.html#FMT

[25] java.util.Formatter: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Formatter.html

[26] process(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/StringTemplate.Processor.html#process(java.lang.StringTemplate)

[27] Unnamed Patterns and Variables (Preview) (JEP 443): https://openjdk.org/jeps/443

[28] Unnamed Classes and Instance Main Methods (Preview) (JEP 445): https://openjdk.org/jeps/445

[29] синтетическим: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Class.html#isSynthetic()

[30] simple name: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Class.html#getSimpleName()

[31] имя: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Class.html#getName()

[32] Virtual Threads (JEP 444): https://openjdk.org/jeps/444

[33] Loom: https://openjdk.org/projects/loom/

[34] Java 19: https://openjdk.org/jeps/425

[35] java.lang.Thread: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html

[36] interrupt(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#interrupt()

[37] ThreadLocal: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ThreadLocal.html

[38] ForkJoinPool: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ForkJoinPool.html

[39] демоном: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#setDaemon(boolean)

[40] Thread.Builder: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.Builder.html

[41] Thread.startVirtualThread(Runnable): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#startVirtualThread(java.lang.Runnable)

[42] Thread.isVirtual(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#isVirtual()

[43] Executors.newVirtualThreadPerTaskExecutor(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Executors.html#newVirtualThreadPerTaskExecutor()

[44] Sequenced Collections (JEP 431): https://openjdk.org/jeps/431

[45] SequencedCollection: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SequencedCollection.html

[46] SequencedSet: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SequencedSet.html

[47] SequencedMap: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SequencedMap.html

[48] Collection: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Collection.html

[49] LinkedHashSet: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/LinkedHashSet.html

[50] List: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/List.html

[51] SortedSet: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SortedSet.html

[52] Deque: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Deque.html

[53] getLast(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SequencedCollection.html#getLast()

[54] reversed(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SequencedCollection.html#reversed()

[55] LinkedHashMap: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/LinkedHashMap.html

[56] SortedMap: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SortedMap.html

[57] Scoped Values (Preview) (JEP 446): https://openjdk.org/jeps/446

[58] Java 20: https://openjdk.org/jeps/429

[59] инкубационном статусе: https://openjdk.org/jeps/11

[60] ScopedValue: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ScopedValue.html

[61] set(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ThreadLocal.html#set(T)

[62] get(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ThreadLocal.html#get()

[63] where(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ScopedValue.html#where(java.lang.ScopedValue,T)

[64] run(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ScopedValue.Carrier.html#run(java.lang.Runnable)

[65] get(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ScopedValue.html#get()

[66] Structured Concurrency (Preview) (JEP 453): https://openjdk.org/jeps/453

[67] 19: https://openjdk.org/jeps/428

[68] 20: https://openjdk.org/jeps/437

[69] StructuredTaskScope: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html

[70] fork(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html#fork(java.util.concurrent.Callable)

[71] join(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html#join()

[72] остановлен: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html#shutdown()

[73] ExecutorService: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ExecutorService.html

[74] submit(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ExecutorService.html#submit(java.lang.Runnable)

[75] close(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html#close()

[76] Foreign Function & Memory API (Third Preview) (JEP 442): https://openjdk.org/jeps/442

[77] в Java 19: https://openjdk.org/jeps/424

[78] java.lang.foreign: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/foreign/package-summary.html

[79] Panama: https://openjdk.org/projects/panama/

[80] Java 22: https://openjdk.org/jeps/454

[81] Vector API (Sixth Incubator) (JEP 448): https://openjdk.org/jeps/448

[82] jdk.incubator.vector: https://docs.oracle.com/en/java/javase/21/docs/api/jdk.incubator.vector/module-summary.html

[83] в Java 16: https://openjdk.org/jeps/338

[84] Valhalla: https://openjdk.org/projects/valhalla/

[85] Key Encapsulation Mechanism API (JEP 452): https://openjdk.org/jeps/452

[86] механизм инкапсуляции ключей: https://en.wikipedia.org/wiki/Key_encapsulation_mechanism

[87] KEM: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/crypto/KEM.html

[88] getInstance(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/crypto/KEM.html#getInstance(java.lang.String)

[89] Encapsulator: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/crypto/KEM.Encapsulator.html

[90] encapsulate(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/crypto/KEM.Encapsulator.html#encapsulate()

[91] Decapsulator: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/crypto/KEM.Decapsulator.html

[92] decapsulate(): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/crypto/KEM.Decapsulator.html#decapsulate(byte%5B%5D)

[93] KEMSpi: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/javax/crypto/KEMSpi.html

[94] Generational ZGC (JEP 439): https://openjdk.org/jeps/439

[95] Java 15: https://openjdk.org/jeps/377

[96] ведётся работа: https://openjdk.org/jeps/404

[97] Shenandoah: https://openjdk.org/jeps/379

[98] Java 9: https://openjdk.org/jeps/248

[99] Prepare to Disallow the Dynamic Loading of Agents (JEP 451): https://openjdk.org/jeps/451

[100] Attach API: https://docs.oracle.com/en/java/javase/21/docs/api/jdk.attach/com/sun/tools/attach/package-summary.html

[101] Deprecate the Windows 32-bit x86 Port for Removal (JEP 449): https://openjdk.org/jeps/449

[102] ссылка: https://openjdk.org/projects/jdk/21/jeps-since-jdk-17

[103] Источник: https://habr.com/ru/articles/762084/?utm_source=habrahabr&utm_medium=rss&utm_campaign=762084