Java 25 finalised two changes aimed squarely at the first hour of learning Java: a program no
longer needs a class declaration or a static main, and a single import can pull in a
whole module. Together they make a first Java file three lines long.
The first program, then and now
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world");
}
}
That version asks a first-day beginner to accept four concepts before printing a line:
visibility, static, arrays, and the System.out stream. None of them can be
explained yet, so they get taught as incantation.
void main() {
IO.println("Hello, world");
}
That is a complete, standard Java 25 program. No class, no static, no
String[] args, no System.out. It compiles with plain
javac and runs with java Hello.java.
What is actually happening
The compiler wraps the file in an implicitly declared class whose name comes from the file. You never write or reference that name, so it does not appear anywhere in your code — but it exists, and it is why this is a source-file feature rather than a language-wide change.
Three rules govern it:
- The file must declare a
mainmethod — eithervoid main()orvoid main(String[] args). It need not bestaticorpublic. - The implicit class is final and has no package. It cannot be referenced by other classes, which is why this suits scripts and not libraries.
- Everything else is ordinary Java. You can declare other methods, classes and records in the file.
record Order(String id, int quantity) { }
int total(List<Order> orders) {
return orders.stream().mapToInt(Order::quantity).sum();
}
void main() {
var orders = List.of(new Order("a", 2), new Order("b", 3));
IO.println("total: " + total(orders));
}
Methods and types sit at the top level of the file, and main is just one of them.
The IO class
void main() {
IO.println("What is your name?");
String name = IO.readln();
IO.println("Hello, " + name);
}
java.lang.IO is a small class with println, print and
readln. It is automatically available in a compact source file, so a beginner can read
input without meeting Scanner, System.in or checked exceptions on day
one.
It is deliberately minimal. For anything beyond simple console interaction you still use the real
APIs — IO exists so that the first week does not require them.
Module imports
The second change. import module M; imports every public package that module
exports:
import module java.base;
void main() {
List<String> names = new ArrayList<>(List.of("Ana", "Bo"));
Map<String, Integer> ages = new HashMap<>();
Path path = Path.of("data.txt");
Instant now = Instant.now();
IO.println(names.getFirst() + " " + ages.size() + " " + path + " " + (now != null));
}
java.base covers java.util, java.io,
java.nio.file, java.time, java.util.stream and much more — so
one line replaces the six or seven imports a typical file starts with.
In a compact source file, java.base is imported implicitly, so even that line is
optional for scripting.
Ambiguity, and what to do about it
Importing broadly reintroduces the problem wildcard imports have: two modules can export a type with the same simple name. The compiler reports it rather than guessing, and a single-type import wins over any module import:
import module java.base;
import java.util.List; // explicit import resolves any ambiguity
void main() {
List<String> names = List.of("Ana");
IO.println(names.size());
}
The classic collision is java.util.List against java.awt.List, which is
exactly why import java.util.*; plus import java.awt.*; has always been a
compile error waiting to happen.
Growing out of it
The design goal was that a program should grow from a script into an application without a rewrite, and that mostly holds. Adding a class declaration around the methods is the only structural change:
// The same program, now an ordinary class
public class Orders {
record Order(String id, int quantity) { }
static int total(List<Order> orders) {
return orders.stream().mapToInt(Order::quantity).sum();
}
public static void main(String[] args) {
var orders = List.of(new Order("a", 2), new Order("b", 3));
System.out.println("total: " + total(orders));
}
}
Three things change: the class declaration appears, the methods become static because
main is now static, and IO.println becomes
System.out.println. The logic is untouched.
That is a real improvement on the old situation, where a beginner's first program shared no structure at all with a real one. It is also the argument for teaching the compact form first: what you learn transfers, rather than being replaced.
Where to use this, and where not
Use it for learning, for scratch files, for the single-file scripts that Java 11 made runnable, and for a bug reproduction you want someone else to be able to run in one command.
Do not use it in an application. The implicit class has no package and cannot be referenced, so nothing else can use it; there is no way to organise a codebase out of files that cannot see each other. The moment you want a second file, you want ordinary classes and a build tool.
And be clear about what this does not change: it is not a new dialect and it does not simplify the language. Every one of the concepts it postpones — classes, static, packages — still has to be learned. It just no longer has to be learned in the first five minutes.
Next
Stream Gatherers is next — the extension point streams have been missing since Java 8.