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.
void main() {
IO.println("hello");
}
// run with: java Hello.javaJava 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.basemodule, soList,Map,PathandFilesneed no imports. java.lang.IOgives youIO.println,IO.printandIO.readlnfor 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
packagedeclaration 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.readlnreturnsnullat the end of input, for example when a script runs in CI with nothing piped in. - Adding both
main()andmain(String[])and expecting the no-argument one to run. TheString[]version always wins.
Example
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.
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 argumentsInstance 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.
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 ordersReading 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.
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