Running Source Files with java
Run Java source directly with java File.java, split a program across several files without a build tool, and know when to compile with javac instead.
java [jvm-options] Main.java [program-arguments]
java --source 25 script-without-extension [args]
javac -d out Main.java && java -cp out MainThe java launcher has two modes. Given a class name, it runs compiled bytecode. Given a file ending in .java, it switches to source-file mode: it compiles the file in memory and runs it, with no .class files left on disk.
java Main.java # compile in memory, then run
java Main.java 9090 prod # arguments after the file go to main
java -Dapp.env=prod Main.java # JVM options go before the file
Source-file mode arrived in Java 11. Since Java 22 the program can also span several files: when Main.java uses a class Greeter, the launcher looks for Greeter.java in the same directory and compiles it too. Packages work as directories, as they would with javac. The class that runs is the one named after the file, so Tool.java whose main class is called Main fails with "can't find class: Tool".
Order matters
Everything before the file name is for the JVM; everything after it is for your program. java Main.java -Dapp.env=prod does not set a system property: main simply receives the string -Dapp.env=prod in args. This catches people out in Dockerfiles and run scripts.
Libraries and newer language features
Add JAR files with --class-path (-cp) before the file name, for example java -cp 'lib/*' Main.java. Preview features need --enable-preview before the file name; when compiling with javac, add --release 25 --enable-preview.
Scripts with a shebang
On Linux and macOS a file whose first line is #!/path/to/java --source 25 can be marked executable and run directly. The file must not end in .java; with a .java name the launcher treats the #! line as source and compilation fails. Pass the same file explicitly with java --source 25 healthcheck.
When to compile instead
Source-file mode compiles on every run, which adds noticeable startup time on each launch. It is ideal for learning, scripts, cron-style jobs and quick tools. Services are compiled once by a build tool (Maven or Gradle run javac for you), packaged into a JAR and started with java -jar app.jar. The manual equivalent is:
javac -d out src/*.java
java -cp out Main
A common surprise: java Main (no extension) means "run the compiled class Main", and fails with "Could not find or load main class" if you never ran javac.
Example
When to use it
- Run a database migration helper or data fix as java Fix.java on a bastion host without a build.
- Keep a small operations tool in two or three source files in a repository and run it directly.
- Track down why a -Dspring.profiles.active or -Xmx setting in a start script is being ignored.
More examples
A program split across two files, no build tool
The launcher found Greeter.java next to App.java and compiled both in memory, which works since Java 22. No .class files were written, as the directory listing shows.
void main() throws Exception {
Path dir = Files.createTempDirectory("multi-file");
Files.writeString(dir.resolve("App.java"), """
class App {
public static void main(String[] args) {
System.out.println(Greeter.greet("web"));
}
}
""");
Files.writeString(dir.resolve("Greeter.java"), """
class Greeter {
static String greet(String name) { return "Hello, " + name; }
}
""");
// Equivalent to typing "java App.java" inside that directory.
String javaCommand = ProcessHandle.current().info().command().orElseThrow();
Process process = new ProcessBuilder(javaCommand, "App.java")
.directory(dir.toFile()).redirectErrorStream(true).start();
IO.println(new String(process.getInputStream().readAllBytes()).strip());
IO.println("exit code: " + process.waitFor());
try (var files = Files.list(dir)) {
IO.println("files on disk: " + files.map(p -> p.getFileName().toString()).sorted().toList());
}
}
// Output:
// Hello, web
// exit code: 0
// files on disk: [App.java, Greeter.java]JVM options before the file, program arguments after
The second command puts -D after the file name, so the JVM never sees it and the program receives it as a plain argument instead. Check this first when a setting in a run script appears to have no effect.
String run(Path dir, String... command) throws Exception {
Process process = new ProcessBuilder(command).directory(dir.toFile()).redirectErrorStream(true).start();
String output = new String(process.getInputStream().readAllBytes()).strip();
process.waitFor();
return output;
}
void main() throws Exception {
Path dir = Files.createTempDirectory("args-demo");
Files.writeString(dir.resolve("Server.java"), """
void main(String[] args) {
IO.println("env=" + System.getProperty("app.env", "unset") + " args=" + List.of(args));
}
""");
String javaCommand = ProcessHandle.current().info().command().orElseThrow();
IO.println(run(dir, javaCommand, "-Dapp.env=prod", "Server.java", "9090"));
IO.println(run(dir, javaCommand, "Server.java", "9090", "-Dapp.env=prod"));
}
// Output:
// env=prod args=[9090]
// env=unset args=[9090, -Dapp.env=prod]Compile once with javac, then run the class
This is the two-step path a build tool automates: javac writes class files once, and java runs them by class name from the class path. Compiled classes start faster because nothing is compiled at launch.
void main() throws Exception {
Path dir = Files.createTempDirectory("javac-demo");
Files.writeString(dir.resolve("Report.java"), """
public class Report {
public static void main(String[] args) {
System.out.println("report ready");
}
}
""");
// javac -d out Report.java
var javac = java.util.spi.ToolProvider.findFirst("javac").orElseThrow();
int compiled = javac.run(System.out, System.err, "-d", dir.resolve("out").toString(),
dir.resolve("Report.java").toString());
IO.println("javac exit code: " + compiled);
IO.println("class file written: " + Files.exists(dir.resolve("out/Report.class")));
// java -cp out Report (a class name, not a file name)
String javaCommand = ProcessHandle.current().info().command().orElseThrow();
Process process = new ProcessBuilder(javaCommand, "-cp", "out", "Report")
.directory(dir.toFile()).redirectErrorStream(true).start();
IO.println(new String(process.getInputStream().readAllBytes()).strip());
IO.println("exit code: " + process.waitFor());
}
// Output:
// javac exit code: 0
// class file written: true
// report ready
// exit code: 0
Discussion