The best new features in Java 25
Java continues its fast and feature-packed release schedule, with JDK 25 delivering syntax improvements, simplifications, and performance enhancements. This article looks at the core updates in Java 25, with code and usage examples. JDK 25 is now the most current long-term support (LTS) release, and it’s packed with new features to explore.
Simpler source files and instance main methods
Over the years, Java has consistently moved away from its verbose syntax and toward greater simplicity. A longstanding hurdle for Java beginners was the verbosity of defining even a simple “Hello World” program:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, InfoWorld!");
}
}
A beginner had to either ignore mysterious keywords like public
, class
, and static
or learn about visibility, classes, and static members before ever touching on Java syntax basics.
Java 25 addresses this issue with JEP 512: Compact source files and instance main methods, which lets us write the same program as:
void main() {
IO.println("Hello, World!");
}
The most obvious thing to notice is that all the OOP syntax is gone. Of course, you can add this code back in as needed. But also notice the IO.println()
call. The basic IO libraries were moved into the Java IO package, which is part of java.lang
and does not require an explicit import. This addresses another longstanding gripe for Java developers, of how much code was required simply to output to the console.
New flexible constructors
Another win for flexibility over formality, JEP 513: Flexible constructor bodies lands in Java 25, following a long incubation period. Flexible constructors, as the name suggests, make constructors less rigid. Specifically, Java no longer requires calling super()
or this()
as the first thing that happens in a constructor.
Previously, even if you did not explicitly call super()
or this()
, the compiler would do it for you. And, if you tried calling them elsewhere, it would cause an error. All of this is now more flexible.
There still are some rules that must be observed, but it’s now possible to run initialization code on the class itself before calling super()
. This avoids situations where you might have to do additional work because the subclass initialization obviates the superclass initialization. (For more about this, look at the Person->Employee example for JEP 513.)
To fully understand the benefits of flexible constructors, consider an example where you have a Shap
e superclass, and you want the constructor to accept an area:
class Shape {
final int area;
public Shape(int area) {
if (area <= 0) throw new IllegalArgumentException("Area must be positive.");
this.area = area;
}
}
Now say you want to have a Rectangle
class. In Java before JDK 25, you’d have to somehow extract the calculation to use it in the super()
call, usually using a static method:
// The old way
class Rectangle extends Shape {
private static int checkAndCalcArea(int w, int h) {
if (w <= 0 || h <= 0) {
throw new IllegalArgumentException("Dimensions must be positive.");
}
return w * h;
}
public Rectangle(int width, int height) {
super(checkAndCalcArea(width, height)); // super() had to be first
// ... constructor logic ...
}
}
This code is quite clunky. But in Java 25, it’s easier to follow your intention, and run the area calculation in the Rectangle
constructor:
class Rectangle extends Shape {
final int width;
final int height;
public Rectangle(int width, int height) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Dimensions must be positive.");
}
int area = width * height;
super(area); // Before 25, this was an error
this.width = width;
this.height = height;
}
}
Wholesale module imports
Another feature finalized in JDK 25, JEP 511: Module import declarations, lets you import an entire module instead of having to import each package one by one.
Although there are some nuances (like the need to explicitly resolve package name collisions), the basic idea is that instead of having to import every package in a module, you can just import the entire module with all its packages included. This example from the JEP shows the old way of importing the contents of a module:
import java.util.Map; // or import java.util.*;
import java.util.function.Function; // or import java.util.function.*;
import java.util.stream.Collectors; // or import java.util.stream.*;
import java.util.stream.Stream; // (can be removed)
String[] fruits = new String[] { "apple", "berry", "citrus" };
Map m =
Stream.of(fruits)
.collect(Collectors.toMap(s -> s.toUpperCase().substring(0,1),
Function.identity()));
Now you can just type:
import java.base; // Imports the module containing the required packages
Scoped values
Scoped values are a convenient and efficient alternative to thread-local variables, especially for use with virtual threads and structured concurrency.
A common example of using thread local variables is in web apps, where you want to put data into memory for only the specific thread. For example, you might recover the user data and place it in memory to be used by whatever business logic is executed along the way.
The drawbacks of thread local variables become more evident with virtual threads. Apps using virtual threads may spawn hundreds or even thousands of concurrent virtual threads, and the parent thread will share the thread local variable with all of them. Thread locals also live for the entire life of the thread and are fully mutable.
Scoped values in JDK 25 (JEP 506) are immutable and only live for the life of the calling method. That means they can be efficiently shared among any number of virtual threads, and it is simple for developers to define their lifespan.
Scoped values let you declare the shared value and pass in a function that initiates all subsequent work:
ScopedValue.where(NAME, ).run(() -> {
//... NAME.get() ... call methods using NAME ...
});
In this case, NAME
itself is the scoped value:
static final ScopedValue<...> NAME = ScopedValue.newInstance();
Primitive types in patterns
Still in preview in Java 25, JEP 507 lets us use primitives like int
and double
in patterns, instanceof
, and switch
. Although this looks like a minor improvement, it’s another step toward eventually uniting primitives and objects in Project Valhalla.
Compact object headers
Compact object headers (JEP 519) improve memory performance in the JVM. When you update to Java 25, you get this feature for free, without doing anything extra. It reduces memory overhead, which for some applications can result in a significant gain.
Object headers store the information the JVM uses to describe an object’s metadata in memory. This change uses clever coding to compress object headers into a smaller footprint. The gain in memory efficiency also reduces the frequency of garbage collection.
Generational garbage collection
Although the G1 collector remains the standard, the Shenandoah option is popular for low-latency demands in server applications. It avoids potential lags during garbage collection and was recently updated to include generational garbage collection. This feature is now standard when using Shenandoah.
While most heap objects are allocated and disposed of quickly, long-lived objects are less common. Generational garbage collection takes advantage of that fact by allocating memory in the heap, then sweeping it after a short time to dispose of unused objects. It then promotes long-lived objects into the longer-term collection space.
As a rule of thumb, when you need maximum throughput (that is, overall performance) stick with the G1 default. If you need minimum latency, meaning the fewest possible pauses for garbage collection, consider Shenandoah.
To use Shenandoah, include the following switch when running Java:
java -XX:+UseShenandoahGC
Conclusion
When I first started using Java, it was an up-and-coming language that I appreciated for its power, especially in web application development. Although I dabble in JVM alternatives like Kotlin and Clojure, I still have a deep affection for Java. It’s nice to see the Java team’s commitment to keeping Java relevant, even when it means refactoring core parts of the language.
Original Link:https://www.infoworld.com/article/4075736/7-key-features-in-java-25.html
Originally Posted: Wed, 22 Oct 2025 09:00:00 +0000
What do you think?
It is nice to know your opinion. Leave a comment.