In this blog post we take a look at the smaller changes in Java 22 to 25. A smaller change is, for example, a new method added to an existing class or smaller language changes. This article does not cover major changes like a new language feature or a major new package like the new Class File API.
I focus on useful features for my daily programming life as an application developer. I'm less interested in changes to low-level APIs like Reflection and low-level IO. This blog post also omits changes to the Java Virtual Machine (JVM), like new Garbage Collectors (GC).
Here are the other parts of this series:
Java 22 (March 2024) ¶
Java 22 introduces unnamed variables and patterns to the language, allowing developers to use _ for variables that are required but not used. The Foreign Function & Memory API becomes finalized after several preview releases.
java.text.ListFormat ¶
The new java.text.ListFormat class provides locale-sensitive formatting of lists. This is useful for creating human-readable lists that follow the grammatical conventions of different languages.
ListFormat listFormat = ListFormat.getInstance(Locale.ENGLISH, ListFormat.Type.STANDARD, ListFormat.Style.FULL);
String result = listFormat.format(List.of("apple", "banana", "cherry"));
System.out.println(result); // "apple, banana, and cherry"
ListFormat orFormat = ListFormat.getInstance(Locale.ENGLISH, ListFormat.Type.OR, ListFormat.Style.FULL);
String orResult = orFormat.format(List.of("tea", "coffee"));
System.out.println(orResult); // "tea or coffee"
// Different locales have different formatting rules
ListFormat germanFormat = ListFormat.getInstance(Locale.GERMAN, ListFormat.Type.STANDARD, ListFormat.Style.FULL);
String germanResult = germanFormat.format(List.of("Apfel", "Banane", "Kirsche"));
System.out.println(germanResult); // "Apfel, Banane und Kirsche"
The ListFormat.Type enum defines whether the list should be formatted as a conjunction (AND), disjunction (OR), or unit list. The ListFormat.Style enum controls the verbosity of the formatting (FULL, SHORT, NARROW).
Enhanced Path.resolve methods ¶
Two new overloaded resolve methods have been added to java.nio.file.Path to simplify path resolution with multiple path components.
resolve(String, String...) converts path strings to paths and resolves them iteratively:
Path basePath = Paths.get("/home/user");
Path resolved = basePath.resolve("documents", "projects", "myapp", "src");
System.out.println(resolved); // /home/user/documents/projects/myapp/src
// Equivalent to the more verbose:
// basePath.resolve("documents").resolve("projects").resolve("myapp").resolve("src")
resolve(Path, Path...) works with Path objects directly:
Path base = Paths.get("/var");
Path logs = Paths.get("log");
Path app = Paths.get("myapp");
Path file = Paths.get("application.log");
Path fullPath = base.resolve(logs, app, file);
System.out.println(fullPath); // /var/log/myapp/application.log
These methods make path construction more concise when working with multiple path segments.
java.io.Console new isTerminal method ¶
A new method isTerminal() has been added to java.io.Console. It returns true if the console is connected to a terminal, which is useful for determining if the application is running in an interactive session.
Console console = System.console();
if (console != null && console.isTerminal()) {
console.printf("This is an interactive terminal.%n");
} else {
System.out.println("This is not an interactive terminal.");
}
Java 23 (September 2024) ¶
JEP 467 adds support for JavaDoc comments written in Markdown rather than solely in a mixture of HTML and JavaDoc @-tags.
java.io.Console new locale-aware methods ¶
The java.io.Console class has new methods that accept a Locale for formatting: format, printf, readLine, and readPassword. These allow for locale-specific formatting of prompts and output.
Console console = System.console();
if (console != null) {
console.format(Locale.GERMAN, "Der Wert ist %,.2f%n", 1234567.567);
// Der Wert ist 1.234.567,57
console.printf(Locale.UK, "The price is %,.2f%n", 1234567.567);
// The price is 1,234,567.57
String name = console.readLine(Locale.US, "Please enter your name: ");
console.printf("Hello, %s.%n", name);
char[] password = console.readPassword(Locale.US, "Enter your password: ");
console.printf("Password entered.%n");
java.util.Arrays.fill(password, ' ');
}
java.time.Instant new until method ¶
A new convenience method until(Instant) has been added to java.time.Instant. It calculates the Duration between two Instant objects.
Instant start = Instant.parse("2025-09-10T10:00:00Z");
Instant end = Instant.parse("2025-09-10T11:30:45Z");
Duration duration = start.until(end);
System.out.println(duration); // PT1H30M45S
System.out.println("Total seconds: " + duration.toSeconds()); // 5445
Java 24 (March 2025) ¶
This release finalizes the Class-File API (JEP 484) for parsing, generating, and transforming Java class files. It also introduces Stream Gatherers (JEP 485) to enhance the Stream API with custom intermediate operations.
Two small changes include:
New waitFor(Duration) method in java.lang.Process. Blocks the current thread until the process ends or the specified timeout elapses. Returns true if the process has finished.
Process process = ProcessHandle.current().info().command().map(cmd -> {
try {
return new ProcessBuilder(List.of(cmd, "-version"))
.redirectErrorStream(true).inheritIO().start();
} catch (Exception e) {
throw new RuntimeException(e);
}
}).orElseThrow();
process.waitFor(Duration.ofSeconds(5));
New of(CharSequence) method in java.io.Reader. This method returns a Reader that reads characters from a CharSequence. The reader is initially open and reading starts at the first character in the sequence.
try (Reader reader = Reader.of("Hello, World!")) {
int ch;
while ((ch = reader.read()) != -1) {
System.out.println((char) ch);
}
System.out.println();
}
catch (IOException e) {
e.printStackTrace();
}
Java 25 (September 2025) ¶
Java 25 introduces several features aimed at simplifying the language and making it more flexible. Two notable JEPs are JEP 512: Compact Source Files and Instance Main Methods and JEP 513: Flexible Constructor Bodies. Other new features include JEP 506 Scoped Values API and JEP 510 Key Derivation Function (KDF) API.
JEP 512: Compact Source Files and Instance Main Methods ¶
This JEP simplifies writing "Hello, World!" and other small programs. It allows for instance main methods, which means you no longer need static and the String[] args parameter for simple programs.
A simple "Hello, World!" can now be written as:
void main() {
System.out.println("Hello, World!");
}
With the new java.lang.IO class (see below), it can be simplified even further to:
void main() {
IO.println("Hello, World!");
}
java.lang.IO ¶
To further simplify beginner-friendly programming, Java 25 introduces the java.lang.IO class. It offers static methods for simple, line-oriented console input and output, removing the need to deal with System.out and System.in directly.
IO.println prints a message to the standard output, and IO.readln reads a line from the standard input.
void main() {
// Print a prompt and read a line of input
String name = IO.readln("What is your name? ");
// Print a greeting
IO.println("Hello, " + name + "!");
// Print different data types
IO.println(42);
IO.println(3.14);
}
Methods in java.lang.IO include:
printmethods for printing without a newlineprintlnmethods for printing with a newlinereadlnmethods for reading input, with or without a prompt
Because everything from the java.lang package is implicitly imported, you can use IO directly without any import statements.
JEP 513: Flexible Constructor Bodies ¶
This feature allows statements to appear before an explicit constructor invocation (this() or super()).
This is useful for validating arguments before chaining constructors.
public class PositiveInteger {
private final int value;
public PositiveInteger(int value) {
if (value <= 0) {
throw new IllegalArgumentException("Value must be positive");
}
this.value = value;
}
public PositiveInteger(String s) {
// Validation before this() call
if (s == null || s.isEmpty()) {
throw new IllegalArgumentException("String cannot be null or empty");
}
this(Integer.parseInt(s));
}
}
java.io.Reader new readAll methods ¶
Two new methods, readAllAsString() and readAllLines(), have been added to java.io.Reader for easily consuming the entire content of a reader.
readAllAsString() reads all characters from the reader and returns them as a single string.
try (Reader reader = new StringReader("line 1\nline 2\nline 3")) {
String content = reader.readAllAsString();
System.out.println(content);
// Output:
// line 1
// line 2
// line 3
} catch (IOException e) {
// ...
}
readAllLines() reads all lines from the reader and returns them as a list of strings.
try (Reader reader = new StringReader("line 1\nline 2\nline 3")) {
List<String> lines = reader.readAllLines();
lines.forEach(line -> System.out.println(line));
// Output:
// line 1
// line 2
// line 3
} catch (IOException e) {
// ...
}
HTTP response body size limiting ¶
The HttpResponse API has been enhanced with BodyHandlers.limiting and BodySubscribers.limiting to control the size of response bodies. This is useful for preventing excessive memory usage when handling large responses.
BodyHandlers.limiting creates a body handler that wraps another body handler and limits the number of bytes passed to it.
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://placehold.co/600x400"))
.build();
long limit = 1024 * 1024; // 1 MB limit
HttpResponse<Path> response = client.send(request,
BodyHandlers.limiting(BodyHandlers.ofFile(Paths.get("body.bin")), limit));
BodySubscribers.limiting works at a lower level, creating a body subscriber that limits the data passed to a downstream subscriber.
java.util.concurrent.ForkJoinPool new scheduling methods ¶
ForkJoinPool now includes methods for scheduling tasks, similar to ScheduledThreadPoolExecutor.
schedule(Runnable, long, TimeUnit)schedule(Callable, long, TimeUnit)scheduleAtFixedRate(Runnable, long, long, TimeUnit)scheduleWithFixedDelay(Runnable, long, long, TimeUnit)
These methods allow you to schedule tasks for future or periodic execution within the ForkJoinPool.
try (ForkJoinPool pool = new ForkJoinPool()) {
pool.schedule(() -> System.out.println("Delayed task executed"), 1, TimeUnit.SECONDS);
pool.scheduleAtFixedRate(() -> System.out.println("Periodic task"), 0, 2, TimeUnit.SECONDS);
Thread.sleep(5000);
pool.shutdown();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
java.lang.Math and java.lang.StrictMath new Exact methods ¶
New *Exact methods have been added for safe arithmetic operations that throw an ArithmeticException on overflow.
powExact(int, int)andpowExact(long, int)for integer exponentiation.unsignedMultiplyExact(...)for unsigned multiplication.unsignedPowExact(...)for unsigned exponentiation.
try {
int result = Math.powExact(10, 9);
System.out.println(result); // 1000000000
int overflow = Math.powExact(10, 10);
} catch (ArithmeticException e) {
System.err.println(e); // java.lang.ArithmeticException: integer overflow
}
try {
long result = Math.unsignedMultiplyExact(0xFFFFFFFFFFFFFFFFL, 2);
} catch (ArithmeticException e) {
System.err.println(e); //java.lang.ArithmeticException: unsigned long overflow
}
CharSequence.getChars ¶
The CharSequence interface has a new default method getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin).
This method provides a convenient way to copy a range of characters from any CharSequence
into a character array. Since it's a default method, it's available on all classes that implement CharSequence,
like String, StringBuilder, StringBuffer and CharBuffer.
CharSequence text = "Hello, Java 25!";
char[] destination = new char[8];
text.getChars(7, 15, destination, 0);
IO.println(new String(destination)); // "Java 25!"
Deflater and Inflater are now AutoClosable ¶
In Java 25, java.util.zip.Deflater and java.util.zip.Inflater now implement AutoCloseable. This means you can use them in a try-with-resources statement, which simplifies resource management and ensures that the close() method is called automatically.
Example usage of Deflater and Inflater with try-with-resources:
byte[] input = "Hello, World!".getBytes(StandardCharsets.UTF_8);
byte[] output = new byte[100];
int compressedSize;
try (Deflater deflater = new Deflater()) {
deflater.setInput(input);
deflater.finish();
compressedSize = deflater.deflate(output);
}
// Decompressing data
byte[] decompressed = new byte[100];
try (Inflater inflater = new Inflater()) {
inflater.setInput(output, 0, compressedSize);
int decompressedSize = inflater.inflate(decompressed);
IO.println(new String(decompressed, 0, decompressedSize, StandardCharsets.UTF_8)); // "Hello, World!"
}