DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Top Java Security Vulnerabilities and How to Prevent Them in Modern Java
  • Multithreading in Modern Java: Advanced Benefits and Best Practices
  • Apache Spark 3 to Apache Spark 4 Migration: What Breaks, What Improves, What's Mandatory
  • Memory Optimization and Utilization in Java 25 LTS: Practical Best Practices

Trending

  • AI Won't Keep You from Hitting the Scalability Wall
  • Strategy Design Pattern
  • Comparing Golang with Java
  • What Is Plagiarism? How to Avoid It and Cite Sources
  1. DZone
  2. Coding
  3. Java
  4. Exploring A Few Java 25 Language Enhancements

Exploring A Few Java 25 Language Enhancements

Brief and with practical code examples that raise developers' curiosity and interest in exploring these language enhancements in detail.

By 
Horatiu Dan user avatar
Horatiu Dan
DZone Core CORE ·
Jul. 10, 26 · Tutorial
Likes (0)
Comment
Save
Tweet
Share
184 Views

Join the DZone community and get the full member experience.

Join For Free

Although Java 26 was released in mid-March this year, Java 25 is the latest LTS version available, and thus I chose to focus my attention on it in the first place.

Irrespective of whether certain Java 25 language improvements are still available as preview features or not, this article briefly outlines a few. The main purpose is to first make the developers aware that Java is continuously refined and evolved by its API contributors and secondly, to raise the curiosity and interest of exploring these enhancements in detail.

Out of the bunch of features proposed in JDK 25 [Resource 1], the following five language enhancements are briefly explored here:

  • JEP 512 – Compact source files and instance main methods
  • JEP 513 – Flexible Constructor Bodies
  • JEP 507 – Primitive Types in Patterns, instanceof and switch
  • JEP 506 – Scoped Values
  • JEP 502 – Stable Values

Compact Source Files and Instance Main Methods (JEP 512)

After its initial proposal as part of JDK 21 as JEP 445 – ‘Unnamed Classes and Instance main Methods', this feature has been gradually improved in the next releases based on the feedback received, and it was finalized in JDK 25.

The goal is clear – Simplify Java’s entry point for beginner developers and in small programs — reducing boilerplate and ceremony — while remaining fully compatible with the standard Java language and toolchain.

Let’s imagine we quickly want to write a small program that:

  • prompts the user and keeps reading their input in a loop
  • if the user types exit (case-insensitive), it prints “Goodbye!” and ends
  • otherwise, it prints the length of the entered string

The code for this resides directly in a package, in a file called CompactSourceFile.java file, whose content looks as below:

Java
 
static final String EXIT = "exit";

String prompt(String exit) {
  return "Enter a string (or '" + exit  + "' to quit): ";
}

void main() {
  while (true) {
    String input = IO.readln(prompt(EXIT));
    if (EXIT.equalsIgnoreCase(input)) {
      IO.println("Goodbye!");
      break;
    }
    IO.println("Length: " + input.length());
  }
}


Suggestive and to the point — no class declaration, just the aimed simple piece of code.

If run and after providing a few prompts, the output is as expected:

Plain Text
 
Enter a string (or 'exit' to quit): joke
Length: 4
Enter a string (or 'exit' to quit): meeting
Length: 7
Enter a string (or 'exit' to quit): exit
Goodbye!


A few observations are worth making:

  • The need for an explicit class declaration is removed
  • Although not visible, the compiler implicitly declares a class that is final and part of an unnamed package
  • The traditional public static void main(String[] args) is replaced with a simpler enough instance method that is a clearly defined program entry point
  • The program entry-point still needs to be named main() as the JVM  looks for such a launchable method
  • All fields and methods belong to the implicit class, just as in the regular case
  • The simple program focuses directly on its purpose without additional details
  • It’s experimental; it’s straightforward. If it turns into a real application though, it’s advisable to preserve the object-oriented structure and all known best practices

Flexible Constructor Bodies (JEP 513)

Until JDK 25, one clear rule regarding constructors was that no statements could be written before super() or this() calls. For the sake of expressivity and readability, JEP 513 relaxes this constraint, while the existing code continues to compile and function correctly, and moreover, the object’s safety is 100% preserved.

In Java, when an object instance is constructed, there are two stages that happen, one before and one after; the hierarchy of constructor chaining begins its execution. During the former, the memory is allocated and the instance fields are initialized, then during the latter, once the this() and super() calls complete, the rest of the object is basically constructed. This process is mainly a safety-wise one, that is to ensure the inherited object parts are completely initialized before any child-related code is run. Joshua Bloch has already advised in his ‘Effective Java’ book to prevent this reference to escape “too early.” The result – objects are not partially constructed at any moment.

Simply put, starting with Java 25, statements are now allowed to be executed before this() or super() as part of constructor bodies and still, internally without making any compromises in regard to object core safety while building it.

Observations:

  • Allowed statements – only those that don’t depend on instance state and are guaranteed to be safe:
    • manipulation of locally declared variables that live on the stack
    • constructor parameter validation
  • Syntax is made more permissive, the object safety is preserved

Let’s have a small example where we minimally model a Car through an approximate length and the number of wheels, where the former is inherited from a Vehicle super class.

Java
 
static class Vehicle {
  
  private final long length;
  
  Vehicle(long length) {
    if (length < 0) {
      throw new IllegalArgumentException("Length must be positive");
    }
    this.length = length;
  }
  
  Vehicle(double length) {
    long round = Math.round(length);
    this(round);
  }
  
  public long length() {
    return length;
  }
}

static class Car extends Vehicle {
  
  private final int wheels;
  
  Car(double length, int wheels) {
    if (wheels < 0) {
      throw new IllegalArgumentException("Wheels must be positive");
    }
    super(length);
    this.wheels = wheels;
  }
  
  public int wheels() {
    return wheels;
  }
}

void main() {
  var car = new Car(4.6d, 4);
  IO.println("Car is about " + car.length() +
      " meters long and has " + car.wheels() + " wheels.");
}


If we run it, the following output is observed — Car is about 5 meters long and has 4 wheels. 

First, one may observe that the Vehicle#length is first rounded as it's kept as a long value (line 13) then passed to the other constructor. Secondly, the number of wheels is validated before the super constructor is invoked (line 30), then set.

Let’s now model a motorcycle using records.

Java
 
record Moto(long length, int wheels) {
  Moto {
    if (length < 0) {
      throw new IllegalArgumentException("Length must be positive");
    }
    if (wheels < 0) {
      throw new IllegalArgumentException("Wheels must be positive");
    }
  }
  
  Moto(double length, int wheels) {
    long round = Math.round(length);
    this(round, wheels);
  }
}

void main() {
  var moto1 = new Moto(3, 2);
  IO.println("Moto 1 is about " + moto1.length() +
        " meters long and has " + moto1.wheels() + " wheels.");
  var moto2 = new Moto(2.1d, 2);
  IO.println("Moto 2 is about " + moto2.length() +
        " meters long and has " + moto2.wheels() + " wheels.");
}


While before Java 25, the parameters’ validation is allowed in canonical record constructors (line 2), the ability is now extended for non-canonical constructors as well (line 12), and moreover the this() call is allowed.

If we run it, moto1 is constructed using only the canonical constructor, while moto2 via both and the output is obviously the one below.

Plain Text
 
Moto 1 is about 3 meters long and has 2 wheels.
Moto 2 is about 2 meters long and has 2 wheels.


Regarding enums, let’s consider the following experimental code. 

Java
 
enum Bike {
  CITY(12),
  MOUNTAIN("10");
  
  private final int weight;
  
  Bike(int weight) {
    if (weight < 0) {
      throw new IllegalArgumentException("Weight must be positive");
    }
    this.weight = weight;
  }
  
  Bike(String description) {
    int weight = Integer.parseInt(description);
    this(weight);
  }
  
  public int weight() {
    return weight;
  }
}

void main() {
  IO.println("Bike is " + Bike.MOUNTAIN.weight() + " kg heavy.");
}


While validation as in the first constructor has been allowed prior to Java 25, additional operations before calling this() are now permitted as well.

To conclude, at class, record or enum level, the way the constructors can now be written is cleaned and improved, while the object safety is still preserved without any compromises.

Primitive Types in Patterns, instanceof and switch (JEP 507)

In general, pattern matching is a language procedure that basically combines a few steps into a feature that facilitates testing a particular value. The focus is on what is being checked and not necessarily on the means of doing it. In addition to situations where pattern matching is applied in case of instanceof and switch constructs, Java 25 allows using it with primitives — byte, short, int, long, float, double, char, boolean are now part of this model. The reference type boundary is now extended, making the feature uniform and more intuitive as the applicability restrictions have been reduced significantly.

Let’s consider the following examples:

Java
 
void main() {
  Number doubleBoxed = 3.99;
  
  if (doubleBoxed instanceof int i) {
    IO.println("'num' fits in int: " + i);
  } else {
    IO.println("'num' does NOT fit losslessly in int (value=" + doubleBoxed + ")");
  }
  
  IO.println(describe(Byte.MAX_VALUE));
  IO.println(describe(Short.MAX_VALUE));
  IO.println(describe(42));
  IO.println(describe(Integer.MAX_VALUE));
  IO.println(describe(Long.MAX_VALUE));
  IO.println(describe(3.14f));
  IO.println(describe(2.718281828459045));
}

static String describe(Number n) {
  return switch (n) {
    case byte b -> n + " fits in byte → " + b;
    case short s -> n + " fits in short → " + s;
    case int i -> n + " fits in int → " + i;
    case long l -> n + " fits in long → " + l;
    case float f -> n + " fits in float → " + f;
    case double d -> n + " fits in double → " + d;
    case null, default -> n + " unknown numeric type";
  };
}


If run, it produces the below output: 

Plain Text
 
'num' does NOT fit losslessly in int (value=3.99)
127 fits in byte → 127
32767 fits in short → 32767
42 fits in int → 42
2147483647 fits in int → 2147483647
9223372036854775807 fits in long → 9223372036854775807
3.14 fits in float → 3.14
2.718281828459045 fits in double → 2.718281828459045


Observations:

  • describe() allows to easily describe a Number as the most compact type it fits into (line 19)
  • A Number reference can now be pattern-matched directly to a primitive (line 20)
  • The feature enables safe, lossless narrowing checks without manual casting or range checks

Going deeper with the exploration, what I personally find interesting regarding this feature is the deep nested patterns.

The below example allows introspecting the object and directly matching the content.

Java
 
record Age(int years) {}
record Wine(String name, Age age) {}

void analyze(Object value) {
  IO.println("Analyzing - " + value);
  if (value instanceof Wine(String name, Age(int years))) {
    IO.println("Wine: " + name + " (" + years + " years old)");
  } else {
    IO.println("Not a wine");
  }
}

void main() {
  var value1 = new Wine("Merlot", new Age(10));
  analyze(value1);
  
  var value2 = "Cabernet Sauvignon";
  analyze(value2);
}


If run, the result is again obvious, but the code is clean, concise, and very expressive. 

Plain Text
 
Analyzing Wine[name=Merlot, age=Age[years=10]]
Wine: Merlot (10 years old)
Analyzing Cabernet Sauvignon
Not a wine


To conclude, beginning with Java 25 in regard to the current state of the pattern matching feature, code has a great chance to become cleaner and safer as a whole. 

Scoped Values (JEP 506)

As Project Loom brought virtual threads in Java, that definitely made room for another enhancement — passing immutable context between and across threads in a more structured, predictable, and safer way. ScopedValues are a finalized feature in Java 25 and allow exactly this, within the boundaries of a precise execution scope.

To better understand them, let’s refer to the following simple example:

Java
 
static final ScopedValue<User> USER = ScopedValue.newInstance();

record User(int id, String name) {}

static void handleFurther() {
  IO.println("handleFurther - start for " + USER.get());
  ScopedValue.where(USER, new User(2, "AD"))
      .run(() -> {
        IO.println("handleFurther - something specific for " + USER.get());
      });
  IO.println("handleFurther - finished for " + USER.get());
}

