Java 25 is here, offering increased productivity through three features that are set to transform how we write Java applications. These include Compact Source Files and Instance Main Methods, which eliminate the boilerplate that has confused many Java beginners for decades. Now, Java developers can write streamlined, single-file programs. Flexible Constructor Bodies liberate constructors from the 30-year-old restriction of placing super() or this() first, enabling more natural initialization logic. Meanwhile, Advanced Pattern Matching brings a superpower to switch expressions, which in turn makes it a very elegant, data-oriented programming approach.
Let’s explore how each feature works and why it’s a game-changer for Java development.
Part 1: Compact Source Files—The End of “Hello, World!” Boilerplate
For thirty years, every Java programmer’s journey began with this daunting incantation:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
All we want to print is “Hello, World!” in the console. To accomplish that, we went through a lot. There is too much clutter here, too much code, too many concepts, and too many constructs for what the program does. Having taught for 2 years at a university, I often pained myself when introducing this to beginners. I often found myself saying, “Don’t worry about that now, you’ll understand it later,” which left students with the impression that Java is unnecessarily complex.
Java 25’s Compact Source Files sweep away this complexity:
void main() {
IO.println("Hello, World!");
}
That’s it. No class declaration. No public modifiers. No static keywords. No String[] args. Just the code that matters.
This isn’t a dumbed-down dialect of Java. It’s the whole language with smart defaults. The compiler implicitly creates a class, but you don’t need to think about it until you’re ready.
How Compact Sources Work
When the Java compiler encounters a source file with methods and fields not enclosed in a class declaration, it implicitly declares a class with these characteristics:
- It’s a final top-level class in the unnamed package
- It extends java.lang.Object
- It has a default no-argument constructor
- All unenclosed fields and methods become members of this class
- It must have a launchable main method
This feature enables you to start with minimal, script-like code and expand it into a full program without the usual boilerplate code.
void main() {
IO.println("Welcome to Java!");
}
Now save the file with a .java extension (e.g., Hello.java). We can just run it:
java Hello.java
Now, similarly, we can add additional code, such as a method like greeting(), and include it in the same file.
String greeting() {
return "Hello from Java 25!";
}
void main() {
IO.println(greeting());
}
We can now run it as we did earlier. However, that’s not the only option; we can declare variables and use them directly in a method.
String userName = "Developer";
int count = 0;
void greet() {
count++;
IO.println("Hello " + userName + "! (Visit #" + count + ")");
}
void main() {
greet();
greet();
}
The New IO Class – Console Interaction Made Simple
Java 25 introduces java.lang.IO, a utility class that makes console interaction straightforward:
void main() {
String name = IO.readln("Enter your name: ");
int age = Integer.parseInt(IO.readln("Enter your age: "));
IO.println("Hello " + name + "!");
if (age >= 18) {
IO.println("You're an adult.");
} else {
IO.println("You're a minor.");
}
}
The IO class provides five essential methods:
IO.print(Object obj)– Print without newlineIO.println(Object obj)– Print with newlineIO.println()– Print an empty lineIO.readln(String prompt)– Read a line with a promptIO.readln()– Read a line without a prompt
Automatic Imports – The java.base Module at Your Fingertips
Compact source files automatically import all public classes from the java.base module. This means instant access to essential APIs without explicit imports:
void main() {
// List from java.util - no import needed!
var languages = List.of("Java", "Python", "JavaScript", "Go");
// Stream API - available immediately
languages.stream()
.filter(lang -> lang.startsWith("J"))
.forEach(IO::println);
// Math utilities - ready to use
IO.println("Random number: " + Math.random());
// File I/O from java.nio - no import required
Path file = Path.of("data.txt");
if (Files.exists(file)) {
var lines = Files.readAllLines(file);
IO.println("File has " + lines.size() + " lines");
}
}
This eliminates the mystery of imports for beginners while providing convenience for experienced developers writing scripts and utilities.
Growing Your Program
The beauty of compact source files is seamless evolution. When your program outgrows its initial simplicity, wrapping it in a class is trivial:
// Before: Compact source file
void processData(String filename) throws IOException {
var data = Files.readAllLines(Path.of(filename));
data.stream()
.map(String::toUpperCase)
.forEach(IO::println);
}
void main(){
processData("input.txt");
}
// After: Grown into a full class
import module java.base; // Optional but explicit
class DataProcessor {
void processData(String filename) {
var data = Files.readAllLines(Path.of(filename));
data.stream()
.map(String::toUpperCase)
.forEach(IO::println);
}
void main() {
processData("input.txt");
}
}
The code inside doesn’t change; you just add the class wrapper when needed.
Part 2: Flexible Constructor Bodies – Breaking a 30-Year Constraint
For 30 years, Java developers have lived with an unusual rule: in every constructor, the call to super() or this() had to be the very first statement. This restriction often forced developers into awkward workarounds, creating static helper methods or initializing objects in unnatural places.
Consider this old-school example:
public class EmailService extends Service {
private final EmailConfig config;
public EmailService(Map<String, String> settings) {
super(validateAndPrepare(settings)); // Must go first! Awkward!
this.config = buildConfig(settings);
}
private static String validateAndPrepare(Map<String, String> settings) {
if (!settings.containsKey("smtp.host")) {
throw new IllegalArgumentException("SMTP host required");
}
return settings.get("service.name");
}
private static EmailConfig buildConfig(Map<String, String> settings) {
return new EmailConfig(/* ... */);
}
}
Notice how validateAndPrepare() and buildConfig() had to be static. They exist only because we couldn’t do this work inline before calling super(). The flow feels unnatural: we’re forced to break logic into scattered methods instead of writing it directly.
Enter Java 25: Prologue and Epilogue
Java 25 fixes this with prologue and epilogue support inside constructors. You can now write validation and preparation code before super() or this(), and perform extra initialization afterwards.
public class EmailService extends Service {
private final EmailConfig config;
public EmailService(Map<String, String> settings) {
// Prologue: validation and preparation
if (!settings.containsKey("smtp.host")) {
throw new IllegalArgumentException("SMTP host required");
}
var serviceName = settings.getOrDefault("service.name", "EmailService");
this.config = new EmailConfig(
settings.get("smtp.host"),
Integer.parseInt(settings.getOrDefault("smtp.port", "587")),
settings.getOrDefault("smtp.auth", "true").equals("true")
);
// Now we can safely call the parent constructor
super(serviceName);
// Epilogue: final setup
logger.info("Email service initialized with host: " + config.getHost());
registerShutdownHook();
}
}
Now, the constructor reads in the natural order:
- Prologue → Validate and prepare data
- Constructor call → Pass well-formed values to the parent
- Epilogue → Final adjustments or registrations
Now constructors finally flow like natural code. No more juggling between static helpers and scattered initialization.
Part 3: Advanced Pattern Matching – Smarter, Safer, Cleaner
If Compact Sources simplify starting with Java, and Flexible Constructors simplify building with Java, then Advanced Pattern Matching simplifies reasoning with Java.
Before Java 25, pattern matching supported only reference types. That meant primitive values like int, double, boolean, still required old‑fashioned if/else or manual unboxing to handle correctly.
With JEP 507, Java 25 extends pattern matching to cover primitive types in both instanceof and switch. This means you can now write patterns that directly handle primitives without converting them into objects.
int code = retrieveCode();
String result = switch (code) {
case (byte b) when b >= 0 && b < 10 -> "Small positive code: " + b;
case (int i) when i >= 1000 -> "Large code: " + i;
case (short s) -> "Other short based code: " + s;
default -> "Unhandled code: " + code;
};
Here the switch works naturally with primitive values. You can add guard clauses using when and keep the logic compact.
This feature enables more expressive and type-safe code by allowing you to match patterns against values and extract components in a single expression.
Here’s a practical example demonstrating pattern matching with sealed types, records, and guard conditions:
import module java.base;
sealed interface ApiResponse permits
Success, Error, Pending {
}
record Success(String data, long timestamp) implements ApiResponse {
}
record Error(int code, String message) implements ApiResponse {
}
record Pending(String taskId) implements ApiResponse {
}
public class ApiHandler {
static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.of("UTC"));
void main() {
var responses = new ApiResponse[]{
new Success("User created", System.currentTimeMillis()),
new Error(404, "Resource not found"),
new Pending("task-123"),
new Success("", System.currentTimeMillis()),
new Error(500, "Internal server error"),
null
};
for (var response : responses) {
IO.println(processResponse(response));
IO.println("---");
}
}
private String processResponse(ApiResponse response) {
return switch (response) {
case null -> "No response received";
case Success(var data, var time) when data.isEmpty() -> "Success but no data at " + formatTime(time);
case Success(var data, var time) -> "Success: " + data + " at " + formatTime(time);
case Error(var code, var msg) when code >= 500 -> "Server error (" + code + "): " + msg;
case Error(var code, var msg) when code >= 400 -> "Client error (" + code + "): " + msg;
case Error(var code, var msg) -> "Error " + code + ": " + msg;
case Pending(var id) -> "Processing task: " + id;
};
}
private String formatTime(long time) {
Instant instant = Instant.ofEpochMilli(time);
return formatter.format(instant);
}
}
Conclusion
Java 25 introduces a productivity trifecta that streamlines everyday coding. Compact Source Files cut away boilerplate, Flexible Constructors allow natural initialization, and Advanced Pattern Matching unifies object and primitive handling in clean, expressive switches. Together, these features make Java simpler, safer, and more enjoyable without changing its core.

This article is part of the magazine issue ’Java 25 – Part 1′.
You can read the complete issue with all contributions here.