Skip to main content

Command Palette

Search for a command to run...

Java 25 features

Published
8 min readView as Markdown
Java 25 features

Introduction

Java 25, a Long-Term Support (LTS) release, became public on September 16, 2025. This version introduces several key features and enhancements that aim to simplify the language, improve developer productivity, and enhance performance and stability.

Key features

  • Language and Compiler Improvements

    • Compact Source Files and Instance Main Methods.

    • Flexible Constructor Bodies.

    • Module Import Declarations.

    • Primitive Types in Patterns like instanceof and switch.

  • API and Performance Enhancements

    • Scoped Values

    • Structured Concurrency

    • Compact Object Headers

    • Ahead-of-Time Method Profiling

    • JFR Cooperative Sampling

Details

Compact Source Files and Instance Main Methods.

  • This feature, introduced in Java 25, aims to simplify writing small, standalone Java programs. It reduces the verbosity of the traditional Java main method, making the language more user-friendly for beginners and better for scripting. The main idea is to let a program run without requiring a formal class declaration or a static main method.

    Traditionally, writing a simple "Hello, World!" program in Java requires a lot of boilerplate code.

      public class HelloWorld {
          public static void main(String[] args) {
              System.out.println("Hello, World!");
          }
      }
    
  • This structure is essential to Java's object-oriented design, but it can be a challenge for beginners. They need to grasp the ideas of public, class, static, and void before they can write even a simple line of code to print to the console. This complexity also makes Java less attractive for quick scripting tasks, where other languages like Python or JavaScript are more favorable.

  • JEP 512 addresses this issue by cutting down on the necessary boilerplate. This change lets developers concentrate on the logic of their code right away.

  • How It Works:

    • Implicit Class and Main Method: If a source file contains only top-level code (not inside a class or interface), the compiler automatically wraps it in an unnamed class.

    • Instance Main Methods: The entry point can now be an instance method called main that takes no arguments.

  •   //HelloWorld.java with instance main method
      void main() {
          System.out.println("Hello, world!");
      }
    
      // HelloWorld.java most compact form
      IO.println("Hello, world!");
    
  • Advantages

    • Improved Accessibility for Beginners: The simplified syntax lowers the barrier to entry, making it easier for new programmers to start with Java.

    • Concise Scripting: It makes Java a more viable choice for writing small utility scripts and one-off programs.

    • Focus on Logic: Developers can get straight to the code that matters without being bogged down by boilerplate.

  • Disadvantages

    • Potential for Confusion: The new syntax might confuse developers who are used to the traditional, explicit structure. It could also lead to a "dual standard" for writing simple programs versus more complex ones.

    • Reduced Object-Oriented Clarity: The feature hides the underlying object-oriented nature of Java, which could be misleading for beginners who are meant to learn the foundational principles of the language.

    • Limited Scope: This feature is primarily for simple, single-file programs. It is not intended for larger, multi-class applications, and its use outside of this context would be inappropriate.

Flexible Constructor Bodies

  • Flexible Constructor Bodies, a feature in Java 25, changes a long-standing rule about constructor syntax. It allows statements to be placed before the explicit this() or super() constructor calls, which were previously forbidden. This enhancement makes constructors more expressive and powerful, allowing for logic that was previously confined to helper methods or complex workarounds.

  • The traditional rule in Java states that the first statement in a constructor must be either a call to another constructor in the same class (this(...)) or a call to a superclass constructor (super(...)). This strict rule stops any logic, like input validation or initial calculations, from happening before the superclass constructor is called.

  • For example, if you wanted to validate an argument before passing it to the superclass constructor, you would have to:

    • Pass the unvalidated argument to super(), which can lead to invalid object states.

    • Perform the validation in a static helper method, which can be cumbersome and less readable.

  • How It Works

    • With this feature, you can now write code in the constructor body before the super() or this() call. The key is that the code must not access the fields of the object being constructed. This ensures that the superclass constructor, which is responsible for initializing the superclass's state, is called before any subclass fields are accessed.

        //Validation can be done directly in the constructor with Flexible Constructor Bodies (Java 25)
        public class MySubClass extends MySuperClass {
            public MySubClass(int value) {
                if (value < 0) {
                    throw new IllegalArgumentException("Value cannot be negative");
                }
                super(value);
                // ... more code
            }
        }
      
  • Advantages

    • Improved Readability: Code for validation or calculation is now located directly within the constructor, where it is most relevant.

    • Enhanced Safety: It allows for validation of constructor arguments before the superclass is initialized, preventing the creation of invalid objects.

    • Reduced Boilerplate: Eliminates the need for separate static helper methods, making the code more concise

  • Disadvantages

    • Potential for Confusion: Developers might get confused about which variables can and cannot be accessed before the super() call. The rule is simple (no instance fields), but it's a new mental model to adopt.

    • Minimal Impact on Day-to-Day Coding: This feature is primarily useful in a specific scenario—argument validation or calculation for the superclass constructor—and may not be used frequently by many developers.

