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.

Syntaxjava [jvm-options] Main.java [program-arguments] java --source 25 script-without-extension [args] javac -d out Main.java && java -cp out Main

The 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

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

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.

Example · java
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.

Example · java
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.

Example · java
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

  • Be the first to comment on this lesson.