Compact Source Files and Instance main

Write single-file Java programs without a class declaration or static main, know how the launcher picks a main method, and see when to move to a normal class.

Syntaxvoid main() { IO.println("hello"); } // run with: java Hello.java

Java 25 made compact source files and instance main methods final (JEP 512). A source file can now hold methods and fields with no class around them, and main no longer has to be public static or take String[]:

void main() {
    IO.println("ready");
}

What the compiler does for you

  • Every top-level method and field goes into an implicitly declared class. It is final, lives in the unnamed package, and your code cannot refer to it by name.
  • The file automatically imports every package exported by the java.base module, so List, Map, Path and Files need no imports.
  • java.lang.IO gives you IO.println, IO.print and IO.readln for console work.

Records, enums, interfaces and ordinary classes can still be declared in the same file, and you can still write explicit imports such as java.util.stream.Gatherers or java.net.http.HttpClient from other modules.

How the launcher picks main

The launcher prefers a main that takes String[]; only if there is none does it use a main() with no parameters. Either one may be static or an instance method. For an instance method the launcher creates the object with its no-argument constructor first, so that constructor must exist and must not be private. The rules also apply to ordinary named classes: a class Tool with a plain void main() runs fine.

Where it fits in back-end work

Compact files are ideal for the small programs around a service: a one-off data fix, a script that calls an endpoint and checks the answer, a load generator, a reproduction case for a bug report. Run them directly with java Fix.java, no build tool needed.

Mistakes people make

  • Expecting a package declaration to work. A compact source file is always in the unnamed package; once you need packages, tests or a build, convert it to a named class.
  • Treating top-level fields as globals. They are instance fields of the implicit class, initialised when the launcher creates the instance.
  • Forgetting that IO.readln returns null at the end of input, for example when a script runs in CI with nothing piped in.
  • Adding both main() and main(String[]) and expecting the no-argument one to run. The String[] version always wins.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • A one-file data repair script, run once with <code>java FixPrices.java</code> against a staging database export.
  • A smoke test that calls a deployed service's health and version endpoints after each release.
  • A minimal reproduction of a production bug, small enough to paste into an issue tracker.

More examples

Which main the launcher chooses

A <code>main</code> taking <code>String[]</code> is preferred over one with no parameters, whether static or not. Keep a single <code>main</code> per file to avoid surprises.

Example · java
static void main() {
    IO.println("static main with no parameters");
}

void main(String[] args) {
    IO.println("instance main with " + args.length + " arguments");
}
// Output:
// instance main with 0 arguments

Instance main in an ordinary named class

The launcher calls the no-argument constructor and then the instance <code>main</code>, so fields are ready to use without <code>static</code>. This is the usual shape once a script grows into a small tool.

Example · java
import java.util.List;

public class ExportTool {
    private final List<String> tables = List.of("customers", "orders");

    ExportTool() {
        IO.println("tool created");
    }

    void main() {
        for (String t : tables) {
            IO.println("exporting " + t);
        }
    }
}
// Output:
// tool created
// exporting customers
// exporting orders

Reading input safely with IO.readln

<code>IO.readln</code> prints the prompt and returns <code>null</code> at the end of input. Scripts that may run without a terminal should always handle that case.

Example · java
void main() {
    String name = IO.readln("Service name:");
    IO.println();
    if (name == null || name.isBlank()) {
        IO.println("no input, using default");
        name = "orders-api";
    }
    IO.println("checking " + name.strip());
}
// Output:
// Service name:
// no input, using default
// checking orders-api

Discussion

  • Be the first to comment on this lesson.