Вышла Java 23

от автора

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

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

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

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

Markdown Documentation Comments (JEP 467)

Теперь JavaDoc поддерживает формат Markdown. Для его использования документация должна начинаться с ///:

/// Returns `true` if, and only if, [#length()] is `0`. /// /// @return `true` if [#length()] is `0`, otherwise `false` public boolean isEmpty() {     // ... }

Markdown компактнее, читабельнее и удобнее для написания, чем существующий формат HTML. Рассмотрим несколько примеров элементов, написанных в формате HTML и Markdown:

HTML Markdown
{@link java.util.List} [java.util.List]
{@code true} `true`
<em>warning</em> _warning_
<b>error</b> **error**
<ul>   <li>Item 1</li>   <li>Item 2</li>   <li>Item 3</li> </ul>
- Item 1 - Item 2 - Item 3
<p> Не нужен (просто необходимо вставить новую строку)

При этом JavaDoc-теги, такие как {@inheritDoc}, @param, @return, @throws, остаются прежними:

/// {@inheritDoc} /// In addition, this method calls [#wait()]. /// /// @param i the index public void m(int i) {     // ... }

Кроме компактности и удобства новый формат также решает проблему сочетания двух символов */, которые в формате HTML означают окончание документации:

/**  * <pre>  * var pattern = Pattern.compile("\\w*/"); // Проблема  * </pre>  */

В Markdown-документации же можно не только использовать эту последовательность символов, но и вставлять целые блоки HTML-комментариев:

/// Here is an example: /// /// ``` /// /** Hello World! */ /// public class HelloWorld { ///     public static void main(String... args) { ///         System.out.println("Hello World!"); // the traditional example ///     } /// } /// ```

Primitive Types in Patterns, instanceof, and switch (Preview) (JEP 455)

Теперь в режиме preview паттерны и операторы instanceof / switch поддерживают примитивные типы:

// --enable-preview --source 23  Object obj = 42; if (obj instanceof int i) { // matches     System.out.println("int: " + i); }  switch (obj) {     case int i -> System.out.println("int: " + i); // matches     case double d -> System.out.println("double: " + d);     default -> System.out.println("other"); }

Проверять можно также и то, попадают ли значения в диапазон типа:

