Вышла общедоступная версия Java 22. В этот релиз попало около 2300 закрытых задач и 12 JEP'ов. Release Notes можно посмотреть здесь. Полный список изменений API – здесь.


Java 22 не является LTS-релизом, и у неё будут выходить обновления только полгода (до сентября 2024 года).



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




Рассмотрим все JEP'ы, которые попали в Java 22.



Unnamed Variables & Patterns (JEP 456)


Безымянные переменные и паттерны, которые появились в режиме preview в Java 21, теперь стали постоянной языковой конструкцией.



Безымянная переменная – это переменная, которая обозначена автором как неиспользуемая и обозначаемая символом подчёркивания (_).



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


static int count(Iterable<Order> orders) {
    int total = 0;
    for (Order _ : orders) // order is unused
        total++;
    return total;
}

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


Queue<Integer> q = ... // x1, y1, z1, x2, y2, z2, ...
while (q.size() >= 3) {
   var x = q.remove();
   var y = q.remove();
   var _ = q.remove();
   ... new Point(x, y) ...
}

Здесь были необходимы только координаты x и y, поэтому для третьей координаты была явно выбрана безымянная переменная, чтобы явно продемонстрировать, что она не используется.



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


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

Здесь важен сам факт наличия исключения, но не само исключение.



try с ресурсом:


try (var _ = ScopedContext.acquire()) {
    ... no use of acquired resource ...
}


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


...stream.collect(Collectors.toMap(String::toUpperCase, _ -> "NODATA"))


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



Безымянными могут быть не только переменные, но и паттерны:


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


Аналогичным образом можно извлечь только цвет, если нужен только он, но не нужны координаты:


if (r instanceof ColoredPoint(_, Color c)) {
    ... c ...
}


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


switch (ball) {
    case RedBall _   -> process(ball);
    case BlueBall _  -> process(ball);
    case GreenBall _ -> stopProcessing();
}


Код выше можно сократить, объединив две первые ветки case в одну:


switch (ball) {
    case RedBall _, BlueBall _  -> process(ball);
    case GreenBall _            -> stopProcessing();
}


Заметим, что такое объединение было бы невозможным без использования безымянных паттернов, так как раньше несколько паттернов в одной ветке разрешались только при отсутствии в них переменных паттернов.



Более сложный пример со вложенными паттернами, где есть и безымянные паттерны, и безымянные переменные паттернов:


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


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



Launch Multi-File Source-Code Programs (JEP 458)


Теперь лаунчер java может запускать программы, состоящие из нескольких исходных файлов Java.


Напомним, что ранее в Java 11 появилась возможность запускать программы, состоящие из одного файла, без необходимости самостоятельной компиляции (JEP 330):


// Prog.java

class Prog {
    public static void main(String[] args) { Helper.run(); }
}

class Helper {
    static void run() { System.out.println("Hello!"); }
}

Такой файл можно было запустить, просто написав:


$ java Prog.java
Hello!


А сейчас эта возможность была расширена до произвольного количества файлов:


// Prog.java
class Prog {
    public static void main(String[] args) { Helper.run(); }
}

// Helper.java
class Helper {
    static void run() { System.out.println("Hello!"); }
}

Если программу выше запустить через java Prog.java, то Java скомпилирует в память класс Prog и запустит его метод main. Так как класс Prog ссылается на класс Helper, то Java найдёт его в файле Helper.java и тоже скомпилирует его. Таким образом, программа, разбитая на два файла будет работать точно так же, как если бы все классы были помещены в один исходный файл. Этот алгоритм может быть расширен до произвольного количества файлов. Например, если Helper ссылается ещё на один класс HelperAux, то будет найден и скомпилирован файл HelperAux.java.



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



Программы, использующие библиотеки в виде уже скомпилированных jar-файлов, также могут быть запущены напрямую. Например, если есть структура:


  • Prog.java
  • Helper.java
  • libs/library.jar

То такую программу можно запустить с помощью опции --class-path (или -cp):


$ java --class-path "libs/*" Prog.java


String Templates (Second Preview) (JEP 459)


Строковые шаблоны, которые появились в режиме preview в Java 21, уходят на второй раунд preview без изменений.


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


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

То есть это строковая интерполяция, которая уже давно есть во многих других известных языках программирования.



Реализация строковых шаблонов в Java отличается от большинства реализаций в других языках: в Java строковый шаблон на самом деле сначала превращается в объект java.lang.StringTemplate, а затем процессор, реализующий java.lang.StringTemplate.Processor, конвертирует этот объект в строку или объект другого класса (примечание: сейчас идут обсуждения относительно отказа идеи процессоров и оставления только StringTemplate). В примере выше 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}";


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



Statements before super(...) (Preview) (JEP 447)


В режиме preview теперь стало возможным писать инструкции кода в конструкторе перед явным вызовом конструктора (super() или this()):


// --enable-preview --release 22
public class PositiveBigInteger extends BigInteger {
    public PositiveBigInteger(long value) {
        if (value <= 0)
            throw new IllegalArgumentException("non-positive value");
        super(value);
    }
}


Напомним, что с самого первого релиза Java 1.0 это было запрещено, поэтому в случаях, когда необходимо выполнить код перед вызовом конструктора, приходилось использовать обходные пути, например, прибегать к вспомогательным статическим методам:


public class PositiveBigInteger extends BigInteger {
    public PositiveBigInteger(long value) {
        super(verifyPositive(value));
    }

    private static long verifyPositive(long value) {
        if (value <= 0)
            throw new IllegalArgumentException("non-positive value");
    }
}


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


public class Sub extends Super {
    private Sub(int i, F f) { // Auxiliary constructor
        super(f, f); // f is shared here
        ...
    }

    public Sub(int i) {
        this(i, new F());
    }
}


В Java 22, включив режим preview, то же самое можно реализовать гораздо короче:


// --enable-preview --release 22
public class Sub extends Super {
    public Sub(int i) {
        var f = new F();
        super(f, f); // f is shared here
        ...
    }
}


Не всякий код можно поместить перед вызовом конструктора: код в прологе не должен ссылаться на конструируемый объект. Это обеспечивает гарантию того, что инициализация всегда происходит сверху-вниз: инициализация полей суперкласса должна всегда выполняться раньше инициализации полей подкласса (возможно такое ограничение смягчат в Java 23 в следующем preview). Рассмотрим несколько примеров некорректного кода:


class A {
    int i;

    A() {
        this.i++;               // Error
        hashCode();             // Error
        System.out.print(this); // Error
        super();
    }
}


Ссылаться на поля суперкласса также нельзя (ведь это тоже часть текущего объекта):


class D {
    int i;
}

class A extends D {
    int i;

    A() {
        i++; // Error
        super();
    }
}


Также запрещены ситуации, когда есть неявная ссылка на объект, например, через экземпляр внутреннего класса:


class Outer {
    class Inner {
    }

    Outer() {
        new Inner(); // Error - 'this' is enclosing instance
        super();
    }
}


Интересно, что новая возможность затрагивает исключительно компилятор Java – JVM уже и так давно поддерживает байткод, в котором присутствуют инструкции перед вызовом super() или this(), если эти инструкции не трогают конструируемый объект (JVM даже ещё более либеральна, например, она разрешает несколько вызовов конструкторов, если любой путь обязательно завершается одним вызовом конструктора).



Implicitly Declared Classes and Instance Main Methods (Second Preview) (JEP 463)


В Java 21 в режиме preview появились Unnamed Classes and Instance Main Methods. В Java 22 было принято решение оставить эту фичу на второе preview с некоторыми изменениями. Основное из них – это отказ от безымянных классов в пользу неявно объявленных классов. Также упрощена процедура выбора main-метода для запуска: если есть метод main с String[] args, то запускается он (неважно, static или нет), иначе запускается метод main без аргументов.


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


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

В таком случае во время запуска JVM сама создаст экземпляр класса HelloWorld и вызовет у него метод main():


$ java --enable-preview --source 22 HelloWorld.java
Hello, World!


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


// HelloWorld.java

String greeting = "Hello, World!";

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

$ java --enable-preview --source 22 HelloWorld.java
Hello, World!


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


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

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

Неявный класс обладает практически всеми возможностями явного класса (возможность содержать методы, поля), но есть несколько отличий:


  • Код в неявном классе не может ссылаться на него по имени.
  • Неявный класс всегда имеет один неявный конструктор без аргументов.
  • Неявный класс может находиться только в безымянном пакете.

При этом неявный класс не является безымянным: у него есть имя, совпадающее с именем файла (но это является деталью реализации, на которую не стоит полагаться).



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


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


Stream Gatherers (Preview) (JEP 461)


Stream API был усовершенствован, чтобы поддерживать произвольные промежуточные операции, в режиме preview.


Напомним, что стримы с появления в Java 8 имели фиксированный набор промежуточных операций (map, flatMap, filter, reduce, limit,
skip и т.д). В Java 9 были добавлены takeWhile и dropWhile. Хотя этот стандартный набор операций довольно богатый и покрывает большинство случаев, иногда бывают необходимы более изощрённые промежуточные операции для более сложных задач. Чтобы решить эту проблему, было предложено создать точку расширения для стримов, которая позволит кому угодно создать свои промежуточные операции.


Новая точка расширения – это новый метод Stream::gather(Gatherer), который обрабатывает элементы стрима путём применения объекта, реализующего интерфейс Gatherer, предоставляемого пользователем. Операция gather() аналогична уже имеющейся операции Stream::collect(Collector): если collect() и Collector определяют точку расширения для терминальных операций, то gather() и Gatherer определяют точкой расширения для промежуточных.


Gatherer представляет собой трансформацию элементов стрима. Манера трансформации может быть совершенно произвольной: one-to-one, one-to-many, many-to-one или many-to-many. Поддерживается короткое замыкание, если надо в какой-то момент остановить обработку и отбросить все дальнейшие элементы. Бесконечные стримы могут преобразовываться в конечные, и наоборот, конечные могут преобразовываться в бесконечные. Поддерживается параллельное исполнение. Всё это возможно благодаря максимально обобщённой форме интерфейса Gatherer.


gather() также является промежуточной операцией, поэтому может быть несколько gather() в одной цепочке:


source.gather(a).gather(b).gather(c).collect(...)


Вместе с самим Gatherer было добавлено несколько готовых gatherer'ов, определённых в новом классе Gatherers. Это fold, mapConcurrent, scan, windowFixed и
windowSliding.


Давайте рассмотрим несколько примеров:


jshell> Stream.of(1,2,3,4,5,6,7,8,9)
   ...>       .gather(Gatherers.fold(() -> "", (str, n) -> str + n))
   ...>       .findFirst()
   ...>       .get();
$1 ==> "123456789"

jshell> Stream.of(1,2,3,4,5,6,7,8,9)
   ...>       .gather(Gatherers.scan(() -> "", (str, n) -> str + n))
   ...>       .toList()
$2 ==> [1, 12, 123, 1234, 12345, 123456, 1234567, 12345678, 123456789]

jshell> Stream.of(1,2,3,4,5,6,7,8).gather(Gatherers.windowFixed(3)).toList()
$3 ==> [[1, 2, 3], [4, 5, 6], [7, 8]]

jshell> Stream.of(1,2,3,4,5,6).gather(Gatherers.windowSliding(3)).toList()
$4 ==> [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6]]


Дизайн интерфейса Gatherer был создан под влиянием интерфейса Collector. Вот основная часть его сигнатуры:


public interface Gatherer<T, A, R> {
    Supplier<A> initializer();
    Integrator<A, T, R> integrator();
    BinaryOperator<A> combiner();
    BiConsumer<A, Downstream<? super R>> finisher();
}

Если взглянуть на Collector, то он также имеет три параметра T, A, R и содержит 4 основных метода: supplier, accumulator, combiner и finisher. Однако Gatherer использует два вспомогательных интерфейса Integrator и
Downstream, так как поддержка произвольных промежуточных операций требует немного более сложного устройства, чем терминальных.


Для написания собственных gatherer'ов, как правило, не приходится с нуля реализовывать интерфейс Gatherer и можно воспользоваться готовыми методами-фабриками: Gatherer::of(Integrator), Gatherer::ofSequential(Integrator) или другими вариациями.



Class-File API (Preview) (JEP 457)


В режиме preview появилось стандартное API для парсинга, генерации и трансформации class-файлов.


Новое API находится в пакете java.lang.classfile. Оно должно заменить копию библиотеки ASM внутри JDK, которую планируется удалить, как только все компоненты JDK перейдут с неё на новое API.


Основная проблема ASM (и других библиотек для работы с class-файлами) – это то, что она не успевает за ускорившимся в последнее время темпом выхода релизов JDK (два раза в год), а соответственно, и за изменениями в формате class-файлов. Кроме того, ASM – это сторонняя библиотека, а значит её поддержка возможностей class-файлов всегда отстаёт от JDK, что создаёт проблемы как в экосистеме, так и в самой JDK. Стандартное API же эволюционирует одновременно с форматом class-файлов. Как только выходит новая версия Java, фреймворки и инструменты, использующие API, немедленно и автоматически получают поддержку нового формата.


Новое API также спроектировано с учётом новых возможностей Java, таких, как лямбды, записи, sealed-классы и паттерн-матчинг. ASM же – очень старая библиотека, основанная на визиторах, что совершенно неуместно в 2024 году.



Structured Concurrency (Second Preview) (JEP 462)


Structured Concurrency, которое перешло в режиме preview в Java 21, уходит на второй раунд preview без изменений. Ранее оно было в инкубаторе в Java 19 и Java 20



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 должно облегчить написание безопасных многопоточных программ благодаря знакомому структурному подходу.



Scoped Values (Second Preview) (JEP 464)


Scoped Values, которые стали preview в Java 21, как и Structured Concurrency, уходят на второе preview без изменений. До этого Scoped Values были в инкубаторе в Java 20.


Новый класс 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 может быть единственно возможным решением.



Foreign Function & Memory API (JEP 454)


Foreign Function & Memory API, которое было долго в режиме preview (а до этого ещё дольше в инкубаторе), наконец-то стабилизировалось.


Главной задачей FFM API является замена устаревшего JNI, который является опасным и хрупким средством вызова нативных библиотек и обработки нативных данных. FFM API, напротив, создан как безопасное, удобное, читаемое и эффективное средство интеропа со средой вне Java.


FFM API находится в пакете java.lang.foreign. Оно состоит из двух частей: API для доступа к внешней памяти (foreign memory) и API для вызова внешних функций (foreign functions).


API для доступа к внешней памяти предоставляет классы и интерфейсы, которые позволяют выделять, освобождать внешнюю память и манипулировать ею: MemorySegment, Arena, SegmentAllocator, MemoryLayout. Также оно использует уже существующий класс VarHandle. API для вызова внешних функций предоставляет классы и интерфейсы
Linker, SymbolLookup, FunctionDescriptor. Для непосредственно вызовов используется привычный MethodHandle.


Вот небольшой пример использования FFM API, в котором код на Java получает MethodHandle для функции radixsort, написанной на C, и вызывает её для сортировки массива из 4 строк:



// 1. Find foreign function on the C library path
Linker linker          = Linker.nativeLinker();
SymbolLookup stdlib    = linker.defaultLookup();
MethodHandle radixsort = linker.downcallHandle(stdlib.find("radixsort"), ...);
// 2. Allocate on-heap memory to store four strings
String[] javaStrings = { "mouse", "cat", "dog", "car" };

// 3. Use try-with-resources to manage the lifetime of off-heap memory
try (Arena offHeap = Arena.ofConfined()) {
    // 4. Allocate a region of off-heap memory to store four pointers
    MemorySegment pointers
        = offHeap.allocate(ValueLayout.ADDRESS, javaStrings.length);
    // 5. Copy the strings from on-heap to off-heap
    for (int i = 0; i < javaStrings.length; i++) {
        MemorySegment cString = offHeap.allocateFrom(javaStrings[i]);
        pointers.setAtIndex(ValueLayout.ADDRESS, i, cString);
    }
    // 6. Sort the off-heap data by calling the foreign function
    radixsort.invoke(pointers, javaStrings.length, MemorySegment.NULL, '\0');
    // 7. Copy the (reordered) strings from off-heap to on-heap
    for (int i = 0; i < javaStrings.length; i++) {
        MemorySegment cString = pointers.getAtIndex(ValueLayout.ADDRESS, i);
        javaStrings[i] = cString.reinterpret(...).getString(0);
    }
} // 8. All off-heap memory is deallocated here

assert Arrays.equals(javaStrings,
                     new String[] {"car", "cat", "dog", "mouse"});  // true

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



Большая часть FFM API является безопасной по умолчанию. Многие задачи, для которых ранее необходимо было писать нативный код, вызываемый через JNI, теперь решаются написанием только Java-кода. Однако у FFM API есть ограниченные методы (например, MemorySegment::reinterpret), которые по своей сути являются небезопасными. При их использовании могут возникнуть ужасные последствия вроде краха JVM, которые виртуальная машина не в состоянии предотвратить. Поэтому при выполнении ограниченного метода JVM выдаёт предупреждение, например:


WARNING: A restricted method in java.lang.foreign.Linker has been called
WARNING: Linker::downcallHandle has been called by com.foo.Server in an unnamed module
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

Чтобы разрешить модулю использовать ограниченные методы без предупреждений, необходимо использовать опцию командной строки --enable-native-access=M, где M – имя модуля или список модулей через запятую (можно использовать ALL-UNNAMED для всего кода в classpath). При этом любое использование ограниченных методов вне списка модулей будет выбрасывать IllegalCallerException.



Vector API (Seventh Incubator) (JEP 460)


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


Векторное API остаётся так долго в инкубаторе, потому что зависит от некоторых фич проекта Valhalla (главным образом, от value-классов), который пока что находится в разработке. Как только эти фичи станут доступны в виде preview, векторное API тоже сразу же выйдет из инкубатора в статус preview.



Region Pinning for G1 (JEP 423)


В сборщике мусора G1 было реализовано закрепление регионов, которое предотвращает отключение сборки мусора, пока JNI находится в критическом регионе.


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