static void handle() {
  IO.println("handle - start for " + USER.get());
  handleFurther();
  IO.println("handle - finished for " + USER.get());
}

void main() {
  ScopedValue.where(USER, new User(1, "HCD"))
      .run(() -> {
        IO.println("main - before handling - " + USER.get());
        handle();
        IO.println("main - after handling - " + USER.get());
      });
				
  //handle();
}


The spot for the shared User is first created as USER.

The context passed during the execution (and not as a parameter of the methods engaged) is the User instance. It might be seen as the “current” user.

Once the instance is bound (line 23), its scope is clearly defined in the main() method and passed throughout the execution – to handle() and further to handleFurther(). Access is read-only; it cannot be changed.

If during the execution flow it is re-set, as in handleFurther(), that is, a new (nested) sub scope is created and once this sub scope ends, the previous outer scope is continued.

If run, the code produces the below output which exemplifies even more clearly what has already been stated.

Properties files
 
main - before handling - User[id=1, name=HCD]
handle - start for User[id=1, name=HCD]
handleFurther - start for User[id=1, name=HCD]
handleFurther - something specific for User[id=2, name=AD]
handleFurther - finished for User[id=1, name=HCD]
handle - finished for User[id=1, name=HCD]
main - after handling - User[id=1, name=HCD]


In case handle() would be called outside the scope (line 30) and the code re-run, a clear exception is thrown upon reaching this point – Exception in thread "main" java.util.NoSuchElementException: ScopedValue not bound.

Key points:

  • where(…).run(…) – binds the value for the duration of the lambda, then unbinds it automatically – there’s no need for manual cleanup.
  • Immutable within scope – once bound, it cannot be changed (but can be re-bound in a nested scope).
  • Cheap with virtual threads – no copying, just a reference.
  • Easy to reason about – the value is always what was bound at the top of the current scope
  • Good alternative to ThreadLocal which has unbounded lifetime, is mutable and pretty hard to reason about, as its value can be changed anywhere in the call stack.
  • Works beautifully with Structured Concurrency (JEP 505) – child tasks automatically share the parent’s scoped values without copying.

To conclude, scoped variables contribute a lot to the concurrency cleanness and safety and help prevent issues such as memory leaks or stale data leaking.

Stable Values (JEP 502)

I see this enhancement as enforcing effective immutability — both at instance and object level. If prior to Java 25 we created an instance, declared it final, initialized it, and documented that it shall remain unchanged, the reality was sometimes different, as some “content” of the instance was still mutable. StableValue feature allows constructing immutable instances by all means so that once initialized, the object content is guaranteed to remain unchanged as well.

StableValues are a JVM enhancement that offers a way of achieving thread-safety and deep immutability, an alternative to accomplishing this via combining locks, synchronization, volatile variables and Atomic references.

The behavior is thread-safe by design, detail ensured by the JVM’s internal handling of StableValues.

Let’s examine the following code:

Java
 
static class User {
  
  private final StableValue<String> id = StableValue.of();
  private final String name;
  
  public User(String name) {
    this.name = name;
  }
  
  public String id() {
    return id.orElseSet(() -> UUID.randomUUID().toString());
  }
  
  public String name() {
    return name;
  }
  
  @Override
  public String toString() {
    return name + " (" + id() + ")";
  }
}

private record Task(CountDownLatch latch, Runnable runnable) implements Runnable {
  @Override
  public void run() {
    try {
      latch.await();
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    runnable.run();
  }
}

void main() {
  var user1 = new User("HCD");
  IO.println("Created " + user1);
  
  var user2 = new User("Andrei");
  IO.println("Created " + user2);
  
  IO.println("User's unique identifiers are: " + user1.id() + ", " + user2.id());
}


Observations:

