Most Useful Modern Java Features
A curated guide to practical latest Java features from Java 21 onward, with examples.

Audio controls for Most Useful Modern Java Features
Listen to this article
Java evolves with every release, introducing features that can make code cleaner, simplify development, and improve application performance. But which changes are actually worth learning?
This guide highlights practical Java features worth knowing, with concise examples showing what changed, why it matters, and where each feature can be useful. It's a curated selection focused on real-world value rather than an exhaustive catalogue of language and JVM enhancements.
The features are organised from newest to oldest, making it easy to explore recent improvements or revisit earlier releases. Each example targets the Java release identified in its section, with preview features distinguished from finalised features where relevant.
Java 27
1. Compact Object Headers by Default — JEP 534
The benefit: Reduce JVM memory overhead in applications that create large numbers of objects.
Consider an application that stores millions of customers:
class Customer {
String name;
int age;
Customer(String name, int age) {
this.name = name;
this.age = age;
}
}Creating a customer also requires the JVM to maintain metadata about the object, including its class information. This metadata is stored in an object header, which consumes memory even though you don't declare it in your Java code.
On a typical 64-bit HotSpot configuration, the traditional header occupies 12 bytes. Compact Object Headers reduce it to 8 bytes.
Imagine an application creating one million customer objects. A four-byte reduction per object would represent approximately 4 MB of potential memory savings across those headers alone.
The actual total memory saving depends on object layout, alignment and JVM configuration. Compact headers may also improve performance in some workloads, but gains aren't guaranteed.
What changes in your code? Nothing in the typical case. Java 27 enables compact headers by default in HotSpot, allowing eligible applications to benefit without rewriting their classes.
Reference: JEP 534 — Compact Object Headers by Default
Java 26
2. HTTP/3 Support in HTTP Client API — JEP 517
The benefit: Use HTTP/3 for Java HTTP requests without adopting a separate HTTP client library.
Previously, the built-in HttpClient supported HTTP/1.1 and HTTP/2. Java 26 adds HTTP/3, which uses QUIC instead of TCP and can improve connection setup and performance on some networks.
var client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_3)
.build();
var request = HttpRequest.newBuilder(
URI.create("https://example.com"))
.GET()
.build();
var response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);The request and response handling remain familiar. The key change is selecting HTTP_3 as the client's preferred protocol version.
One thing to know: This expresses a preference, not a guarantee. The server, network, and client configuration determine whether HTTP/3 is actually used; the client can fall back to HTTP/2 or HTTP/1.1 when appropriate.
Reference: JEP 517 — HTTP/3 for the HTTP Client API
Java 25
3. Compact Source Files and Instance main() Methods — JEP 512
Java 25 finalizes compact source files and instance main() methods. A compact source file omits an explicit class declaration; the compiler implicitly declares a class around its members. In compact source files, public types from packages exported by java.base are implicitly imported. java.lang.IO is available for simple input and output:
void main() {
IO.println("Hello from a compact source file");
}Save this as Hello.java and run it with java Hello.java on JDK 25. The same feature allows an ordinary class to use an instance entry method:
class Greeting {
private final String message = "Hello from an instance main";
void main() {
System.out.println(message);
}
}The launcher creates an instance using an accessible no-argument constructor when it invokes an instance main. Java 25 accepts several entry-method forms. Methods with a String[] parameter take precedence over no-argument methods; describing the resolution as only three forms or saying the JVM always creates an instance is inaccurate. Compact source files and instance entry methods are finalized in Java 25 and need no preview flag.
Reference: JEP 512.
4. Flexible Constructor Bodies — JEP 513
Flexible constructor bodies allow a restricted prologue before an explicit super(...) or this(...) invocation. This is useful when validating parameters before passing them to a superclass. The prologue runs in an early-construction context: it cannot use the instance under construction as though superclass construction had completed.
class Person {
Person(int age) {
if (age < 0) {
throw new IllegalArgumentException("age must not be negative");
}
}
}
class Employee extends Person {
Employee(int age) {
if (age < 18) {
throw new IllegalArgumentException("employee must be an adult");
}
super(age);
}
}This feature is final in Java 25. It permits useful work before constructor invocation while retaining restrictions that prevent unsafe access to the not-yet-constructed object.
Reference: JEP 513.
5. Module Import Declarations — JEP 511
import module M; imports public top-level types on demand from packages that module M exports to the current module, including relevant transitive exports. It can be used even when the importing source file is not itself part of a named module.
import module java.base;
class App {
List<String> names = List.of("Ada", "Grace");
}This is convenient when a source file uses types from many exported packages. It does not import every type in every package or make inaccessible packages public. Importing several modules can make a simple name ambiguous; resolve that with a specific type import or a qualified name. Module import declarations are final in Java 25.
Reference: JEP 511.
Java 24
6. Stream Gatherers — JEP 485
The benefit: Create custom stream operations when map(), filter(), and other standard methods aren't enough.
For example, analysing rolling metrics often requires grouping consecutive values into overlapping windows. Java 24 makes this straightforward:
var windows = Stream.of(10, 20, 30, 40, 50)
.gather(Gatherers.windowSliding(3))
.toList();
System.out.println(windows);
// [[10, 20, 30], [20, 30, 40], [30, 40, 50]]Each window contains three consecutive values, advancing by one value at a time.
Java also provides windowFixed(n) for non-overlapping batches and mapConcurrent(n, ...) for concurrent transformations with a limit on concurrency.
When to use it: For rolling metrics, batching, and specialised stream transformations that would otherwise require custom loops or extra state-management code.
Reference: JEP 485 — Stream Gatherers
Java 23
7. Markdown Documentation Comments — JEP 467
The benefit: Write readable API documentation without mixing ordinary prose with HTML tags.
Traditionally, JavaDoc often uses HTML for formatting:
/**
* Returns the customer's name.
*
* <p><b>Example:</b> {@code getName()}
*
* @return the customer name
*/
String getName() {
return name;
}Java 23 introduces Markdown-style documentation comments using ///:
/// Returns the customer's name.
///
/// **Example:** `getName()`
///
/// @return the customer name
String getName() {
return name;
}You can use familiar Markdown formatting, including headings, lists, links, and code formatting. The standard javadoc tool converts these comments into generated API documentation.
Where it helps: Open-source libraries, internal SDKs, and projects where developers frequently read or update JavaDoc.
Important: Existing /** ... */ JavaDoc comments remain supported. You can adopt Markdown documentation incrementally rather than rewriting an entire codebase.
Takeaway: If your team maintains JavaDoc, Markdown comments can make documentation easier to write, review, and maintain.
Reference: JEP 467 — Markdown Documentation Comments
Java 22
8. Unnamed Variables and Patterns — JEP 456
The benefit: Stop naming variables you never intend to use.
Sometimes, Java requires a variable even when its value doesn't matter. For example, you might want to count items without using each item:
int count = 0;
for (String _ : names) {
count++;
}The _ tells readers that the value is intentionally ignored.
Unnamed patterns are useful when matching a record but only one of its components matters:
if (value instanceof Order(String name, _)) {
System.out.println("Customer: " + name);
}Here, the order's second component is deliberately ignored. The example assumes Order is a record whose components are a String and another value.
Where it helps: Loops, exception handlers, lambda parameters, and record patterns where some values are irrelevant.
Takeaway: Use _ to make intentional omissions clear and remove distracting variable names.
Reference: JEP 456 — Unnamed Variables & Patterns
9. Launch Multi-File Source-Code Programs — JEP 458
The benefit: Run a Java program spread across multiple source files without manually compiling it first.
Previously, Java's source-file launch mode was limited to a single source file. Java 22 extends this capability to programs that depend on other Java source files.
Imagine a simple project:
demo/
├── Main.java
└── Greeting.javaGreeting.java:
class Greeting {
static String message() {
return "Hello from Java 22!";
}
}Main.java:
class Main {
public static void main(String[] args) {
System.out.println(Greeting.message());
}
}Run the program from the project directory:
java Main.javaThe Java launcher locates and compiles the required source files automatically.
Where it helps: Small utilities, coding experiments, demonstrations, and learning projects where setting up a build tool would be unnecessary overhead.
Takeaway: Use multi-file source launch when you want to move beyond a single-file example without immediately setting up a full build.
Reference: JEP 458 — Launch Multi-File Source-Code Programs
Java 21
Java 21 introduced several features that make everyday Java development more expressive and scalable. These three are especially worth knowing: cleaner type-based branching, easier access to structured data, and lightweight concurrency.
10. Pattern Matching for switch — JEP 441
The benefit: Handle different object types without repetitive type checks and casts.
Instead of writing:
if (value instanceof String) {
String text = (String) value;
return "Text: " + text;
}Java 21 lets you express the same logic more directly:
return switch (value) {
case String text -> "Text: " + text;
case Integer number -> "Number: " + number;
case null -> "No value";
default -> "Unsupported value";
};Each case checks the type and makes the matched value available through a variable. The switch expression returns the result directly, while case null allows null to be handled explicitly.
Where it helps: Processing different event types, API values, or implementations of a sealed interface.
Takeaway: Use pattern matching when branching across several types makes ordinary conditionals difficult to read.
Reference: JEP 441 — Pattern Matching for switch
11. Record Patterns — JEP 440
The benefit: Extract nested record data without repeatedly calling accessors or creating intermediate variables.
Consider an order containing customer information:
record Customer(String name) {}
record Order(Customer customer, double total) {}Traditionally, you would retrieve the customer and then call customer.name() to access the nested value.
With record patterns, Java 21 lets you match the structure directly:
if (value instanceof Order(
Customer(String name),
double total)) {
System.out.println(name + ": " + total);
}Java checks the record structure and binds its components to name and total. If the value doesn't match the pattern, the body isn't executed.
Where it helps: Working with nested data-transfer objects, domain events, parser results, and structured business models.
Takeaway: Record patterns are most useful when you need to inspect several components of a record, particularly nested records.
Reference: JEP 440 — Record Patterns
12. Virtual Threads — JEP 444
The benefit: Handle many concurrent, I/O-heavy tasks without requiring a separate operating-system thread for every task.
A traditional thread pool might look like this:
var executor =
Executors.newFixedThreadPool(100);Java 21 offers a simpler alternative for workloads that spend much of their time waiting:
try (var executor =
Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> fetchOrder(101));
executor.submit(() -> fetchOrder(102));
}Each task runs in its own virtual thread. The Java runtime can suspend virtual threads during supported blocking operations, freeing carrier threads to do other work.
Why it matters: This can simplify backend services that make many blocking HTTP or database calls, without forcing every task into an asynchronous programming model.
Important: Virtual threads don't make CPU-intensive work faster or remove database connection limits. They improve the economics of concurrency, not the capacity of downstream systems.
Takeaway: Consider virtual threads for highly concurrent, I/O-bound applications. Measure the results under realistic workloads before adopting them.
Reference: JEP 444 — Virtual Threads
Quick reference
| No. | Java release | Feature |
|---|---|---|
| 1 | 27 | Compact Object Headers by Default (JEP 534) |
| 2 | 26 | HTTP/3 in the HTTP Client API (JEP 517) |
| 3–5 | 25 | Compact Source Files and Instance main() Methods; Flexible Constructor Bodies; Module Import Declarations |
| 6 | 24 | Stream Gatherers |
| 7 | 23 | Markdown Documentation Comments |
| 8–9 | 22 | Unnamed Variables and Patterns; Multi-file Source-Code Launch |
| 10–12 | 21 | Pattern Matching for switch; Record Patterns; Virtual Threads |
Did you find this article useful?