Використання Java 26 для Java додатків

💡 Усі статті, обговорення, новини про Java — в одному місці. Приєднуйтесь до Java спільноти!

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

Що нового в Java 26

Java 26 — це короткостроковий реліз, в якому не так багато нових фітч, проте є фітчі, що стосуються API/синтаксису, про які цікаво буде дізнатися:

  1. Зміни у роботі з final полями
  2. Підтримка HTTP/3

Ще в JDK 11 з’явився HTTP клієнт, який одразу підтримував дві версії HTTP: 1.1 та 2.0. У 2022 році вийшов вже HTTP/3, який заснований на протоколі QUIC (а той, у свою чергу на UDP). Було б логічним додати його підтримку ще у JDK 25, проте вона з’явилася тільки зараз і тільки для клієнтського використання:

var client = HttpClient.newBuilder()
      .version(HttpClient.Version.HTTP_3)
      .build();

При цьому за замовчуванням використовується той самий HTTP/2. Оскільки зараз немає можливості заздалегідь визначити, чи підтримує сервер HTTP/3, то у разі відсутності підтримки з’єднання автоматично переходить на HTTP/2, тому такий підхід у конфігурації клієнта є безпечним.

Підтримка нової версії HTTP виглядає приємним сюрпризом для тих, хто використовує HTTP клієнти, оскільки в одній із найпопулярніших бібліотек для роботи з HTTP — Apache HTTPClient досі немає підтримки HTTP/3. Її немає і ще в одній популярній бібліотеці — okhttp. Серед веб-серверів така підтримка вже є у Jetty (починаючи з 12-ї версії) та Netty, але поки що відсутня у Tomcat/Undertow. При цьому варіант API в HTTP клієнті від JDK дозволяє простіше вказувати/міняти версію HTTP, а ось, наприклад, у популярному клієнті Reactor Netty ви повинні явно написати, що використовуєте HTTP/3 та QUIC, що ускладнює міграцію з HTTP/2:

Connection connection = QuicClient.create()
      .remoteAddress(() -> new InetSocketAddress("localhost", 8080))
      .connectNow();

Ще одна зміна в API JDK 26 — поява такого режиму роботи з рядками як folding. Зазвичай розробники для порівняння рядків використовують два режими/операції:

  • Реєстронезалежний (equals)
  • Реєстрозалежний (equalsIgnoreCase)

І ось зараз з’явилося так зване fold-порівняння символів за їх UNICODE-кодами в класі String (новий метод equalsFoldCase). Так, наприклад, якщо у вас є два рядки — «Fuß» та «FUSS», то будь-які порівняння у попередніх версіях Java повертали false. Тепер Java використовує спеціальні таблиці folding mapping для такого порівняння:

String a = "Fuß";
String b = "FUSS";
boolean equalsFoldCase = a.equalsFoldCase(b);      // true      
boolean equalsIgnoreCase = a.equalsIgnoreCase(b);  //false

Також у JDK 26 є кілька інфраструктурних змін:

  1. Видалення Applet API
  2. Підтримка AOT (ahead-of-time) кешування об’єктів для будь-якого збирача сміття
  3. Поліпшення продуктивності в G1

Деяким може здатися сумним видалення аплетів з JDK, адже саме на них і базувалися початкові успіхи та популярність Java ще в середині 90-х років. Тим не менш, сучасні браузери вже не підтримують аплети (останнім припинив підтримку Firefox на початку 2017 року). Ну і сама Java поступово готувалася до їхнього догляду:

  • Вони були оголошені deprecated у Java 9
  • У JDK 11 було видалено утиліту appletviewer, яка дозволяла тестувати аплети без браузера.
  • У JDK 17 було оголошено, що Applet API готується до видалення
  • І ось нарешті був повністю вилучений їх код

Також у JDK 26 є кілька фітч, які все ще продовжують перебувати у статусі «ознайомчих» (preview):

  1. Structred concurrency
  2. Lazy constants
  3. Vector API
  4. Примітивні типи в патернах, instanceof і switch
  5. Шифрування криптографічних ключів, використовуючи PEM (Privacy-Enhanced Mail) формат

Більшість із них не змінилася з часів JDK 25, крім двох, про зміни в яких я розповім докладніше.

Lazy constants — це фітча, яка в JDK 25 була відома як Stable Values, але потім її піддали і ребрендингу, і спрощенню, видаливши у JDK 26 низькорівневий API. Lazy constants — це об’єкти, які розглядаються JVM як константи, але з можливістю lazy initialization:

var lazyValue = LazyConstant.of(() -> 1);
var value = lazyValue.get();

У JDK 26 додали нові утилітні методи для роботи з колекціями, наприклад створення пулу об’єктів фіксованого розміру (тут один елемент):

var lazyList = List.ofLazy(1, _ -> new Item());

І тут також пул фіксованого розміру, але на базі Map і з наявністю ключів:

var lazyMap = Map.ofLazy(Set.of(1,2), key -> new Item(key));

Що стосується такої фітчі, як примітивні типи в switch, то тут дві невеликі зміни, що стосуються dominance checks на базі unconditional exactness.

Якщо JDK 25 подібний код відкомпілюється і виведе «float»:

int j = 0;
switch (j) {
     case float f -> System.out.println("Float");
     case 16_777_216 -> System.out.println("16_777_216");
     default -> System.out.println("default");
}

то в JDK 26 він вже компілюватися не буде саме через dominance checks, так як значення 16_777_216 хоч і не є типом float (це integer), але може бути неявно перетворено на тип float.

Тому в JDK 26 його треба переписати як:

switch (j) {
    case 16_777_216 -> {}
    case float f    -> {}            
    default         -> {}
}

Аналогічно більше не компілюватиметься switch типу:

switch (j) {
    case int _ -> System.out.println("int");
    case float _ -> System.out.println("float");
}

Тому що в цьому випадку тип float є більш загальним (домінує) над типом int.

Зміни у роботі з final полями

Якщо ви досвідчений Java-розробник, то, напевно, багаторазово бачили подібний код у своєму проекті або вихідному коді різних бібліотек/фреймворків:

var starter = new Starter();
var field = Starter.class.getDeclaredField("status");
field.setAccessible(true);
field.set(starter, "");

Така можливість у нас є завдяки Reflection API (з’явилася в JDK 1.1), а також змінам у JDK 5, які дозволили змінювати final поля (для підтримки нових фреймворків, таких як Spring/Hibernate), а також для десеріалізації об’єктів з такими полями. У той же час динамічна зміна значень полів final є деяким злом:

  1. Розробники більше не можуть бути впевнені, що final поле не зміниться після створення об’єкта, що в принцу нівелює цінність immutable даних
  2. JVM також доводиться щоразу прочитувати значення такого поля, а не використовувати constant folding, що негативно впливає на performance

Тому зміна final полів під час виконання давно є анти-патерном і використовується дедалі рідше. І в JDK 26 були вжиті певні зміни для того, щоб зробити final поля дійсно незмінними. Але насамперед цікаво розглянути, як працює Reflection API з final полями у JDK 25. Візьмемо Java record:

record UserInfo(int id, String name) {
}

І спробуємо змінити значення id:

var info = new UserInfo(1, «John»);
var field = UserInfo.class.getDeclaredField("id");
field.setAccessible(true);
field.set(info, 10);

В результаті отримуємо помилку:

Exception in thread "main" java.lang.IllegalAccessException: Can not set final int field UserInfo.id to java.lang.Integer
at java.base/jdk.internal.reflect.FieldAccessorImpl.throwFinalFieldIllegalAccessException(FieldAccessorImpl.java:132)
at java.base/jdk.internal.reflect.FieldAccessorImpl.throwFinalFieldIllegalAccessException(FieldAccessorImpl.java:136)

в якій немає нічого дивного, тому що заборона на зміну полів у Java records діє з JDK 15. (Самі Java records з’явилися в JDK 14 і стали стабільною фітчею в 16 версії). Причому, що цікаво, помилка викидається не в setAccessible, а саме при виконанні методу set. Аналогічно помилку видасть код, який спробує змінити значення полів у hidden класах. Заборона на такі вільності існує в Java давно, ще до JDK 15, наприклад, ви не можете змінювати значення констант (static final полів):

public static final String status = "ok";

За будь-якої спроби змінити його ви також отримаєте аналогічну помилку:

var field = Starter.class.getDeclaredField("status");
field.setAccessible(true);
field.set(null, "");