До Java 22 G1 имел простейшую стратегию: если хотя бы один из потоков находился в критическом регионе, то он просто отключал сборку мусора. Это могло приводить к различным проблемам, начиная с длительных пауз и заканчивая нехваткой памяти при её фактическом избытке.


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

Комментарии (19)


  1. snuk182
    19.03.2024 21:40

    Главным образом, это может пригодиться для быстрого прототипирования или на ранних стадиях проектов, когда проект ещё не обрёл более-менее стабильную форму.

    Работает? В прод! ©®™


  1. Trink
    19.03.2024 21:40
    +1

    с переменными такая чехарда начнется... Некоторые до сих пор var не могут переварить


  1. Myxach
    19.03.2024 21:40
    +1

    Что-то ощущение, что новость не из 2024, а из 2014-2015...типо, щас же даже в C++ есть большенство этих вещей


  1. Shatun
    19.03.2024 21:40
    +1

    String Templates (Second Preview) (JEP 459)

    Их скорее всего очень сильно переделают в следующем релизе, и реализацию, и синтаксис. Пока выглядит что синтаксис будет $"Bla-bla {var}"(вместо STR."Bla-bla {var}"), но это не финальное решение.
    Одно из обощающих писем для тех кому интересны детали:
    https://mail.openjdk.org/pipermail/amber-spec-experts/2024-March/004062.html

    Implicitly Declared Classes and Instance Main Methods (Second Preview) (JEP 463)

    Мне ненравится как сделали этот JEP. Есть проблема-многословный синтаксис, лишние абстракции которые как выяснилось не всегда нужны. Что мы сделаем, исправим это в языке? Нет, введем дополнительный граничный случай для начинающих с неявно объявленными классами, которые как потом выснится больше нигде работать небудут.


    1. ris58h
      19.03.2024 21:40
      +1

      Одно из обощающих писем для тех кому интересны детали

      Автор упомянул об этом:

      примечание: сейчас идут обсуждения относительно отказа идеи процессоров и оставления только StringTemplate


      1. ris58h
        19.03.2024 21:40

        Слегка ошибся - по ссылке в статье другое письмо, но по той же теме.


        1. Shatun
          19.03.2024 21:40

          Автор упомянул об этом

          Незаvетил примечания

          Слегка ошибся - по ссылке в статье другое письмо, но по той же теме.

          Да, там несколько обощающих писем, и прямо сейчас идет активное обсуждение, не удивлюсь если уже опять все поменялось


  1. DenSigma
    19.03.2024 21:40

    Не нравятся мне Scoped Values. По-сути это глобальные переменные. Применяться они будут только как инструмент для костылестроения, когда неохота добавлять параметры и грамотно рефакторить код при разросшемся количестве параметров. Когда надо пробросить параметр через пять вложенных вызовов и несколько классов параметров.


    1. Shatun
      19.03.2024 21:40

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


    1. pkokoshnikov
      19.03.2024 21:40
      +1

      А как mdc делать например? Таскать все параметры и каждый раз в логер передовать?


  1. domix32
    19.03.2024 21:40

    windowSliding

    интересно почему не человекочитаемое slidingWindow или просто window.


    1. Shatun
      19.03.2024 21:40

      Думаю потому что есть windowFixed


      1. domix32
        19.03.2024 21:40

        А это как? Типа с фиксированным шагом делать? или типа чанками обрабатывать?


        1. Shatun
          19.03.2024 21:40

          В статье есть пример

          jshell> Stream.of(1,2,3,4,5,6).gather(Gatherers.windowSliding(3)).toList()
          $4 ==> [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6]]

          Хотя насколько часто такое надо я не знаю.


          1. domix32
            19.03.2024 21:40

            Так про этот пример и возник вопрос - оно использует скользящее окно. Вы заявили, что есть ещё некий windowFixed, но не пояснили, что тот должен делать. Функции, которые используют последовательные окна вместо скользящих обычно зовутся chunk или bulk и их вариации.


            1. Shatun
              19.03.2024 21:40
              +1

              windowFixed

              jshell> Stream.of(1,2,3,4,5,6,7,8).gather(Gatherers.windowFixed(3)).toList()
              $3 ==> [[1, 2, 3], [4, 5, 6], [7, 8]]


              1. domix32
                19.03.2024 21:40

                А есть API чтобы он то же самое делал, но без хвостов?


                1. Shatun
                  19.03.2024 21:40

                  Нет. А что вы бы хотели сделать, отобросить последний массив [7,8]?


  1. stepagrus
    19.03.2024 21:40
    +2

    Java как язык догоняет C#. A .NET как рантайм пытается догнать Java.