Since Java 11 you can run a .java file directly, with no separate compile step and no
.class file left behind. It sounds like a convenience for beginners, and it is, but it
also makes Java usable for the kind of quick script you would otherwise have written in Python.
The change
# Before Java 11 — two commands, one artefact on disk
javac Hello.java # produces Hello.class
java Hello
# Java 11 onwards — one command, nothing left behind
java Hello.java
Note the difference in what you pass. java Hello takes a class name and
looks for compiled bytecode. java Hello.java takes a file name — the
.java extension is what tells the launcher to compile first.
What actually happens
The launcher compiles the file in memory and runs it immediately. No .class file is
written, and the compilation is thrown away when the program exits.
Two things follow from that. Startup is slightly slower, because you pay for compilation on every run — irrelevant for a script, unacceptable for a server. And errors that a build would have caught now surface at launch, which is exactly what you want while experimenting.
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, " + (args.length > 0 ? args[0] : "world"));
}
}
java Hello.java # Hello, world
java Hello.java Folau # Hello, Folau
Arguments after the file name go to your program, not to the launcher — which is what makes real scripting possible.
The rules
Four constraints, and each has a reason:
- Everything must be in one file. The launcher compiles that file and nothing
else. You may declare several classes in it; you may not reference a class in another
.javafile beside it. - The first class in the file wins. Whichever class is declared first must have a
mainmethod — that is the entry point, regardless of the file name. - The file name need not match the class. This is the one rule that
relaxes:
java script.javaworks even if the class inside is calledHello. It is why shebang scripts can have no extension at all. - Only the JDK is on the classpath unless you add one with
--class-path. No Maven, no dependency resolution.
public class Report { // first class: this one runs
public static void main(String[] args) {
Formatter formatter = new Formatter();
System.out.println(formatter.line("done"));
}
}
class Formatter { // helper in the same file: fine
String line(String text) {
return "== " + text + " ==";
}
}
Shebang scripts
On Unix you can drop the extension entirely and make the file executable, which is where this stops being a curiosity:
#!/usr/bin/java --source 11
chmod +x wordcount
./wordcount notes.txt
The --source flag is mandatory in a shebang file. Without an extension the launcher
cannot tell it is source rather than a class name, so the flag is what disambiguates. It also lets
you pin the language level.
A shebang file is not valid Java — the first line is not legal syntax — so it can only be run this
way, never compiled with javac. That is a deliberate trade: it is a script, not a class.
A real script
public class WordCount {
public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.err.println("usage: wordcount <file>");
System.exit(1);
}
Map<String, Long> counts = Files.lines(Path.of(args[0]))
.flatMap(line -> Arrays.stream(line.toLowerCase().split("\\W+")))
.filter(word -> !word.isBlank())
.collect(Collectors.groupingBy(word -> word, Collectors.counting()));
counts.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(10)
.forEach(e -> System.out.printf("%6d %s%n", e.getValue(), e.getKey()));
}
}
That runs with java WordCount.java notes.txt and needs nothing installed beyond a
JDK. It is also a fair illustration of why the feature matters: streams
and collectors make Java competitive for this kind of
text-munging, and the compile step was the last thing making it inconvenient.
Adding a dependency anyway
"Only the JDK is on the classpath" is the rule, but it is a default rather than a hard limit. If you have a jar sitting somewhere, you can point at it:
# One jar
java --class-path lib/gson-2.10.1.jar Script.java
# A directory of them
java --class-path "lib/*" Script.java
# Or set it once in the shebang line
#!/usr/bin/java --source 11 --class-path /opt/lib/*
That works, and it is worth knowing for a script that needs to parse JSON. But it is also the point at which you should ask whether this is still a script. You are now managing a classpath by hand, with no version resolution and nothing recording which jars the script needs — the two problems a build tool exists to solve.
Where it fits
Use it for learning — typing an example and running it in one command removes a step that teaches you nothing. Use it for one-off scripts, build helpers and anything you would otherwise write as a shell script but want types for. Use it for a bug reproduction you are attaching to an issue, where a single self-contained file is worth a great deal.
Do not use it for anything real. There is no dependency management, no test framework, no packaging and no incremental build — the moment you want a second file or a library, you want a build tool. The feature is a scratchpad, and it is very good at being one.
Java 25 pushed this further with compact source files, where a script no longer needs a class or a
static main at all — see Module Imports and
Simple Source Files.
Next
The Java 11 migration guide is next — what was removed between 8 and 11, and what breaks when you upgrade.