Розбираємо реліз Java 23. Як використовувати його у ваших застосунках

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Всім привіт. Я Сергій Моренець, розробник, тренер, викладач, спікер і технічний письменник. Хочу поділитися з вами своїм досвідом міграції проєктів з JDK 22 на JDK 23. Ця версія вийшла тиждень тому. Це короткостроковий реліз з підтримкою протягом 6 місяців (наступний довгостроковий — JDK 25), в якому немає жодної серйозної фічі в статусі «стабільна». Проте він містить цікаві зміни, заради яких варто познайомитися з оновленням.

Я вже писав про нові фічі в JDK 22, тому в цій статті пропущу ті з них, які не змінилися за ці півроку. Сподіваюся, матеріал буде корисним для всіх, хто хоче більше дізнатися про новинки в Java 23, розібратися в тому, як провести міграцію застосунків на JDK 23, і які тут можуть бути складнощі.

Що нового в Java 23

Спочатку поговоримо про те, чого немає у новій версії. Дуже рідко таке буває, але фіча String templates, яка довго перебувала в режимі preview (ознайомлення), була повністю видалена з JDK 23. Це ще раз підкреслює той факт, що не варто використовувати preview features в реальних проєктах, оскільки вони можуть не тільки кардинально змінюватись, а й зникати.

Якими новими фічами нас порадували розробники JDK:

  1. Примітивні типи в патернах, switch і instanceof.
  2. Декларації імпортів модулів.

Крім того, продовжують обкатуватися фічі, які все ще перебувають у режимі ознайомлення:

  1. Class-File API.
  2. Vector API.
  3. Stream Gatherers.
  4. Неявно визначені main-методи та класи.
  5. Structured Concurrency.
  6. Scoped Values.
  7. Гнучкі визначення конструкторів.

Також є ще дві фічі, про які варто поговорити окремо. Перша з них — це оголошення deprecated сумно відомого класу Unsafe:

@Deprecated(since="23", forRemoval=true)
    @ForceInline
    public int getInt(Object o, long offset) {
        beforeMemoryAccess();
        return theInternalUnsafe.getInt(o, offset);
    }

Цей клас з’явився ще в 2002 році і з того часу широко використовується в різних Java-технологіях для прискорення роботи з пам’яттю (як on-heap, так і off-heap), адже пропонує пряме звернення до неї. Проблема у тому, що це внутрішній метод JDK, не створений для публічного використання. І така широка популярність заважає змінювати та розвивати його. Більше того, не всі розробники правильно використовують можливості Unsafe, оскільки він знову ж таки не документований. У Java вже багато років борються із його незаконним використанням.

Ця ініціатива з’явилася за часів роботи над JDK 9, коли було вирішено інкапсулювати всі внутрішні API. Таким чином, у JDK 9 з’явилася фіча VarHandle API, а в JDK 22 — Foreign Function & Memory API, які пропонують публічний API і фактично інкапсулюють Unsafe. Тому всі, хто все ще його використовують, повинні перейти на новий API. Правда, зробити це буде не так просто, тому що більшість бібліотек все ще перебуває у режимі сумісності з JDK 8/11, а їх перехід на JDK 21/25 — це до дуже далекого майбутнього. Зрештою, в JDK 25 всі методи Unsafe викидатимуть виключення, а в JDK 26 будуть видалені.

Підтримка Markdown

Ще одна не менш фундаментальна зміна — підтримка мови розмітки Markdown для написання JavaDoc. Історично для цього ще з самого початку в 1995 році використовувався HTML спільно з JavaDoc-тегами. Тоді HTML активно розвивався, а до появи Markdown залишалося 9 років. Але через 29 років Markdown став дуже популярним, на ньому пишуть коментарі та пости в GitHub, Stackoverflow і Reddit. Тому не дивно, що на таке зростання популярності відреагували і розробники Java.

Правда, писати JavaDocs можна за допомогою будь-якої мови розмітки. Головне, щоб був препроцесор для перетворення його у вихідний HTML. У JDK 23 для цього використовується бібліотека commonmark-java, яка підтримує специфікацію CommonMark. І тепер можна забути про HTML та багато специфічних тегів JavaDoc і перейти на Markdown.

