Home | Send Feedback | Share on Bluesky |

Small changes in Java 22 to 25

Published: 10. September 2025  •  java

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:

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.

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.

  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!"
  }