Pull to refresh

Вышла Java 21

Level of difficultyMedium
Reading time18 min
Views62K
Вышла общедоступная версия Java 21. В этот релиз попало около 2500 закрытых задач и 15 JEP'ов. Release Notes можно посмотреть здесь. Изменения API – здесь.

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

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


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


Язык


Pattern Matching for switch (JEP 441)


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


Новый паттерн-матчинг существенно расширяет возможности оператора 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)


Отдельным видом паттернов являются паттерны записей. Они появились в Java 19 в режиме 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)


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


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

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


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


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

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


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


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


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

Процессоры необязательно должны возвращать String. Вот общая сигнатура метода process() интерфейса 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)


Ещё одно новшество в режиме 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)


Теперь в режиме 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);
}
// }

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


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
}

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

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

API


Virtual Threads (JEP 444)


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


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


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


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


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


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

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



Sequenced Collections (JEP 431)


Появились три новых интерфейса 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 к общему знаменателю:


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

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


Особый интерес представляет метод reversed(), который возвращает 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 и наследники SortedMap.



Scoped Values (Preview) (JEP 446)


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


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


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


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

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


ScopedValue лишён вышеперечисленных недостатков. В отличие от ThreadLocal, ScopedValue не имеет метода set(). Значение ассоциируется с объектом ScopedValue путём вызова другого метода where(). Далее вызывается метод run(), на протяжении которого это значение можно получить (через метод get()), но нельзя изменить. Как только исполнение метода 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)


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


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


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


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

Пример использования 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 и submit(), но у StructuredTaskScope есть несколько принципиальных отличий, которые делают код безопаснее:


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

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



Foreign Function & Memory API (Third Preview) (JEP 442)


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


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



Vector API (Sixth Incubator) (JEP 448)


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


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



Key Encapsulation Mechanism API (JEP 452)


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


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


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


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


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


Decapsulator – функция декапсуляции, которая вызывается принимающей стороной. У класса есть метод decapsulate(), который принимает 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, позволяющий предоставлять пользовательские реализации алгоритмов KEM.



JVM


Generational ZGC (JEP 439)


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


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

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


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


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


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



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


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


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, который, кроме всего прочего, позволил загружать агенты динамически прямо в работающий 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)


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

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

Полный список JEP'ов, попавших в JDK 21, начиная с JDK 17: ссылка.
Tags:
Hubs:
Total votes 74: ↑74 and ↓0+74
Comments49

Articles