Головна відмінність при новому підході — відмова від традиційних символів /* */ і перехід на спеціальний початок коментаря ///, щоб позбавитися багатьох обмежень, які приносить перший варіант. Тепер ви можете написати JavaDoc для методу toBoolean класу StringUtils таким чином:

///Converts [java.lang.String] value into **boolean** variable
///
///Text values that will be converted to true value:
///- true
///- TRUE
///
///Usage examples:
///
///| Argument | Returned value |
///|----------|----------------|
///| "true"   | true           |
///| "TRUE"   | true           |
///| "yes"    | false          |
///| "false"  | false          |
///| null     | false          |
public static boolean toBoolean(String str) {
    return str != null && str.equalsIgnoreCase("true");
}

Тут використовується одразу чотири елементи Markdown:

  1. квадратні дужки для посилання на клас;
  2. ** для відображення жирним шрифтом;
  3. список за допомогою —;
  4. таблиця із двома стовпцями.

Утиліта javadoc згенерувала цілком коректний HTML:

Єдине, до чого можна причепитися — таблиця без ліній та кольорів.

Тепер перейдемо до нових фіч в JDK 23.

Примітивні типи в патернах, instanceof і switch

Відразу може виникнути питання, чи switch не підтримує примітивні типи. У будь-якій версії Java ви можете написати наступний блок switch:

public void print(int value) {
    switch (value) {
        case 0:
            System.out.println("0"); break;
        case 1: System.out.println("1"); break;
    }
}

Понад те, лише примітивні типи у перших версіях і підтримувалися. Але нова фіча трошки про інше. У JDK 21 з’явилася функціональність pattern matching for switch:

 public static void print(Object value) {
    switch (value) {
        case String text -> System.out.println("This is a String value");
        case Integer i -> System.out.println("THis is an integer argument");
        default -> {}
    }
}

І одним із обмежень була неможливість використання примітивних типів у блоці case. Тепер це обмеження знято:

public static void print(Object value) {
    switch (value) {
        case byte b -> System.out.println("This is a byte value");
        case int i -> System.out.println("This is an integer argument");
        default -> System.out.println("Unknown type");
    }
}

Але якщо для reference типів перевірка типу на ідентичність зрозуміла, то як вона буде відбуватися для примітивних типів і значень?

Якщо цей метод передати 100:

print(100);

Здається логічним, якщо відпрацює case з byte-аргументом. Адже 100 — це те значення, яке можна присвоїти змінній типу byte. Але виведеться:

                    This is an integer argument

Цікаво, що якщо трохи змінити цей виклик:

byte b = 100;
print(b);

То вже виведеться інший рядок:

This is a byte value

Таким чином, switch спочатку робить операцію boxing, перетворюючи примітивне значення на об’єкт, а далі порівнює цей тип з тими, які вказані в case (теж boxed). Саме тому в першому випадку відпрацював case з int, тому що 100 — це за умовчанням значення типу int. А якби написали:

        print(100L);

То вивелося б:

Unknown type

Чи можна змішувати примітивні та reference-типи в switch? Можна, але при цьому типи, вказані першими в випадку, не повинні бути пов’язані з типами, вказаними після них. Тобто такий switch компілюватися не буде:

public static void print(Object value) {
    switch (value) {
        case Number n -> System.out.println("This is number");            
        case byte b -> System.out.println("This is a byte value");
        case int i -> System.out.println("This is an integer argument");
        default -> System.out.println("Unknown type");
    }
}

Перейдемо до instanceof. Тут ситуація трохи інша. Тепер ви можете вказувати примітивні типи в instanceof:

int i = 100;
if(i instanceof byte b) {
    System.out.println(b);
}

Але при цьому тут перевіряються не типи (на відміну від switch), а можливість значення бути присвоєним тому чи іншому примітивному типу. Так як 100 лежить в діапазоні значень, допустимих для byte, то даний instanceof поверне true.

Це дуже потрібна функціональність, тому що раніше ви могли написати такий код:

int i = 200;
byte b = (byte) i;

І втратити початкове значення. Тепер же ви можете перевірити, чи дійсно можна зробити таке приведення типів без втрати інформації.