Module Import Declarations

  • Module Import Declarations, a feature in Java 25, introduces a new way to import packages from a module. This provides an alternative to using multiple, repetitive import statements. The main goal is to simplify code and clarify dependencies by allowing a single declaration to import all public top-level classes and interfaces from a specified module.

  • Java code often needs a long list of import statements at the top of a file, especially when using large libraries. For example, a file that uses classes from the java.sql module might include:

      import java.sql.Connection;
      import java.sql.DriverManager;
      import java.sql.PreparedStatement;
      import java.sql.ResultSet;
      import java.sql.SQLException;
      // ... and so on
    
  • This verbose list can clutter the code, making it harder to read and maintain. While IDEs can manage these imports, the visual noise remains. The new feature addresses this by offering a more concise syntax for importing an entire module's public API.

  • How It Works

    • Instead of importing individual classes, you can now use a single import module declaration. This declaration makes all public top-level classes and interfaces within that module available in the current compilation unit.

        import module java.base;
      
        public class MyClass {
            // java.base contains java.util, so all classes are available
            List<String> list = new ArrayList<>();
            Map<String, String> map = new HashMap<>();
            // No need for individual imports
        }
      
  • Advantages

    • Concise: Reduces the number of import statements, making code cleaner and more readable.

    • Clarity of Dependencies: It's immediately clear that the code is using a specific module's entire API.

    • Simplicity for Scripting: For small scripts or single-file programs that use many classes from one module, this syntax is much more convenient.

  • Disadvantages

    • Potential for Naming Conflicts: Importing an entire module can introduce name clashes if multiple modules contain classes with the same name.

    • Loss of Granularity: You lose the ability to see exactly which classes are being used by a quick glance at the import list. This could make it harder to understand a file's dependencies at a glance.

    • Not for All Cases: This feature is most beneficial for importing foundational modules like java.base. For a mix of classes from many different modules, individual imports might still be the clearer choice.

Primitive Types in Patterns like instanceof and switch

  • Primitive Types in Patterns is a preview feature that extends Java's pattern matching to work with all primitive types. This change makes the pattern-matching framework more consistent and reduces the need for manual type-checking and casting.

  • Before this feature, pattern matching was mainly used with reference types. For example, you could use instanceof to check if an object was a String. Then, you could immediately use it as a String in the same expression:

      Object obj = "hello";
      if (obj instanceof String s) {
          System.out.println(s.length()); // 's' is already a String
      }
    
  • However, this did not work with primitive types. If you had a Number object, you would have to manually check its type and then unbox and cast it to a primitive:

      Object numberObj = 100;
      if (numberObj instanceof Integer) {
          int value = ((Integer) numberObj).intValue();
          System.out.println(value);
      }
    
  • This manual process is verbose and error-prone. Primitive Types in Patterns solves this by allowing pattern variables to be of a primitive type.

  • How It Works

    • This feature introduces a new form of pattern, a primitive type pattern, which can be used with instanceof and switch expressions. When a primitive type pattern matches, the value is automatically unboxed and assigned to the pattern variable.

        // Using instanceof
        Object obj = 100;
        if (obj instanceof int i) {
            System.out.println("The value is a primitive int: " + i);
        }
      
        Object obj2 = "text";
        if (obj2 instanceof String s) {
            System.out.println("The value is a String: " + s);
        }
      
        // Using switch
        Object obj = 3.14;
        String result = switch (obj) {
            case int i -> "It's an int: " + i;
            case double d -> "It's a double: " + d;
            default -> "It's something else.";
        };
        System.out.println(result); // Output: "It's a double: 3.14"
      
  • Advantages

    • Consistency: It makes the pattern-matching framework consistent across both reference and primitive types.

    • Reduced Boilerplate: It eliminates the need for explicit type checking, unboxing, and casting, making the code more concise and readable.

    • Improved Readability: The code becomes more declarative, expressing "what" you want to do (e.g., "if this is an int, assign it to i") rather than "how" to do it.

  • Disadvantages

    • Preview Feature: As a preview feature, it might not be suitable for production code. Its final form may change in future releases.

    • Potential for Confusion: The distinction between wrapper classes and primitive types can still be a source of confusion for new developers, even with the new syntax.

Let’s Cover The API and Performance Enhancements in the next blog.