І ось зараз при спробі змінити значення final поля за допомогою Reflection API ви отримаєте попередження у консолі:

WARNING: Final field status in class demo.Starter has been mutated reflectively by class demo.Starter in unnamed module @23fc625e 
WARNING: Use —enable-final-field-mutation=ALL-UNNAMED to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled

Щоб його позбутися, потрібно передати в JVM опцію —enable-final-field-mutation=ALL-UNNAMED. Якщо ж ви, навпаки, не хочете, щоб у вашому проекті змінювалися final поля та плануєте боротися з цим, потрібно додати інший прапор —illegal-final-field-mutation:

  1. —illegal-final-field-mutation=debug — додає stacktrace для кожного такого випадку
  2. —illegal-final-field-mutation=deny — викидає run-time помилку
Exception in thread "main" java.lang.IllegalAccessException.

При цьому потрібно бути обережним, тому що тепер ви не можете клонувати об’єкти у вашому проекті:

var starter = new Starter();
var clone = starter.clone();

Цікаво, що Java серіалізація/десеріалізація подібних об’єктів працює без помилок навіть в останньому випадку, але в майбутньому потрібно перейти на безпечніший ReflectionFactory API (це стосується і 3rd-party бібліотек).

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

Для тестування JDK 26 та її змін було вибрано декілька Gradle/Maven проектів, при цьому використовувалася остання версія Gradle (9.4.1), яка підтримує запуск на JDK 26. Крім того, враховуючи досвід попередніх міграцій, було оновлено поточні версії:

  • Spring Boot -> до 4.0.4
  • Lombok —> до 1.18.44
  • Mockito —> до 5.20

Перший Gradle проект успішно компілюється та запускається на JDK 26. Цікаво, що сам Lombok при цьому офіційно JDK 26 ще не підтримує.

Тепер спробуємо змінити версію Java для компіляції/генерації байт-коду з:

java {
    sourceCompatibility = JavaVersion.VERSION_25
    targetCompatibility = JavaVersion.VERSION_25
}

На

java {
    sourceCompatibility = JavaVersion.VERSION_26
    targetCompatibility = JavaVersion.VERSION_26
}

І тут проект теж збирається та запускається успішно. Аналогічно і з Maven проектом. При цьому у вас все-таки можуть виникнути кілька складнощів при міграції:

  1. Якщо ви використовуєте Docker images на базі Tomcat, для них немає не-LTS версій, тобто ви повинні використовувати JDK 25
  2. Якщо ви використовуєте JDK images від Adoptium (Eclipse Temurin), то навіть через два тижні після релізу JDK 26 їх все ще немає на Docker Hub. Офіційна причина — проблеми під час запуску автоматизованих тестів (TCK), терміни вирішення цієї проблеми невідомі. Зрозуміло, це тимчасова проблема і якщо ж вам потрібно мати Docker images вже зараз для тестування, то є альтернативні від Amazon Corretto або Azul Zulu.

Що стосується нових фітч, то більшість із них перебувають у стадії preview і їх використовувати не рекомендується, оскільки вони можуть змінитися будь-якої миті.

Висновки

У JDK 26 не так багато змін, заради яких її варто використовувати зараз. Більшість нових фітч все ще проходять обкатку заради фідбека розробників. Тому вона викликає більше інтересу для тих, хто любить просто тестувати нові версії. У той же час, міграція на останню версію Java не поєднується зі змінами коду/конфігурації, якщо ви регулярно оновлюєте версії залежностей.

Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному2
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
Тому що в цьому випадку тип float є більш загальним (домінує) над типом int.

float не домінує над int, у кожного є значення, що не можуть бути представлені іншим типом. Для int це, наприклад, 16777217, 16777219..., float їх округлить до найближчого парного. Тому тут щось не те. Або ви не так зрозуміли, або тут насильство над логікою з боку стандартизаторів.

Цікаво, приклад з 16777216 — тобто останнім з int з неперервної послідовности, яка представляється у float — це випадковий збіг, або це навмисна «дуля в кишені»?

Підтримка нової версії HTTP

Є відгуки, наскільки вона стабільна? Всі баги вичіщені?

Є відгуки, наскільки вона стабільна? Всі баги вичіщені?

На жаль, в ньому і без нового стандарту достатньо проблем.
blog.arkey.fr/...​o-not-use-jdk-httpclient

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