В одной из моих предыдущих статей я писал о фичах между LTS-версиями Java 17 и 21. Сегодня, два года спустя (Как?! Уже два года?!), выходит новый LTS-релиз — Java 25.
Подавляющее большинство проектов пропускают промежуточные релизы и используют только LTS-версии Java. Так что давайте посмотрим, какие возможности новая LTS-версия (Java 25) приносит по сравнению с предыдущей LTS-версией (Java 21).
Митап-версия данной статьи доступна тут.
В таблицах ниже я буду использовать следующие сокращения для обозначения состояния фич:
exp = Experimental;
inc[2|3|4|etc] = Incubator [2|3|4|etc];
pre[2|3|4|etc] = Preview [2|3|4|etc];
prod = Production;
w/d = Withdrawn.
Production-ready фичи в Java 25
Feature | Java 21 | Java 22 | Java 23 | Java 24 | Java 25 |
|---|---|---|---|---|---|
Scoped Values | JEP 446 (pre) | JEP 464 (pre2) | JEP 481 (pre3) | JEP 487 (pre4) | JEP 506 (prod) |
Flexible Constructor Bodies | - | JEP 447 (pre) | JEP 482 (pre2) | JEP 492 (pre3) | JEP 513 (prod) |
Unnamed Variables & Patterns | JEP 443 (pre) | JEP 456 (prod) | + | + | + |
Stream Gatherers | - | JEP 461 (pre) | JEP 473 (pre2) | JEP 485 (prod) | + |
Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism | - | - | - | JEP 496 (prod) | + |
Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm | - | - | - | JEP 497 (prod) | + |
Key Derivation Function API | - | - | - | JEP 478 (pre) | JEP 510 (prod) |
Foreign Function & Memory API | JEP 442 (pre3) | JEP 454 (prod) | + | + | + |
Compact Source Files and Instance Main Methods | JEP 445 (pre) | JEP 463 (pre2) | JEP 477 (pre3) | JEP 495 (pre4) | JEP 512 (prod) |
Markdown Documentation Comments | - | - | JEP 467 (prod) | + | + |
Module Import Declarations | - | - | JEP 476 (pre) | JEP 494 (pre2) | JEP 511 (prod) |
Ahead-of-Time Class Loading & Linking | - | - | - | JEP 483 (prod) | + |
Ahead-of-Time Command-Line Ergonomics | - | - | - | - | JEP 514 (prod) |
Ahead-of-Time Method Profiling | - | - | - | - | JEP 515 (prod) |
JFR Cooperative Sampling | - | - | - | - | JEP 518 (prod) |
JFR Method Timing & Tracing | - | - | - | - | JEP 520 (prod) |
Compact Object Headers | - | - | - | JEP 450 (exp) | JEP 519 (prod) |
Prepare to Restrict the Use of JNI | - | - | - | JEP 472 (prod) | + |
Class-File API | - | JEP 457 (pre) | JEP 466 (pre2) | JEP 484 (prod) | + |
Permanently Disable the Security Manager | - | - | - | JEP 486 (prod) | + |
Launch Multi-File Source-Code Programs | - | JEP 458 (prod) | + | + | + |
Region Pinning for G1 | - | JEP 423 (prod) | + | + | + |
Late Barrier Expansion for G1 | - | - | - | JEP 475 (prod) | + |
ZGC: Generational Mode by Default | - | - | JEP 474 (prod) | + | + |
ZGC: Remove the Non-Generational Mode | - | - | - | JEP 490 (prod) | + |
Generational Shenandoah | - | - | - | JEP 404 (exp) | JEP 521 (prod) |
Synchronize Virtual Threads without Pinning | - | - | - | JEP 491 (prod) | + |
Linking Run-Time Images without JMODs | - | - | - | JEP 493 (prod) | + |
Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal | - | - | JEP 471 (prod) | + | + |
Warn upon Use of Memory-Access Methods in sun.misc.Unsafe | - | - | - | JEP 498 (prod) | + |
Remove the Windows 32-bit x86 Port | - | - | - | JEP 479 (prod) | + |
Deprecate the 32-bit x86 Port for Removal | - | - | - | JEP 501 (prod) | + |
Remove the 32-bit x86 Port | - | - | - | - | JEP 503 (prod) |
Preview-фичи в Java 25
Feature | Java 21 | Java 22 | Java 23 | Java 24 | Java 25 |
|---|---|---|---|---|---|
Stable Values | - | - | - | - | JEP 502 (pre) |
PEM Encodings of Cryptographic Objects | - | - | - | - | JEP 470 (pre) |
Structured Concurrency | JEP 453 (pre) | JEP 462 (pre2) | JEP 480 (pre3) | JEP 499 (pre4) | JEP 505 (pre5) |
Primitive Types in Patterns, instanceof, and switch | - | - | JEP 455 (pre) | JEP 488 (pre2) | JEP 507 (pre3) |
Incubator фичи в Java 25
Экспериментальные фичи в Java 25
Feature | Java 21 | Java 22 | Java 23 | Java 24 | Java 25 |
|---|---|---|---|---|---|
JFR CPU-Time Profiling | - | - | - | - | JEP 509 (exp) |
Отозванные фичи между Java 21 и Java 25
Production-Ready фичи с примерами
Теперь давайте кратко разберём некоторые фичи, готовые к использованию в проде, и посмотрим примеры кода.
Полный исходный код доступен на GitHub: pfilaretov42/java-features.
Scoped Values
Scoped Values предоставляют более безопасную альтернативу thread-local переменным, предлагая неизменяемые, наследуемые значения, доступные только внутри определённой области видимости.
До: подход с ThreadLocal
Словно ношение Единого Кольца — опасно, если доверить кому попало:
class ThreadLocalTest { private static final ThreadLocal<String> CURRENT_RING_BEARER = new ThreadLocal<>(); void dangerousJourney() { CURRENT_RING_BEARER.set("Frodo"); try { travelToMordor(); } finally { CURRENT_RING_BEARER.remove(); } } void travelToMordor() { // Any method in the call chain can access... String bearer = CURRENT_RING_BEARER.get(); System.out.println(bearer + " bears the burden"); // ...and modify CURRENT_RING_BEARER.set("Sam"); bearer = CURRENT_RING_BEARER.get(); System.out.println(bearer + " bears the burden"); } public static void main() { ThreadLocalTest test = new ThreadLocalTest(); test.dangerousJourney(); } }
После: способ со ScopedValue
Подобно фиалу Галадриэль — свет, надёжно удерживаемый внутри:
public class ScopedValuesTest { private static final ScopedValue<String> CURRENT_RING_BEARER = ScopedValue.newInstance(); void safeJourney() { ScopedValue.where(CURRENT_RING_BEARER, "Frodo") .run(this::travelToMordorSafely); } private void travelToMordorSafely() { // Only accessible in this scope, cannot be modified String bearer = CURRENT_RING_BEARER.get(); System.out.println(bearer + " bears the burden safely"); // Attempting to rebind would result in compilation error: //CURRENT_RING_BEARER.set("Sam"); } void noJourney() { // Not accessible outside the scope, throws NoSuchElementException (ScopedValue not bound): String bearer = CURRENT_RING_BEARER.get(); System.out.println(bearer + " bears the burden"); } public static void main() { ScopedValuesTest test = new ScopedValuesTest(); test.safeJourney(); test.noJourney(); } }
Flexible Constructor Bodies
Позволяет более гибко располагать инструкции в теле конструктора, включая возможность размещать код перед явными вызовами других конструкторов (this() или super()).
До: жёсткие правила конструктора
Словно непоколебимые стены Ортанка — никакой гибкости:
class Palantir { private final String owner; private final boolean isCorrupted; public Palantir(String owner) { // This would not compile: //validateOwner(owner); // Call to this() must be the first statement: this(validateOwner(owner), false); } private static String validateOwner(String owner) { if (owner == null || owner.isBlank()) { throw new IllegalArgumentException("Owner cannot be null or blank"); } System.out.println("A new palantir is given to " + owner); return owner; } private Palantir(String owner, boolean isCorrupted) { this.owner = owner; this.isCorrupted = isCorrupted; } }
После: изящная свобода конструктора
Теперь струится, как воды Бруинена — естественно и свободно:
class Palantir { private final String owner; private final boolean isCorrupted; public Palantir(String owner) { validateOwner(owner); // Now this() call is allowed after other statements: this(owner, false); } private static void validateOwner(String owner) { if (owner == null || owner.isBlank()) { throw new IllegalArgumentException("Owner cannot be null or blank"); } System.out.println("A new palantir is given to " + owner); } private Palantir(String owner, boolean isCorrupted) { this.owner = owner; this.isCorrupted = isCorrupted; } }
Unnamed Variables & Patterns
Позволяет разработчикам явно отмечать неиспользуемые переменные и шаблоны с помощью символа подчёркивания (_), чтобы повысить читаемость кода.
Типичные случаи использования:
параметры исключений;
параметры лямбд;
переменные при pattern matching;
переменные в циклах, когда нужен только счётчик.
До: приходилось давать имена неиспользуемым переменным
Как если бы у каждого орка из Мордора был бейджик с именем:
try { int rings = forgeNewRing(); } catch (RingForgingException e) { // Never used System.out.println("The fires of Mount Doom failed us!"); } // Pattern matching with unused bindings if (fighter instanceof Elf(String name, Weapon(String type, int damage))) { // name and damage unused System.out.println("Armed with: " + type); }
После: лаконичное объявление неиспользуемых переменных
Так же элегантно, как Леголас, попирающий законы гравитации:
try { int _ = forgeNewRing(); } catch (RingForgingException _) { // Clear this is unused System.out.println("The fires of Mount Doom failed us!"); } // Clean pattern matching if (fighter instanceof Elf(_, Weapon(String type, _))) { System.out.println("Armed with: " + type); }
Stream Gatherers
Представляет пользовательские промежуточные операции с помощью нового метода gather(Gatherer), который позволяет выполнять более сложные преобразования стримов, например, оконные функции.
До: ограничено встроенными операциями
Как попытка выковать Единое Кольцо, имея лишь простые инструменты:
List<String> hobbits = List.of("Frodo", "Sam", "Merry", "Pippin"); // To get overlapping pairs, we needed a workarounds List<String> pairs = IntStream.range(0, hobbits.size() - 1) .mapToObj(i -> hobbits.get(i) + " & " + hobbits.get(i + 1)) .toList(); System.out.println(pairs); // Output: ["Frodo & Sam", "Sam & Merry", "Merry & Pippin"]
После: пользовательские преобразования стримов
Теперь под рукой могущество эльфийских кузнецов:
List<String> hobbits = List.of("Frodo", "Sam", "Merry", "Pippin"); Gatherer<String, ?, String> pairing = Gatherer.ofSequential( () -> new Object() { String previous; }, (state, element, downstream) -> { if (state.previous != null) { downstream.push(state.previous + " & " + element); } state.previous = element; return true; } ); List<String> pairs = hobbits.stream() .gather(pairing) .toList(); System.out.println(pairs); // Output: ["Frodo & Sam", "Sam & Merry", "Merry & Pippin"]
Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism
Java 24 представляет встроенную поддержку механизма инкапсуляции ключей на основе модульных решёток (ML-KEM), который является частью квантово-устойчивой криптографии.
Эта фича обеспечивает квантово-устойчивую инкапсуляцию ключей, используемую для защиты симметричных ключей при передаче по небезопасным каналам связи с применением криптографии с открытым ключом.
Её можно использовать, чтобы противостоять мощи квантовых компьютеров — будущей угрозе для классических RSA и Диффи–Хеллмана.
На примере обмена сессионными ключами...
// Elrond prepares his runes of protection (Receiver creates key pair) ElrondTheReceiver elrond = new ElrondTheReceiver(); // Gandalf crafts a secret using Elrond’s rune (Sender uses receiver's public key to encapsulate session key) GandalfTheSender gandalf = new GandalfTheSender(elrond.revealPublicRune()); SecretKey senderSessionKey = gandalf.getSessionKey(); // Elrond deciphers the sealed whisper from Gandalf (Receiver decapsulates to get the same session key) SecretKey receiverSessionKey = elrond.decapsulateWhisper(gandalf.getSealedWhisper()); boolean secretsMatch = MessageDigest.isEqual(senderSessionKey.getEncoded(), receiverSessionKey.getEncoded()); // Output for verification: HexFormat hex = HexFormat.of(); System.out.println("Sender session key: " + hex.formatHex(senderSessionKey.getEncoded())); System.out.println("Receiver session key: " + hex.formatHex(receiverSessionKey.getEncoded())); System.out.println("Secrets match: " + secretsMatch); if (secretsMatch) { // Gandalf and Elrond exchange messages using the securely transmitted session key // ... }
...мы видим следующее.
До: передача ключей с помощью RSA
Словно держать в руках сталь Гондора — мощно, но не всесильно:
/** * Receiver generates key pair and decapsulates session key */ class ElrondTheReceiver { private final KeyPair keyPair; public ElrondTheReceiver() throws GeneralSecurityException { KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(4096); keyPair = generator.generateKeyPair(); } public PublicKey revealPublicRune() { return keyPair.getPublic(); } public SecretKey decapsulateWhisper(byte[] sealedWhisper) throws GeneralSecurityException { Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate()); byte[] keyBytes = cipher.doFinal(sealedWhisper); return new SecretKeySpec(keyBytes, "AES"); } }
/** * Sender uses receiver's public key to generate session key */ class GandalfTheSender { private final SecretKey sessionKey; // Sender’s secret session key private final byte[] sealedWhisper; // The message with session key encapsulated for Receiver public GandalfTheSender(PublicKey receiverPublicKey) throws GeneralSecurityException { // Generate session key (AES) KeyGenerator generator = KeyGenerator.getInstance("AES"); generator.init(256); sessionKey = generator.generateKey(); // Encrypt (encapsulate) session key with RSA Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); cipher.init(Cipher.ENCRYPT_MODE, receiverPublicKey); sealedWhisper = cipher.doFinal(sessionKey.getEncoded()); } public SecretKey getSessionKey() { return sessionKey; } public byte[] getSealedWhisper() { return sealedWhisper; } }
После: постквантовая инкапсуляция ключей
Подобно Вратам Мории — неприступны для любой силы:
/** * Receiver generates key pair and decapsulates session key */ class ElrondTheReceiver { private final KeyPair keyPair; public ElrondTheReceiver() throws GeneralSecurityException { KeyPairGenerator generator = KeyPairGenerator.getInstance("ML-KEM"); keyPair = generator.generateKeyPair(); } public PublicKey revealPublicRune() { return keyPair.getPublic(); } public SecretKey decapsulateWhisper(byte[] sealedWhisper) throws GeneralSecurityException { KEM kem = KEM.getInstance("ML-KEM"); KEM.Decapsulator decapsulator = kem.newDecapsulator(keyPair.getPrivate()); return decapsulator.decapsulate(sealedWhisper); } }
/** * Sender uses receiver's public key to generate session key */ class GandalfTheSender { private final SecretKey sessionKey; // Sender’s secret session key private final byte[] sealedWhisper; // The message with session key encapsulated for Receiver public GandalfTheSender(PublicKey receiverPublicKey) throws GeneralSecurityException { KEM kem = KEM.getInstance("ML-KEM"); KEM.Encapsulator encapsulator = kem.newEncapsulator(receiverPublicKey); KEM.Encapsulated encapsulated = encapsulator.encapsulate(); sessionKey = encapsulated.key(); sealedWhisper = encapsulated.encapsulation(); } public SecretKey getSessionKey() { return sessionKey; } public byte[] getSealedWhisper() { return sealedWhisper; } }
Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm
ML-DSA обеспечивает постквантовые защищённые цифровые подписи на основе криптографии на решётках и является частью нового криптографического набора Java для эпохи квантовых вычислений.
На примере подписи сообщений...
// Gandalf prepares his spell (Sender creates a public/private key pair) GandalfTheSender gandalf = new GandalfTheSender(); // Sender signs a message using the private key String scroll = gandalf.speakWordsOfPower(); byte[] waxSeal = gandalf.signMessage(scroll); // Aragorn receives the scroll and the seal AragornTheReceiver aragorn = new AragornTheReceiver(gandalf.revealPublicRune()); // Verifying the true words of Gandalf (Receiver verifies the message using the sender's public key) boolean isTrueScroll = aragorn.verifyMessage(scroll, waxSeal); System.out.println("Is the scroll valid: " + isTrueScroll); // true // Attempt to fool the ranger with a forged scroll (verification fails for counterfeit message) String fakeScroll = """ A new Power is rising. Against it the old allies and policies will not avail us at all. \ There is no hope left in Elves or dying Númenor. """; isTrueScroll = aragorn.verifyMessage(fakeScroll, waxSeal); System.out.println("Is the forged scroll valid: " + isTrueScroll); // false
...мы видим следующее.
До: подпись с помощью ECDSA
Словно стены Изенгарда перед гневом энтов:
/** * Sender signs message with private key */ class GandalfTheSender { private final KeyPair keyPair; public GandalfTheSender() throws Exception { KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); generator.initialize(new ECGenParameterSpec("secp256r1")); keyPair = generator.generateKeyPair(); } public PublicKey revealPublicRune() { return keyPair.getPublic(); } public byte[] signMessage(String message) throws Exception { Signature runeEngraver = Signature.getInstance("SHA256withECDSA"); runeEngraver.initSign(keyPair.getPrivate()); runeEngraver.update(message.getBytes()); return runeEngraver.sign(); } public String speakWordsOfPower() { return """ It is not our part here to take thought only for a season, or for a few lives of Men, \ or for a passing age of the world. We should seek a final end of this menace, \ even if we do not hope to make one. """; } }
/** * Receiver verifies the message with public key */ class AragornTheReceiver { private final PublicKey senderPublicKey; public AragornTheReceiver(PublicKey senderPublicKey) { this.senderPublicKey = senderPublicKey; } public boolean verifyMessage(String message, byte[] signature) throws Exception { Signature runeChecker = Signature.getInstance("SHA256withECDSA"); runeChecker.initVerify(senderPublicKey); runeChecker.update(message.getBytes()); return runeChecker.verify(signature); } }
После: подпись с помощью постквантового ML-DSA
Словно оставляешь свой след в вечных залах Валинора:
/** * Sender signs message with private key */ class GandalfTheSender { private final KeyPair keyPair; public GandalfTheSender() throws Exception { KeyPairGenerator generator = KeyPairGenerator.getInstance("ML-DSA"); keyPair = generator.generateKeyPair(); } public PublicKey revealPublicRune() { return keyPair.getPublic(); } public byte[] signMessage(String message) throws Exception { Signature runeEngraver = Signature.getInstance("ML-DSA"); runeEngraver.initSign(keyPair.getPrivate()); runeEngraver.update(message.getBytes()); return runeEngraver.sign(); } public String speakWordsOfPower() { return """ It is not our part here to take thought only for a season, or for a few lives of Men, \ or for a passing age of the world. We should seek a final end of this menace, \ even if we do not hope to make one. """; } }
/** * Receiver verifies the message with public key */ class AragornTheReceiver { private final PublicKey senderPublicKey; public AragornTheReceiver(PublicKey senderPublicKey) { this.senderPublicKey = senderPublicKey; } public boolean verifyMessage(String message, byte[] signature) throws Exception { Signature runeChecker = Signature.getInstance("ML-DSA"); runeChecker.initVerify(senderPublicKey); runeChecker.update(message.getBytes()); return runeChecker.verify(signature); } }
Key Derivation Function API
Эта фича представляет стандартизированный API для функций формирования ключей (KDF), заменяя разрозненные реализации единым интерфейсом для безопасного получения ключей.
До: ручное получение ключей
Как выковать клинок в грубых кузницах Мордора:
String password = "Mellon!"; byte[] salt = generateSalt(); int iterations = 65_536; int keyLengthBits = 256; // derive the key PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, keyLengthBits); SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); SecretKey key = factory.generateSecret(spec); System.out.println("Derived key: " + HexFormat.of().formatHex(key.getEncoded()));
После: API для генерации ключей
Теперь, подобно кузнецам Эрегиона, мы создаём ключи с высшей точностью:
KDF hkdf = KDF.getInstance("HKDF-SHA256"); byte[] password = "Mellon!".getBytes(); byte[] salt = generateSalt(); byte[] info = "Say 'Friend' and enter".getBytes(); int keyLengthBytes = 32; // derive the key AlgorithmParameterSpec params = HKDFParameterSpec.ofExtract() .addIKM(password) .addSalt(salt) .thenExpand(info, keyLengthBytes); SecretKey key = hkdf.deriveKey("AES", params); System.out.println("Derived key: " + HexFormat.of().formatHex(key.getEncoded()));
Foreign Function & Memory API
Заменяет Java Native Interface (JNI) более безопасным и эффективным способом вызова нативного кода и работы с off-heap памятью.
Для простоты я опущу названия пакетов и длинные пути к файлам. Полностью рабочий пример доступен на GitHub.
До: опасный подход через JNI
Словно Чёрная Речь Мордора — могущественно, но опасно...
Создаём класс ElvenScroll:
public class ElvenScroll { static { // Load the native library forged in C System.loadLibrary("mordor"); } // Declare the native spell that reads the length of ancient text public native int countRunes(String ancientText); public static void main() { ElvenScroll scroll = new ElvenScroll(); String runes = "Speak, friend, and enter"; int length = scroll.countRunes(runes); System.out.println("Runes counted: " + length); } }
Компилируем класс ElvenScroll с опцией -h:
javac -h . ElvenScroll.java
На выходе получается заголовочный файл ElvenScroll.h.
Создаём C-файл:
#include <jni.h> #include <string.h> #include "ElvenScroll.h" JNIEXPORT jint JNICALL Java_ElvenScroll_countRunes(JNIEnv *env, jobject obj, jstring ancientText) { // Convert Elvish runes to C-compatible form const char *runes = (*env)->GetStringUTFChars(env, ancientText, NULL); if (runes == NULL) return 0; // Count the runes int length = (int)strlen(runes); // Release the spellbound memory (*env)->ReleaseStringUTFChars(env, ancientText, runes); return length; }
Собираем C-библиотеку (ко��анда ниже — для macOS):
gcc -shared -fpic -o libmordor.dylib \ -I ${JAVA_HOME}/include \ -I ${JAVA_HOME}/include/darwin \ mordor.c
На выходе получается файл libmordor.dylib.
Теперь запускаем класс ElvenScroll.
После: безопасный FFM API
Словно эльфийские мосты Лотлориэна — изящно и надёжно:
public class ElvenScroll { public static void main() throws Throwable { // Get a linker and a lookup object Linker linker = Linker.nativeLinker(); SymbolLookup stdlib = linker.defaultLookup(); // Get a handle to the foreign function ('strlen' from the C standard library) MethodHandle strlen = linker.downcallHandle( stdlib.find("strlen").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS) ); try (Arena arena = Arena.ofConfined()) { // Allocate off-heap memory String runes = "Speak, friend, and enter"; MemorySegment cString = arena.allocateFrom(runes); // Call the foreign function long runeCount = (long) strlen.invoke(cString); System.out.println("Runes counted: " + runeCount); } // Memory automatically freed here - no memory leaks! // No native code! } }
Compact Source Files And Instance Main Methods
Упрощает синтаксис Java для начинающих, позволяя создавать однофайловые программы без шаблонного кода класса и предлагая более гибкие варианты объявления метода main().
До: многословное заклинание
Словно пространные и мудрёные речи Совета Элронда:
package dev.pfilaretov42.java25.csf_imm; public class RingQuestBefore { public static void main(String[] args) { System.out.println("One does not simply walk into Mordor..."); } }
После: версия размером с хоббита
Словно короткое путешествие Бильбо:
// no package declaration // no imports for classes in java.base module void main() { IO.println("One does not simply walk into Mordor..."); }
Или с использованием instance-метода:
package dev.pfilaretov42.java25.csf_imm; public class RingQuestAfterInstance { void main() { IO.println("One does not simply walk into Mordor..."); } }
Markdown Documentation Comments
Наконец-то! 🎉 В Java появилась поддержка синтаксиса Markdown в комментариях Javadoc, что обеспечивает более удобочитаемую и поддерживаемую альтернативу HTML-тегам.
До: HTML-способ
Путь древних — громоздкий и тяжёлый, словно кузницы Изенгарда:
/** * <h1>The One Ring</h1> * <p>Forged by Sauron in the fires of Mount Doom.</p> * <pre>{@code * if (ring.isFound()) { * frodo.destroy(ring); * } * }</pre> * <p><b>Warning:</b> Do not wear for extended periods.</p> */ public class OneRing { // ... }
После: синтаксис Markdown
Теперь слова льются, словно эльфийская письменность Лотлориэна — аккуратно, красиво и точно:
/// # The One Ring /// Forged by Sauron in the fires of Mount Doom. /// ```java /// if (ring.isFound()) { /// frodo.destroy(ring); /// } /// ``` /// **Warning:** Do not wear for extended periods. public class OneRing { // ... }
Module Import Declarations
Вводит синтаксис import module, позволяющий разработчикам импортировать целые модули в обычных Java-файлах.
Используя следующие классы...
package lothlorien; public class Galadriel { public void speakLightOfEärendil() { // ... } }
package lothlorien; public class Celeborn { public void offerCounsel() { // ... } }
package lothlorien; public class Haldir { public void escort() { // ... } }
...и файл module-info.java...
module elves.of.lothlorien { exports lothlorien; }
...вот что мы имеем.
До: объявления import
Словно бесконечные родословные Дома Финвэ:
import lothlorien.Celeborn; import lothlorien.Galadriel; import lothlorien.Haldir; public class FarewellToLórien { void main() { Haldir guardian = new Haldir(); guardian.escort(); Celeborn lord = new Celeborn(); lord.offerCounsel(); Galadriel lady = new Galadriel(); lady.speakLightOfEärendil(); } }
После: импорт модулей
Теперь всё организовано, словно в гномьем дворце:
import module elves.of.lothlorien; public class FarewellToLórien { void main() { Haldir guardian = new Haldir(); guardian.escort(); Celeborn lord = new Celeborn(); lord.offerCounsel(); Galadriel lady = new Galadriel(); lady.speakLightOfEärendil(); } }
Заключение
Всё ещё думаете, что за 30 лет Java устарела и стала скучной? Тогда взгляните на последнюю LTS-версию Java 25 — она приносит с собой множество классных фич!
