Language features5 min read

Java Compact Source Files: Instance main Methods in JDK 25

Write and run JDK 25 compact source files, understand their implicit class and imports, and migrate a small program to an explicit Java class.

  • compact source files
  • main method
  • JDK 25

JDK 25 lets a source file declare fields and methods without wrapping them in an explicit class. The compiler still creates a final top-level class in the unnamed package, and the launcher still invokes a method on that class. Compact source files remove ceremony for small programs; they do not introduce top-level executable statements or a classless runtime model.

Compact source files and instance main methods became final in JDK 25. JDK 21 through 24 offered evolving preview versions under earlier names, so those releases require --enable-preview and their syntax or automatically imported APIs differ. The examples here target final JDK 25 behavior with no preview flags.

Write a field and an instance main method

Save this as Greeting.java:

String greeting = "Hello";

String message(String name) {
    return greeting + ", " + name;
}

void main(String[] args) {
    String name = args.length == 0 ? "world" : args[0];
    IO.println(message(name));
}

Run the source file directly:

java Greeting.java Duke

It prints Hello, Duke. You can also compile and launch it in two steps:

javac Greeting.java
java Greeting Duke

javac produces Greeting.class, choosing the file’s base name as the generated class name. That name is an implementation detail: source inside the compact file cannot use Greeting to construct another instance or qualify a static method.

The JDK 25 compact-source-file guide specifies that the implicit class is final, extends Object, implements no interfaces, has one zero-argument constructor, and belongs to the unnamed package and unnamed module. Unenclosed fields and methods become members with ordinary defaults, so greeting and message above are instance members.

The launcher chooses a launchable main

JDK 25 accepts a void main with either one String[] parameter or no parameters. It may be static or instance and need not be public. If both parameter shapes are accessible, the launcher prefers the String[] form. For an instance method, it first calls the implicit class’s zero-argument constructor and invokes main on that object.

void main() {
    IO.println("no arguments needed");
}

A compact source file must contain a launchable main; otherwise compilation fails. It cannot declare another constructor because its implicit class has only the generated zero-argument constructor.

Instance main methods are not limited to compact source files. An explicit class can also use one:

class Tool {
    void main(String[] args) {
        System.out.println(args.length);
    }
}

This can be a useful intermediate teaching step, but production conventions and frameworks may still expect the traditional public static void main(String[] args). Verify packaging, launch scripts, and tooling before changing an established entry point.

Compact files receive broad java.base imports

Every compact source file behaves as if it begins with:

import module java.base;

That imports all public top-level types from packages exported by java.base, not only java.lang. Types such as List, Map, Stream, and Collectors can therefore be used without their usual imports:

void main() {
    var lengths = Stream.of("ant", "badger")
        .collect(Collectors.toMap(
            Function.identity(),
            String::length
        ));
    IO.println(lengths);
}

The automatic import applies to type names, not static methods. IO.println must still name IO unless the method is imported explicitly. java.lang.IO itself is a JDK 25 API intended for basic line-oriented console input and output; existing applications may prefer their established logging or I/O abstractions.

Module imports can create ambiguous simple names. The JDK 25 module-import guide explains that an explicit single-type import can resolve an ambiguity by shadowing a module import.

Unnamed-package rules limit reuse

The implicit class cannot declare a package and cannot be referenced by name. A class in a named package cannot import or refer to a class in the unnamed package. These constraints make compact source files suitable for standalone programs, demonstrations, scripts, and small entry points—not reusable library APIs.

You can declare helper classes inside the same file, but growing dependencies, tests, public APIs, or multiple coordinated entry points are signs that explicit packages and classes will make ownership clearer. javadoc also cannot document the implicit class as an accessible API.

Grow the file into an ordinary class

Migration is mechanical because the compact members already have class-member semantics. Keep imports outside and wrap the members:

public final class Greeting {
    private final String greeting = "Hello";

    private String message(String name) {
        return greeting + ", " + name;
    }

    void main(String[] args) {
        String name = args.length == 0 ? "world" : args[0];
        IO.println(message(name));
    }
}

At that point you can add a package, an explicit constructor, interfaces, and normal visibility decisions. If the deployment tool requires the conventional entry point, add public static void main(String[] args) and create the application object there.

Compact syntax is valuable precisely because it grows into ordinary Java rather than forming a separate dialect. Use it while the program is one small unnamed-package entry point; introduce explicit structure when other code needs to name, construct, package, document, or depend on it.