Java 25 RC1 Builds: My First Impressions and Highlights
Dive into my first impressions of Java 25 RC1! I explore key highlights like JEP 455, flexible main methods, string templates, and what they mean for developers.
Daniel Ivanov
Principal Software Engineer with over 15 years of experience building scalable Java systems.
The Java release train stops for no one, and that's a good thing! Just as we've settled in with the fantastic features of the last few LTS and non-LTS releases, the future is already knocking. The first Release Candidate (RC1) builds for Java 25 are now available, and as a long-time Java developer, I couldn't resist taking them for a spin.
An RC build is a significant milestone. It means the feature set for the upcoming release is frozen. What we see now is, barring any major last-minute bugs, what we'll get in the general availability (GA) release in September 2024. It’s the perfect time to get a real feel for how these new features will impact our day-to-day coding.
So, I fired up my IDE, grabbed the latest build, and spent some time with the most promising new JEPs (JDK Enhancement Proposals). Here are my first impressions and highlights.
Getting Started with Java 25
Before we dive in, you might be wondering how to try this yourself. It’s surprisingly easy! The early-access builds, including the new RC1, are available on jdk.java.net.
I personally manage my Java versions using SDKMAN!, which makes switching between JDKs a breeze. A simple command like sdk install java 25-rc.1-open
(the exact identifier might vary) is all it takes. Once installed, you can set it as your default or use it in a specific terminal session. Now, let’s get to the good stuff.
Highlight 1: Primitive Types in Patterns (JEP 455)
Pattern Matching has been one of the most transformative features added to Java in recent years. It has systematically chipped away at boilerplate code, making our `if-else` chains and `switch` statements more expressive and safe. JEP 455 is the next logical step in this evolution, finally extending full pattern matching support to primitive types.
What It Solves
Previously, pattern matching worked beautifully with reference types, but there was an awkward asymmetry when dealing with primitives. If you had an Object
that might be an Integer
, you could match on it. But what if it was an int
? The language forced you to think about wrapper types, leading to slightly clunky code when dealing with generic contexts.
With JEP 455, you can now use type patterns with all eight primitive types. This makes the language more consistent and intuitive.
Code in Action
Consider a method that processes a numeric type stored as an Object
. Before, you’d have to handle the wrapper types explicitly.
// Pre-Java 25
void processNumber(Object obj) {
switch (obj) {
case Integer i -> System.out.println("It's an integer: " + i);
case Double d -> System.out.println("It's a double: " + d);
// No direct way to match a primitive int if it got boxed into obj
default -> System.out.println("Not a recognized number type.");
}
}
Now, you can match directly against the primitive type pattern. This is a subtle but powerful change that smooths out the interaction between primitives and generics/objects.
// With Java 25
void processNumber(Object obj) {
switch (obj) {
case int i -> System.out.println("It's an int: " + i);
case double d -> System.out.println("It's a double: " + d);
case long l -> System.out.println("It's a long: " + l);
default -> System.out.println("Not a recognized number type.");
}
}
This JEP is a classic example of Java’s careful, incremental language improvement. It doesn’t scream for attention, but it refines the language, removes a sharp edge, and makes our code just a little bit cleaner.
Highlight 2: Flexible Main Methods and Anonymous Main Classes (JEP 463)
This one is a game-changer, especially for newcomers to Java and for writing simple scripts. For decades, the entry point to any Java program has been the rigid and verbose incantation: public static void main(String[] args)
.
Lowering the Barrier to Entry
Explaining `public`, `static`, `void`, and `String[] args` to someone writing their first "Hello, World!" is a rite of passage for every Java teacher—and a confusing hurdle for every student. JEP 463 beautifully addresses this by introducing more flexible and concise ways to declare a program's entry point.
You can now simply write:
void main() {
System.out.println("Hello, Java 25!");
}
That's it! You can save this in a file called `HelloWorld.java` and run it directly from your terminal with java HelloWorld.java
. The compiler will infer the class and the necessary modifiers for the main method. You can still include `String[] args` if you need command-line arguments, and you can add `static` if you wish, but it's no longer mandatory for simple programs.
This feature, a follow-up to 'Unnamed Classes and Instance Main Methods' from Java 21, makes Java far more approachable for education and for quick, single-file utilities. It's a huge win for developer experience.
Highlight 3: String Templates (Second Preview - JEP 459)
String manipulation in Java has always been functional but often clumsy. We've relied on `+` concatenation, `StringBuilder`, `String.format()`, or `MessageFormat`. String Templates, now in their second preview, offer a much more elegant and safe alternative.
The core idea is to allow string literals to contain embedded expressions, which are evaluated and interpolated at runtime. The default `STR` template processor is the most common one we'll use:
String name = "Daniel";
int javaVersion = 25;
// Old way
String greetingOld = "Hello, my name is " + name + " and I'm exploring Java " + javaVersion + ".";
// New way with String Templates
String greetingNew = STR."Hello, my name is \{name} and I'm exploring Java \{javaVersion}.";
The syntax is clean and immediately familiar to anyone who has used string interpolation in languages like Python, Kotlin, or JavaScript. What's particularly "Java" about this feature is its emphasis on safety and extensibility. The `STR.` part isn't just syntax; it’s a call to a template processor. This means you can create your own processors. For example, a hypothetical `JSON` processor could automatically escape values to safely construct a JSON string, preventing injection vulnerabilities.
As this is the second preview, the feature is maturing nicely. It's one of those quality-of-life improvements that, once you start using it, you'll wonder how you ever lived without it.
Highlight 4: Scoped Values (Second Preview - JEP 464)
This is a more advanced feature, but it's critically important for the future of concurrency in Java, especially in the era of Project Loom and virtual threads.
Scoped Values are designed as a modern replacement for `ThreadLocal` variables. Thread-local variables have been a common way to pass data implicitly through a call stack (e.g., user credentials, transaction IDs) without polluting every method signature. However, they have significant downsides:
- They are mutable, which can lead to hard-to-debug issues.
- They can cause memory leaks if not properly removed.
- With virtual threads, their cost is magnified, as they can lead to a large memory footprint.
Scoped Values solve these problems elegantly. A `ScopedValue` is immutable and allows you to share data for a bounded period of execution (a "scope"), typically within a specific task being run on a thread.
A Conceptual Look
Imagine you have a web request and you want to make a user's context available to all downstream services. Instead of passing a `UserContext` object everywhere, you can use a `ScopedValue`.
// Define a ScopedValue, typically as a static final field
public static final ScopedValue<UserContext> USER_CONTEXT = ScopedValue.newInstance();
// In your request handler
UserContext currentUser = ...; // Get user from request
ScopedValue.where(USER_CONTEXT, currentUser).run(() -> {
// All code executed here, including calls to other methods,
// can now access the user context.
processOrder();
});
// In a downstream method
void processOrder() {
// No need to pass UserContext as a parameter!
if (USER_CONTEXT.isBound()) {
UserContext user = USER_CONTEXT.get();
// Use the context...
}
}
This approach is much safer, more efficient for virtual threads, and easier to reason about. It's a foundational piece for building robust, scalable, and modern concurrent applications on the JVM.
My Final Thoughts
After playing with Java 25 RC1, I'm incredibly optimistic. The release continues the trend of focusing heavily on developer experience while simultaneously pushing the platform's performance and concurrency models forward.
Features like Flexible Main Methods and String Templates will make Java more welcoming and productive for everyday tasks. Under the hood, the refinement of Pattern Matching and the maturation of Scoped Values show a deep commitment to modernizing the language and its core libraries for the challenges of tomorrow.
Java 25 is shaping up to be another solid, valuable release. I encourage you to download the RC1 build, try out these features in a side project, and see for yourself. The future of Java is bright!