  • A User is simply described by two attributes — while the name is provided at construction time, the id represents an internal unique identifier.
  • id is declared as a StableValue and is lazily initialized when the value is read (if in a concurrent context, by the first thread that performs the action)   
  • Once initialized, this value is deeply immutable; it cannot be changed and remains as such until the object is destroyed

If run, the output is the following:

Properties files
 
Created HCD (477a7dc1-c71f-4189-8c58-13994148ff95)
Created Andrei (47647539-9cbe-4890-af23-050ee1fe9379)
User's unique identifiers are: 477a7dc1-c71f-4189-8c58-13994148ff95, 47647539-9cbe-4890-af23-050ee1fe9379


It’s clear the ids are set when needed, and their values persist whenever read subsequently.

One last observation is worth making regarding the User#id attribute — as a StableValue, it’s automatically thread-safe and lock-free. To demonstrate this, let’s run the next piece of code.

Java
 
void main() {
  var user = new User("Concurrent User");
  
  var latch = new CountDownLatch(1);
  try (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<?> result1 = exec.submit(new Task(latch,
        	() -> IO.println("Task1 - Id: " + user.id() + 
                             " at " + System.currentTimeMillis())));
    Future<?> result2 = exec.submit(new Task(latch,
        	() -> IO.println("Task2 - Id: " + user.id() + 
                             " at " + System.currentTimeMillis())));
    Future<?> result3 = exec.submit(new Task(latch,
        	() -> IO.println("Task3 - Id: " + user.id() + 
                             " at " + System.currentTimeMillis())));
    latch.countDown();
    result1.get();
    result2.get();
    result3.get();
  } catch (ExecutionException | InterruptedException e) {
    throw new RuntimeException(e);
  }
}


Tasks 1, 2, and 3 are created and set to read the id of the user created in advance, then executed in parallel. The output below demonstrates that, in this particular run, Task 3 sets the id, and then Tasks 1 and 2 use the same value. 

Plain Text
 
Task3 - Id: f7e12b49-5c21-4898-883b-12013824a683 at 1773834965123
Task1 - Id: f7e12b49-5c21-4898-883b-12013824a683 at 1773834965123
Task2 - Id: f7e12b49-5c21-4898-883b-12013824a683 at 1773834965123


StableValue also comes with quite a few higher-level helper methods (function(), intFunction(), list(), map(), supplier()), each of them useful and suitable in various scenarios.

Below is an example of how the Singleton pattern could be implemented.

Java
 
record User(int id, String name) {}

static class UserService {
  public UserService() {
    IO.println("UserService created");
  }
  
  public void register(User user) {
    IO.println("Registered " + user);
  }
}

static UserService getInstance() {
  return USER_SERVICE_INSTANCE.orElseSet(UserService::new);
}

private static final StableValue<UserService> USER_SERVICE_INSTANCE = StableValue.of();
    
void main() {
  getInstance().register(new User(1, "HCD"));
  getInstance().register(new User(2, "Andrei"));
}


The aim is to have a single instance of the UserService that can be used to register users via the designated method. If we run it, the output is the one below, which clearly shows the constructor is called only once.

Plain Text
 
UserService created
Registered User[id=1, name=HCD]
Registered User[id=2, name=Andrei]


To conclude, the StableValue enhancement ensures immutability enforced at JVM level – once the value is set, it’s stable and visible to all threads.

Conclusions

This article briefly covered a few Java 25 language enhancements, hoping that the straight-to-the-point examples presented offer a starting point for further deep-diving into these features.

Whether you have already migrated to the latest LTS or not, whether you have started exploring the latest additions and improvements, I consider this worth doing whatsoever.

At JavaOne ’26, during one of the opening keynotes, I remarked this quote: “Java is everywhere AI needs to be.” I couldn’t agree more. In a world where apparently everyone is preoccupied with “Accelerated Inference,” let’s remain optimistic about what the future will bring and continue to build and consolidate our Java foundation by exploring the new additions, staying up to date, and gradually embracing them in our personal and professional projects.

Resources

[1] – JDK 25

[2] – Sample code is available here.

Java (programming language) Data Types

Published at DZone with permission of Horatiu Dan. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Top Java Security Vulnerabilities and How to Prevent Them in Modern Java
  • Multithreading in Modern Java: Advanced Benefits and Best Practices
  • Apache Spark 3 to Apache Spark 4 Migration: What Breaks, What Improves, What's Mandatory
  • Memory Optimization and Utilization in Java 25 LTS: Practical Best Practices

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook