Part 5 of 12

Compact Source Files & Instance Main Methods (JEP 512): Java as a Scripting Language

The Ceremony Problem

When a student writes their first Java program, they copy this boilerplate:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

Three layers of ceremony for one line of logic:

  1. A public class whose name must match the filename
  2. A public static void main(String[] args) signature with specific keywords
  3. The actual code, buried inside two levels of braces

This has been the #1 on-ramp friction point in Java for 30 years. Python, Kotlin, and Groovy all let you write print("Hello") in a .py/.kts/.groovy file and run it. Java could not — until now.


JEP 512: What It Does

JEP 512 introduces two related changes, both finalized in Java 25:

  1. Instance Main Methodsmain() no longer needs to be static, and String[] args is optional
  2. Compact Source Files (formerly “Unnamed Classes”) — a file can contain methods and statements at the top level, without a class declaration

The Simplest Java 25 Program

File: Hello.java

void main() {
    System.out.println("Hello, World!");
}

Run it:

java Hello.java
# Hello, World!

No class. No static. No String[] args. Just a main() method and your code.


What Is a Compact Source File?

When the Java compiler sees a source file where the top-level content is methods, fields, or statements — rather than a class/interface/record/enum declaration — it wraps the content in an implicitly declared class. You never see the class; the compiler creates it for you.

Under the hood, Hello.java above compiles as if you had written:

// What the compiler sees (you don't write this)
final class Hello {
    void main() {
        System.out.println("Hello, World!");
    }
}

The class name matches the filename, is final, and is only accessible from the same file.


Auto-Imports in Compact Source Files

Every compact source file gets import module java.base; automatically. You don’t need to write it. This means List, Map, Stream, Path, Optional, Scanner — everything from java.base — is available without any import statement:

void main() {
    var names = List.of("Alice", "Bob", "Charlie");
    names.stream()
         .filter(n -> n.length() > 3)
         .map(String::toUpperCase)
         .forEach(System.out::println);
}

Output:

ALICE
CHARLIE

No imports needed. No class. This is what Python-style scripting in Java looks like.


Top-Level Fields and Methods

A compact source file can have multiple methods and fields at the top level — they all belong to the implicitly declared class:

// calculator.java
int add(int a, int b)      { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
double divide(int a, int b) {
    if (b == 0) throw new ArithmeticException("Division by zero");
    return (double) a / b;
}

void main() {
    System.out.println("5 + 3 = " + add(5, 3));
    System.out.println("5 - 3 = " + subtract(5, 3));
    System.out.println("5 * 3 = " + multiply(5, 3));
    System.out.println("5 / 3 = " + divide(5, 3));
}
java calculator.java
# 5 + 3 = 8
# 5 - 3 = 2
# 5 * 3 = 15
# 5 / 3 = 1.6666666666666667

Instance Main Methods: The Full Flexibility

The main() method in Java 25 can take several forms. The JVM picks the “most specific” one it finds:

// Option 1: most traditional (still works, highest priority when multiple exist)
public static void main(String[] args) { ... }

// Option 2: no args (new in Java 25, final)
public static void main() { ... }

// Option 3: instance main with args
void main(String[] args) { ... }

// Option 4: instance main, no args (simplest — used in compact source files)
void main() { ... }

Priority when multiple main forms exist in the same class:

  1. static void main(String[] args) — classic, highest priority
  2. static void main() — new
  3. void main(String[] args) — instance
  4. void main() — instance, no args

This means existing code is 100% backwards compatible — the classic signature always wins.


Real-World Use Cases

1. Quick Data Processing Script

// process_csv.java
import java.nio.file.*;

void main() throws Exception {
    var file = Path.of(args()[0]);   // args() is available via System.getProperty tricks? 
    // Actually: use Scanner for stdin or pass args via standard main
    Files.lines(Path.of("data.csv"))
         .skip(1)  // skip header
         .map(line -> line.split(","))
         .filter(parts -> parts.length >= 2)
         .map(parts -> parts[0].trim() + " -> " + parts[1].trim())
         .forEach(System.out::println);
}

2. HTTP Probe Script

// check_url.java
import module java.net.http;

void main() throws Exception {
    var client = HttpClient.newHttpClient();
    var request = HttpRequest.newBuilder()
            .uri(URI.create("https://httpbin.org/get"))
            .GET()
            .build();

    var response = client.send(request, HttpResponse.BodyHandlers.ofString());
    System.out.println("Status: " + response.statusCode());
    System.out.println("Body length: " + response.body().length() + " chars");
}
java check_url.java
# Status: 200
# Body length: 312 chars

3. Teaching Example: FizzBuzz

// fizzbuzz.java
void main() {
    for (int i = 1; i <= 30; i++) {
        if (i % 15 == 0)      System.out.println("FizzBuzz");
        else if (i % 3 == 0)  System.out.println("Fizz");
        else if (i % 5 == 0)  System.out.println("Buzz");
        else                   System.out.println(i);
    }
}

This is now a complete, runnable Java file. No ceremony.


Compact Source File Limitations

Compact source files are not regular classes. They have real restrictions:

FeatureCompact Source FileRegular Class
Class declarationAuto-generated, hiddenExplicit
extends / implements❌ Not allowed
Constructors❌ Not allowed
Access modifiers❌ No public/private on methods
Inner classes❌ Not allowed
Use from other files❌ Cannot import
Fields✅ Top-level (become instance fields)
Methods✅ Top-level (become instance methods)

These restrictions make compact source files appropriate for single-file scripts and demos — not for large applications. For anything more than a few hundred lines, use a proper class.


Regular Classes Still Work

Nothing changes for regular Java code. All of the following are still valid in Java 25:

// Still perfectly valid Java 25
public class Application {
    public static void main(String[] args) {
        System.out.println("Traditional Java still works!");
    }
}

JEP 512 adds a new entry point for small programs. It does not change existing Java.


Summary

Java 24 and earlierJava 25
Minimum programclass + public static void main(String[])Just void main() { }
Auto-importsNonejava.base module
Top-level methodsNot allowed outside a class✅ In compact source files
extends / implementsFull OOPNot in compact source files
Backwards compatN/A✅ Classic signatures still work

Compact Source Files make Java viable as a scripting language for the first time. Combined with Module Import Declarations, a Java 25 script file can be just as concise as an equivalent Python or Groovy script — while still having full access to the Java ecosystem.


Next up: Primitive Types in Patterns →