Цікава ситуація із дійсними числами. Перевірка instanceof пройде, якщо у числа фактично відсутня дробова частина:

double d = 100.0;
if(d instanceof int i) {
    System.out.println(i);
}

І не пройде, якщо дрібна частина є:

double d = 100.1;
if(d instanceof int i) {
    System.out.println(i);
}

Або значення не може бути перетворено без втрати інформації:

double d = Double.MAX_VALUE;
if(d instanceof int i) {
    System.out.println(i);
}

Ну і остання частина цієї фічі — можливість використання примітивних типів у паттернах (для switch та instanceof):

record Coordinate(int x, int y) {}
 
public static void print(Object value) {
    switch (value) {
        case Coordinate(int x, int y) -> System.out.println("This is coordinate with x: " + x + ", y: " + y);
        default -> System.out.println("Unknown type");
    }
}

Декларації імпортів модулів

Я думаю, всі знають, що будь-який тип пакету java.lang можна використовувати в коді без необхідності явно його імпортувати:

 import java.lang.String;

Але за 29 років у JDK з’явилася величезна кількість інших пакетів, які повсюдно використовуються, але їх доводиться імпортувати явно, наприклад, з пакету java.util і його підпакетів. Тому в JDK 23 з’явилася можливість імпортувати відразу всі типи, але не з пакета, як зараз можна зробити, а з модуля Java. Модулі з’явилися в JDK 9 (проєкт Jigsaw) як спроба позбавитися монолітності дистрибутива JDK. Більше того, за допомогою файлу module-info.java ви можете оголошувати свої модулі, надавати або забороняти доступ до того чи іншого модуля.

Всього в JDK 71 модуль, найголовніший з яких — java.base. І всі загальні пакети (java.util, java.text. java.time, java.io і т.д.) знаходяться саме в ньому. Таким чином, якщо вам не хочеться явно вказувати декларації імпортів для типів цих пакетів, тепер можна додати такий рядок:

import module java.base;

Аналогічно можна проімпортувати будь-який інший модуль з JDK або інших проєктів (які підтримують Java модулі), але при цьому будуть проімпортовані тільки пакети, до яких у вас є доступ (тобто вони проекспортовані):

module java.base {
    exports java.io;
    exports java.lang;
    exports java.lang.annotation;

Тепер поговоримо про ті preview-фічі, які проходять ще одну ітерацію (обкатку) у Java-програмістів. При чому торкнемося тих, в які були внесені зміни порівняно з JDK 22.

Flexible constructor bodies

Тут було додано важливу зміну. Уявімо ієрархію з двох класів. Person:

public class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

Та Employee:

public class Employee extends Person {
    private String department;
    public Employee(String name, String department) {
        Objects.nonNull(name);
        Objects.nonNull(department);
        super(name);
        this.department = department;
    }
}

Ця фіча спочатку (вона з’явилася в JDK 22) дозволяла вставляти код перед викликом базового конструктора, наприклад, для перевірки вхідних аргументів. Тепер ви можете і присвоювати значення полів класу:

public Employee(String name, String department) {
    Objects.nonNull(name);
    Objects.nonNull(department);
    this.department = department;
    super(name);        
}

Це зроблено для випадку, коли у базовому конструкторі викликається перевизначений метод, який звертається до полів дочірнього класу. Не найкраща практика, але так інколи роблять. Ці поля при виконанні методу матимуть дефолтні значення, а не ті, які ми можемо їм надати при створенні об’єкта, що може призвести до side effects. Щоб не було такого дисонансу, і було додано таку зміну.

При цьому звертатися до таких полів перед викликом базового конструктора, як і раніше, не можна:

public Employee(String name, String department) {
    Objects.nonNull(name);
    Objects.nonNull(department);
    this.department = department;
    System.out.println(this.department);
    super(name);
}

Неявно визначені основні методи та класи

Тут з’явилися дві важливі зміни. Перша полягає у появі нового утилітного класу-синглтону IO:

@PreviewFeature(feature = PreviewFeature.Feature.IMPLICIT_CLASSES)
public final class IO {
    private IO() {
        throw new Error("no instances");
    }
    public static void println(Object obj) {
        con().println(obj);
    }

Але головне не в тому, що такий клас з’явився, а в тому, що неявно визначені класи можуть використовувати три його статичні методи без їхнього явного імпорту, тобто приблизно так:

void main() {
    println("This is implicitly declared class");
}

Так давно можна було робити в утиліті jshell, тепер ця можливість з’явилася і для звичайного коду.

Друга зміна полягає в тому, що для таких класів більше не потрібно явно імпортувати типи тих пакетів, які потрапили в модуль java.base, тобто java.util, java.io, java.text і так далі:

void main() {
    List<String> items = List.of("1", "2");
    println(items);
}

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

Переходимо на JDK 23

Ми спробували перенести кілька наших проєктів на JDK 23. Хоча жодну з її нових фіч використати не вдасться, але принаймні перевіримо проєкти на сумісність. Оновлюємо Maven-конфігурацію:

       <properties>
              <java.version>1.23</java.version>
       <java.release.version>23</java.release.version>
       </properties>

Якщо у вас Gradle, то тут трохи інший блок змін:

java {
     sourceCompatibility = org.gradle.api.JavaVersion.VERSION_23
     targetCompatibility = org.gradle.api.JavaVersion.VERSION_23
 
     toolchain {
       languageVersion.set(JavaLanguageVersion.of(23))
   }
}  

Усі проєкти, окрім одного, запрацювали без проблем. Але в одному при запуску Maven збірки відразу отримуємо помилку від бібліотеки Google Error Prone:

 Compilation failure
[ERROR] common/infra/event/BaseEvent.java: An unhandled exception was thrown by the Error Prone static analysis plugin.
[ERROR]      Please report this at https://github.com/google/error-prone/issues/new and include the following:
[ERROR]
[ERROR]      error-prone version: 2.30.0
[ERROR]      BugPattern: (see stack trace)
[ERROR]      Stack Trace:
[ERROR]      java.lang.NoSuchFieldError: Class com.sun.tools.javac.parser.Tokens$Comment$CommentStyle does not have member field 'com.sun.tools.javac.parser.Tokens$Comment$CommentStyle JAVADOC'
[ERROR]         at lombok.javac.Javac$JavadocOps_8$1.getStyle(Javac.java:364)
[ERROR]         at jdk.compiler/com.sun.tools.javac.parser.DocCommentParser.getTextKind(DocCommentParser.java:171)

Зважаючи на все, ця помилка пов’язана з підтримкою Markdown в JavaDoc, і можна спочатку припустити, що версія Google Error Prone (2.30), що використовується зараз, не дозволяє запускати збірку на JDK 23. Ця проблема добре відома, але ноги у неї ростуть з іншої бібліотеки, а саме з Lombok.

Так, на жаль, автори Lombok знову не помітили випуск нової версії JDK і забули протестувати свій продукт на сумісність. Це при тому, що JDK 23 вийшла 10 днів тому, а її General Release версія (тобто та, яка і буде фінальною) — понад місяць тому. Більше того, у Lombok немає ніякої робочої edge-версії, яка б підтримувала JDK 23. Таким чином, якщо у вас використовується Lombok, потрібно почекати оновлень від Lombok. Але це єдина технологія Java, де знайдені проблеми сумісності з JDK 23.

Щодо Docker images, то основні дистриб’ютори — Eclipse Temurin, Amazon Corretto, Azul Zulu вже підготували свіжі images під нову версію. Тож із упаковкою в Docker image проблем не виникне.

Висновки

На жаль, у JDK 23 увійшла лише одна нова стабільна фіча — підтримка Markdown. Тож якщо ви любите писати JavaDoc і добре знаєте Markdown, вам буде за що оцінити нову версію Java. Всі інші фічі увійшли в режимі ознайомлення. Застосовувати їх у реальних проєктах не можна, але сподіватимемося, що вони будуть оголошені стабільними вже в наступних версіях Java. Чи станеться це в наступній LTS версії JDK 25 або раніше в JDK 24 — невідомо, але максимум через рік ви зможете їх використовувати.

Міграція на JDK 23 поки що неможлива. Якщо ви використовуєте Lombok у своєму проєкті, необхідно зачекати на офіційне оновлення від авторів.

👍ПодобаєтьсяСподобалось10
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі