Розбираємо реліз Java 23. Як використовувати його у ваших застосунках
Всім привіт. Я Сергій Моренець, розробник, тренер, викладач, спікер і технічний письменник. Хочу поділитися з вами своїм досвідом міграції проєктів з JDK 22 на JDK 23. Ця версія вийшла тиждень тому. Це короткостроковий реліз з підтримкою протягом 6 місяців (наступний довгостроковий — JDK 25), в якому немає жодної серйозної фічі в статусі «стабільна». Проте він містить цікаві зміни, заради яких варто познайомитися з оновленням.
Я вже писав про нові фічі в JDK 22, тому в цій статті пропущу ті з них, які не змінилися за ці півроку. Сподіваюся, матеріал буде корисним для всіх, хто хоче більше дізнатися про новинки в Java 23, розібратися в тому, як провести міграцію застосунків на JDK 23, і які тут можуть бути складнощі.
Що нового в Java 23
Спочатку поговоримо про те, чого немає у новій версії. Дуже рідко таке буває, але фіча String templates, яка довго перебувала в режимі preview (ознайомлення), була повністю видалена з JDK 23. Це ще раз підкреслює той факт, що не варто використовувати preview features в реальних проєктах, оскільки вони можуть не тільки кардинально змінюватись, а й зникати.
Якими новими фічами нас порадували розробники JDK:
- Примітивні типи в патернах, switch і instanceof.
- Декларації імпортів модулів.
Крім того, продовжують обкатуватися фічі, які все ще перебувають у режимі ознайомлення:
- Class-File API.
- Vector API.
- Stream Gatherers.
- Неявно визначені main-методи та класи.
- Structured Concurrency.
- Scoped Values.
- Гнучкі визначення конструкторів.
Також є ще дві фічі, про які варто поговорити окремо. Перша з них — це оголошення 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:
- квадратні дужки для посилання на клас;
- ** для відображення жирним шрифтом;
- список за допомогою —;
- таблиця із двома стовпцями.
Утиліта 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 у своєму проєкті, необхідно зачекати на офіційне оновлення від авторів.
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.
Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів