Extremely Serious

Category: Java 25

Java 25 Compact Source Files: A New Simplicity with Instance Main Methods

Java continues evolving to meet the needs of developers—from beginners learning the language to pros writing quick scripts. Java 25 introduces Compact Source Files, a feature that lets you write Java programs without explicit class declarations, coupled with Instance Main Methods which allow entry points that are instance-based rather than static. This combination significantly reduces boilerplate, simplifies small programs, and makes Java more approachable while preserving its power and safety.

What Are Compact Source Files?

Traditionally, a Java program requires a class and a public static void main(String[] args) method. However, this requirement adds ceremony that can be cumbersome for tiny programs or for learners.

Compact source files lift this restriction by implicitly defining a final top-level class behind the scenes to hold fields and methods declared outside any class. This class:

  • Is unnamed and exists in the unnamed package.
  • Has a default constructor.
  • Extends java.lang.Object and implements no interfaces.
  • Must contain a launchable main method, which can be an instance method (not necessarily static).
  • Cannot be referenced by name in the source.

This means a full Java program can be as simple as:

void main() {
    IO.println("Hello, World!");
}

The new java.lang.IO class introduced in Java 25 provides simple convenient methods for console output like IO.println().

Implicit Imports for Compact Source Files

To keep programs concise, compact source files automatically import all public top-level classes and interfaces exported by the java.base module. This includes key packages such as java.util, java.io, java.math, and java.lang. So classes like List or BigDecimal are immediately available without import declarations.

Modules outside java.base require explicit import declarations.

Limitations and Constraints

Compact source files have some structural constraints:

  • The implicit class cannot be named or explicitly instantiated.
  • No package declarations are allowed; the class is always in the unnamed package.
  • Static members cannot be referenced via method references.
  • The IO class’s static methods require qualification.
  • Complex multi-class or modular programs should evolve into regular class-based files with explicit imports and package declarations.

This feature targets small programs, scripts, and educational use while preserving Java’s rigorous type safety and tooling compatibility.

Using Command-Line Arguments in a Compact Source File

Compact source files support standard command-line arguments passed to the main method as a String[] parameter, just like traditional Java programs.

Here is an example that prints provided command-line arguments:

void main(String[] args) {
    if (args.length == 0) {
        IO.println("No arguments provided.");
        return;
    }
    IO.println("Arguments:");
    for (var arg : args) {
        IO.println(" - " + arg);
    }
}

Save this as PrintArgs.java, then run it with:

java PrintArgs.java apple banana cherry

Output:

textArguments:
 - apple
 - banana
 - cherry

This shows how you can easily handle inputs in a script-like manner without boilerplate class syntax.

Growing Your Program

If your program outgrows simplicity, converting from a compact source file to a named class is straightforward. Wrap methods and fields in a class declaration and add imports explicitly. For instance:

import module java.base;

class PrintArgs {
    void main(String[] args) {
        if (args.length == 0) {
            IO.println("No arguments provided.");
            return;
        }
        IO.println("Arguments:");
        for (var arg : args) {
            IO.println(" - " + arg);
        }
    }
}

The logic inside main remains unchanged, enabling an easy migration path.

Conclusion

Java 25’s compact source files paired with instance main methods introduce a fresh, lightweight way to write Java programs. By reducing ceremony and automatically importing core APIs, they enable rapid scripting, teaching, and prototyping, while maintaining seamless interoperability with the full Java platform. Handling command-line arguments naturally fits into this new model, encouraging exploration and productivity in a familiar yet simplified environment.

This innovation invites developers to write less, do more, and enjoy Java’s expressive power with less friction.

Scoped Values in Java 25: Definitive Context Propagation for Modern Java

With the release of Java 25, scoped values come out of preview and enter the mainstream as one of the most impactful improvements for concurrency and context propagation in Java’s recent history. Designed to address the perennial issue of safely sharing context across method chains and threads, scoped values deliver a clean, immutable, and automatically bounded solution that dethrones the error-prone ThreadLocal for most scenarios.

Rethinking Context: Why Scoped Values?

For years, Java developers have used ThreadLocal to pass data down a call stack—such as security credentials, logging metadata, or request-specific state. While functional, ThreadLocal suffers from lifecycle ambiguity, memory leaks, and incompatibility with lightweight virtual threads. Scoped values solve these problems by making context propagation explicit in syntax, immutable in nature, and automatic in cleanup.

The Mental Model

Imagine code execution as moving through a series of rooms, each with its unique lighting. A scoped value is like setting the lighting for a room—within its boundaries, everyone sees the same illumination (data value), but outside those walls, the setting is gone. Each scope block clearly defines where the data is available and safe to access.

How Scoped Values Work

Scoped values require two ingredients:

  • Declaration: Define a ScopedValue as a static final field, usually parameterized by the intended type.
  • Binding: Use ScopedValue.where() to create an execution scope where the value is accessible.

Inside any method called within the binding scope—even dozens of frames deep—the value can be retrieved by .get(), without explicit parameter passing.

Example: Propagating User Context Across Methods

// Declare the scoped value
private static final ScopedValue<String> USERNAME = ScopedValue.newInstance();

public static void main(String[] args) {
    ScopedValue.where(USERNAME, "alice").run(() -> entryPoint());
}

static void entryPoint() {
    printCurrentUser();
}

static void printCurrentUser() {
    System.out.println("Current user: " + USERNAME.get()); // Outputs "alice"
}

In this sample, USERNAME is accessible in any method within the binding scope, regardless of how far it’s called from the entry point.

Nested Binding and Rebinding

Scoped values provide nested rebinding: within a scope, a method can establish a new nested binding for the same value, which is then available only to its callees. This ensures truly bounded context lifetimes and avoids unintended leakage or overwrites.

// Declare the scoped value
private static final ScopedValue<String> MESSAGE = ScopedValue.newInstance();

void foo() {
    ScopedValue.where(MESSAGE, "hello").run(() -> bar());
}

void bar() {
    System.out.println(MESSAGE.get()); // prints "hello"
    ScopedValue.where(MESSAGE, "goodbye").run(() -> baz());
    System.out.println(MESSAGE.get()); // prints "hello"
}

void baz() {
    System.out.println(MESSAGE.get()); // prints "goodbye"
}

Here, the value "goodbye" is only visible within the nested scope inside baz, while "hello" remains for bar outside that sub-scope.openjdk

Thread Safety and Structured Concurrency

Perhaps the biggest leap: scoped values are designed for modern concurrency, including Project Loom’s virtual threads and Java's structured concurrency. Scoped values are automatically inherited by child threads launched within the scope, eliminating complex thread plumbing and ensuring correct context propagation.

// Declare the scoped value
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

public static void main(String[] args) throws InterruptedException {
    String requestId = "req-789";
    usingVirtualThreads(requestId);
    usingStructuredConcurrency(requestId);
}

private static void usingVirtualThreads(String requestId) {
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        // Launch multiple concurrent virtual threads, each with its own scoped value binding
        Future<?> taskA = executor.submit(() ->
                ScopedValue.where(REQUEST_ID, requestId).run(() -> processTask("Task VT A"))
        );
        Future<?> taskB = executor.submit(() ->
                ScopedValue.where(REQUEST_ID, requestId).run(() -> processTask("Task VT B"))
        );
        Future<?> taskC = executor.submit(() ->
                ScopedValue.where(REQUEST_ID, requestId).run(() -> processTask("Task VT C"))
        );

        // Wait for all tasks to complete
        try {
            taskA.get();
            taskB.get();
            taskC.get();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

private static void usingStructuredConcurrency(String requestId) {
    ScopedValue.where(REQUEST_ID, requestId).run(() -> {
        try (var scope = StructuredTaskScope.open()) {
            // Launch multiple concurrent virtual threads
            scope.fork(() -> {
                processTask("Task SC A");
            });
            scope.fork(() -> {
                processTask("Task SC B");
            });
            scope.fork(() -> {
                processTask("Task SC C");
            });

            scope.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    });
}

private static void processTask(String taskName) {
    // Scoped value REQUEST_ID is automatically visible here
    System.out.println(taskName + " processing request: " + REQUEST_ID.get());
}

No need for explicit context passing—child threads see the intended value automatically.

Key Features and Advantages

  • Immutability: Values cannot be mutated within scope, preventing accidental overwrite and race conditions.
  • Automatic Cleanup: Context disappears at the end of the scope, eliminating leaks.
  • No Boilerplate: No more manual parameter threading across dozens of method signatures.
  • Designed for Virtual Threads: Plays perfectly with Java’s latest concurrency primitives.baeldung+2

Use Cases

  • Securely propagate authenticated user or tracing info in web servers.
  • Pass tenant, locale, metrics, or logger context across libraries.
  • Enable robust structured concurrency with context auto-inheritance.