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. I don't cover in this article major changes like a new language feature or a major new package like the new HTTP client or the Class File API.
I also focus on useful features for my daily programming life as an application developer. I'm personally less interested in changes in low-level APIs like Reflection and low-level IO. I also omit in this blog post 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 reaches its final form 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) ¶
With JEP 467 JavaDoc now supports 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 I've noticed are:
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 was already 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:
print
methods for printing without a newlineprintln
methods for printing with a newlinereadln
methods 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 automatically called.
Here's an example of how to use 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!"
}