int i = 42; if (i instanceof byte b) { // matches     System.out.println("byte: " + b); }  double d = 3.0; switch (d) {     case int i -> System.out.println("int: " + i); // matches     case float f -> System.out.println("float: " + f);     default -> System.out.println("other"); }

В примерах выше 42 попадает в диапазон byte ([-128; 127]), а 3.0 без потери точности приводится к int. Таким образом, это позволит более безопасно приводить одни числовые типы к другим, не прибегая к ручным проверкам диапазонов.

Подобные проверки могут быть полезны и в паттернах записей:

record JsonNumber(double d) {}  var json = new JsonNumber(3.0); if (json instanceof JsonNumber(int i)) { // matches     // ... }

Если раньше типы выражений-селекторов в switch могли быть только int, short, byte и char и для них поддерживались только константные ветки (case 3 и т.п.), то сейчас поддерживаются все примитивные типы и ветки могут быть паттернами:

float f = 1.0f; switch (f) {     case 0f -> System.out.println("0");     case float x when x == 1f -> System.out.println("1"); // matches     case float x -> System.out.println("other"); }  boolean b = "hello".isEmpty(); switch (b) {     case true -> System.out.println("empty");     case false -> System.out.println("non-empty"); // matches }

Module Import Declarations (Preview) (JEP 476)

В режиме preview появилась возможность импортировать модули:

import module M;

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

Например, импорт модуля java.base имеет тот же эффект, как если бы мы вручную импортировались все его 54 экспортированных пакета:

import java.io.*; import java.lang.*; import java.lang.annotation.*; // ... 49 packages ... import javax.security.auth.x500.*; import javax.security.cert.*;

Таким образом, написав всего лишь один импорт, можно будет получить доступ до таких неотъемлемых классов и интерфейсов как List, Map, Stream, Path, Function и др. без необходимости отдельного импорта их соответствующих пакетов.

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

При использовании неявно объявленных классов модуль java.base импортируется автоматически. Об этом следующий JEP 477.

Implicitly Declared Classes and Instance Main Methods (Third Preview) (JEP 477)

В Java 21 в режиме preview появились безымянные классы и инстанс-методы main(). В Java 22 они были оставлены на второе preview с несколькими изменениями, среди которых самым важным был отказ от безымянных классов в пользу неявно объявленных классов.

В Java 23 теперь выходит третье preview этой фичи ещё с несколькими изменениями:

  • Появился новый класс java.io.IO с тремя публичными статическими методами, которые автоматически импортируются во все неявно объявленные классы:
  • Неявно объявленные классы автоматически импортируют модуль java.base (см. JEP 476 выше). То есть автоматически будут видны все публичные верхнеуровневые классы и интерфейсы всех экспортированных пакетов модуля java.base.

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

// --enable-preview --source 23 class HelloWorld {     void main() {         System.out.println("Hello, World!");     } }

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

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

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

// HelloWorld.java  String greeting = "Hello, World!";  void main() {     println(greeting); }
$ java --enable-preview --source 23 HelloWorld.java Hello, World!

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

// import module java.base; ← неявно // import static java.io.IO.*; ← неявно  // class <some name> { ← неявно String greeting = "Hello, World!";  void main() {     println(greeting); } // }

Заметьте, что второй пример стал короче не только из-за отсутствия объявления класса, но и из-за использования метода println() вместо System.out.println().

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

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

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

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

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

Flexible Constructor Bodies (Second Preview) (JEP 482)

Statements before super(), которые появились в Java 22 в режиме preview, остаются на второе preview и теперь называются Flexible Constructor Bodies. По сравнению с Java 22 есть одно важное изменение: теперь можно инициализировать поля до вызова конструктора. Про это будет подробнее рассказано дальше.

Flexible Constructor Bodies разрешают писать инструкции кода в конструкторе перед явным вызовом конструктора (super() или this()):

// --enable-preview --source 23 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 Super {     public Super(C x, C y) { ... } }  public class Sub extends Super {     private Sub(C x) { // Auxiliary constructor         super(x, x); // x is shared here     }      public Sub(int i) {         this(new C(i));     } }

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

// --enable-preview --source 23 public class Sub extends Super {     public Sub(int i) {         var x = new C(i);         super(x, x);     } }

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

class A {     int i;      A() {         System.out.print(this); // Error         var x = i;              // Error         hashCode();             // Error         super();     } }

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

class B {     int i;     void m() {} }  class C extends B {     C() {         var x = i; // Error         m();       // Error         super();     } }

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

class Outer {     class Inner {     }      Outer() {         new Inner(); // Error         super();     } }

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

class A {     int i;      A(int i) {         this.i = i; // OK         super();     } }

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

class Super {     Super() { overriddenMethod(); }      void overriddenMethod() {         System.out.println("hello");     } }  class Sub extends Super {     final int x;      Sub(int x) {         this.x = x;     }      @Override     void overriddenMethod() {         System.out.println(x); // new Sub(42) will print 0     } }

Чтобы предотвратить такую ситуацию, нужно поместить инициализацию поле выше вызова super():

class Super {     Super() { overriddenMethod(); }      void overriddenMethod() {         System.out.println("hello");     } }  class Sub extends Super {     final int x;      Sub(int x) {         this.x = x;         super();     }      @Override     void overriddenMethod() {         System.out.println(x); // new Sub(42) will print 42     } }

Также инициализация полей до super() можно пригодиться в проекте Valhalla для definite assignment полей null-restricted value-классов.

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

Stream Gatherers (Second Preview) (JEP 473)

Stream gatherers, которые появились в Java 22 в режиме preview, остаются на второе preview без изменений.

Gatherers – это усовершенствование Stream API для поддержки произвольных промежуточных операций.

Напомним, что стримы с появления в 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) или другими вариациями.

Stream gatherers станут постоянным API в Java 24.

Class-File API (Second Preview) (JEP 466)

Стандартное API для парсинга, генерации и трансформации class-файлов, которое появилось в Java 22, остаётся на второе preview с несколькими изменениями.

Новое 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 году.

Class-File API станет постоянным API в Java 24.

Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal (JEP 471)

В классе sun.misc.Unsafe все методы доступа к памяти стали deprecated for removal. Это 77 из 87 методов (в JEP написано 79 из 87, но, похоже, автор посчитал неправильно). При этом 3 из них стали deprecated for removal ещё в Java 18: objectFieldOffset(), staticFieldOffset() и staticFieldBase(). Также в Java 22 стали deprecated for removal 6 методов, не относящиеся к памяти: park(), unpark(), fullFence(), loadFence(), storeFence() и getLoadAverage().

Таким образом, в sun.misc.Unsafe остаётся всего 4 метода, которые пока не являются deprecated, причём один из них – это getUnsafe(), который получает сам объект Unsafe.

По факту всё это означает, что Unsafe больше крайне не рекомендуется использовать. Вместо методов доступа к памяти необходимо использовать стандартное API в Java:

Использования deprecated методов в sun.misc.Unsafe будут вызывать предупреждения во время компиляции:

HelloWorld.java:4: warning: [removal] getByte(long) in Unsafe has been deprecated and marked for removal         unsafe.getByte(address);               ^

В дополнение к предупреждениям на этапе компиляции появится возможность включать предупреждения в рантайме при использовании методов доступа к памяти. Для этого появилась новая опция командной строки --sun-misc-unsafe-memory-access={allow|warn|debug|deny}:

  • --sun-misc-unsafe-memory-access=allow – при вызове методов предупреждения нет (дефолтное значение в Java 23).
  • --sun-misc-unsafe-memory-access=warn – выдаётся предупреждение при первом вызове (станет дефолтным значением в Java 24 или 25).
  • --sun-misc-unsafe-memory-access=debug – выдаётся предупреждение при каждом вызове.
  • --sun-misc-unsafe-memory-access=deny – выбрасывается UnsupportedOperationException (станет дефолтным значением в Java 26 или позже; allow использовать будет нельзя).

В конце концов методы доступа к памяти будут удалены совсем (опция --sun-misc-unsafe-memory-access будет игнорироваться какое-то время, а потом удалится).

Structured Concurrency (Third Preview) (JEP 480)

Structured Concurrency, которое находится в режиме preview с Java 21, остаётся на третий раунд preview без изменений (в Java 22 также не было изменений). До этого оно было в инкубаторе в 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 (Third Preview) (JEP 481)

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

В третье preview было внесено пару изменений: метод callWhere() третьим аргументом теперь принимает новый функциональный интерфейс CallableOp вместо Callable, а также удалён метод getWhere().

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

Vector API (Eighth Incubator) (JEP 469)

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

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

ZGC: Generational Mode by Default (JEP 474)

Режим работы с поколениями, который появился в сборщике мусора ZGC в Java 21, стал включённым по умолчанию. То есть теперь опция -XX:+UseZGC автоматически включает опцию -XX:+ZGenerational. Для выключения режима необходимо указать опцию -XX:-ZGenerational. Однако режим без поколений стал deprecated, и в будущем планируется его окончательное удаление (вместе с опцией -XX:±ZGenerational).

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


ссылка на оригинал статьи https://habr.com/ru/articles/844060/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *