Intro
In one of my previous posts, I wrote about features between Java LTS versions 21 and 17. Now, two years later (how!? Two years already!?), a new LTS version is scheduled for release on September 16, 2025 — Java 25. Today, it has moved into the "Final Release Candidate" phase, and the feature set is frozen.
The vast majority of projects skip non-LTS releases and use Java LTS versions only. So, let's take a look at the features that the new LTS version (Java 25) delivers compared to the previous LTS version (Java 21).
I will be using the following abbreviations for the feature states in the tables below:
- 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 Features in 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) | + |
Region Pinning for G1 | - | JEP 423 (prod) | + | + | + |
Late Barrier Expansion for G1 | - | - | - | JEP 475 (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) | + | + | + |
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 Features in 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 Features in Java 25
Feature | Java 21 | Java 22 | Java 23 | Java 24 | Java 25 |
---|---|---|---|---|---|
Vector API | JEP 448 (inc6) | JEP 460 (inc7) | JEP 469 (inc8) | JEP 489 (inc9) | JEP 508 (inc10) |
Experimental Features in Java 25
Feature | Java 21 | Java 22 | Java 23 | Java 24 | Java 25 |
---|---|---|---|---|---|
JFR CPU-Time Profiling | - | - | - | - | JEP 509 (exp) |
Withdrawn Features Between Java 21 and Java 25
Feature | Java 21 | Java 22 | Java 23 | Java 24 | Java 25 |
---|---|---|---|---|---|
String Templates | JEP 430 (pre) | JEP 459 (pre2) | JEP 465 (pre3 - w/d) | - | - |
Production-Ready Features with Examples
Now, let's briefly go through some production-ready features and look at code samples.
The full source code is available on GitHub: pfilaretov42/java-features.
Scoped Values
Scoped Values provide a safer alternative to thread-local variables by offering immutable, inheritable values that are only accessible within a defined scope.
Before: The ThreadLocal Approach
Like carrying the One Ring - dangerous if shared carelessly:
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();
}
}
After: The ScopedValue Way
Like the Phial of Galadriel - light that's safely contained:
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:
//currentRingBearer.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
Allows more flexible ordering of statements in constructor bodies, including permitting code before explicit constructor invocations (this()
or super()
calls).
Before: The Rigid Constructor Rules
Like the rigid walls of Orthanc - no flexibility:
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;
}
}
After: The Graceful Constructor Freedom
Now flows like the waters of Bruinen - natural and flexible:
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
Allows developers to explicitly mark unused variables/patterns with underscore _
to improve code clarity and maintainability.
Common Use Cases:
- Exception parameters
- Lambda parameters
- Pattern matching variables
- Loop variables when only counter is needed
Before: Forced to Name Unused Variables
Like being forced to name every orc in Mordor's army:
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);
}
After: Clean Unused Variable Declaration
As elegant as Legolas defying gravity:
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
Introduces custom intermediate stream operations through the new gather(Gatherer)
method, allowing more complex stream transformations.
Before: Limited to Built-in Operations
Like trying to forge the One Ring with only basic tools:
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"]
After: Custom Stream Transformations
Now wielding the power of the Elven smiths:
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 introduces built-in support for module-lattice-based key encapsulation mechanism (ML-KEM), part of quantum-resistant cryptography.
This feature provides quantum-resistant key encapsulation that is used to secure symmetric keys over insecure communication channels using public key cryptography.
It can be used to withstand the might of quantum computers — future threats to classical RSA and Diffie-Hellman.
With the following session key exchange example...
// 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
// ...
}
..., here is what we have.
Before: RSA Key Transport
Like wielding Gondorian steel — mighty, yet vulnerable to greater powers:
/**
* 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;
}
}
After: Post-Quantum Key Encapsulation
Like the Gates of Moria - impregnable to force:
/**
* 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 provides post-quantum secure digital signatures based on lattice cryptography, part of Java's new cryptographic suite for the quantum computing era.
With the following message signing example...
// 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
..., here is what we have.
Before: Signing with ECDSA
Like the walls of Isengard against Entish wrath:
/**
* 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);
}
}
After: Signing with Quantum-Resistant ML-DSA
Like inscribing your mark in the timeless halls of Valinor.
/**
* 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
Introduces a standardised API for key derivation functions (KDFs), replacing ad-hoc implementations with a unified interface for secure key derivation.
Before: The Manual Key Derivation
Like forging a sword in the crude fires of Mordor:
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()));
After: The Refined KDF API
Now, like the Elven smiths of Eregion, we forge keys with precision:
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
Replaces Java Native Interface (JNI) with a safer, more efficient way to call native code and work with off-heap memory.
I will skip package names and long file paths for simplicity. The fully working sample can be found on GitHub.
Before: The Risky JNI Approach
Like the Black Speech of Mordor - powerful but dangerous...
Create a Java class:
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);
}
}
Compile the ElvenScroll
class with -h
option:
javac -h . ElvenScroll.java
It produces the ElvenScroll.h
header file.
Create a C file:
#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;
}
Build the C library - the command below is for macOS:
gcc -shared -fpic -o libmordor.dylib \
-I ${JAVA_HOME}/include \
-I ${JAVA_HOME}/include/darwin \
mordor.c
This produces libmordor.dylib
file.
Run the ElvenScroll
class.
After: The Safe FFM API
Like the Elven bridges of Lothlórien - elegant and secure:
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
Simplifies Java syntax for beginners by allowing single-file programs without class boilerplate and offering more flexible main()
method declarations.
Before: The Verbose Incantation
Like the long-winded speeches of the Council of Elrond:
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...");
}
}
After: The Hobbit-Sized Version
A swift journey like Bilbo's contract:
// no package declaration
// no imports for classes in java.base module
void main() {
IO.println("One does not simply walk into Mordor...");
}
Or with an instance method:
package dev.pfilaretov42.java25.csf_imm;
public class RingQuestAfterInstance {
void main() {
IO.println("One does not simply walk into Mordor...");
}
}
Markdown Documentation Comments
Finally! 🎉 Java now supports Markdown syntax in Javadoc comments, providing a more readable and maintainable alternative to HTML tags.
Before: The HTML Way
The way of the ancients — cluttered and burdensome, like the forges of Isengard:
/**
* <h1>The One Ring</h1>
* <p>Forged by Sauron in the fires of Mount Doom.</p>
* <p><b>Warning:</b> Do not wear for extended periods.</p>
*/
public class OneRing {
// ...
}
After: Markdown Syntax
Now, the words flow like the elven script of Lothlórien — clean, elegant, and true:
/// # The One Ring
/// Forged by Sauron in the fires of Mount Doom.
/// **Warning:** Do not wear for extended periods.
public class OneRing {
// ...
}
Module Import Declarations
Introduces the import module
syntax, allowing developers to import entire modules within regular Java files.
With the following classes...
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() {
// ...
}
}
...and the module-info.java
file...
module elves.of.lothlorien {
exports lothlorien;
}
..., here is what we have.
Before: Import Declarations
Like the endless genealogies of the House of Finwë:
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();
}
}
After: Module Import
Now organized like a well-ordered dwarven hall:
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();
}
}
Conclusion
Hey, who said Java is old and boring? The latest Java 25 LTS version delivers some great features that are definitely worth checking out.
Dream your code, code your dream.
Top comments (0)