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:
- A public class whose name must match the filename
- A
public static void main(String[] args)signature with specific keywords - 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:
- Instance Main Methods —
main()no longer needs to bestatic, andString[] argsis optional - 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:
static void main(String[] args)— classic, highest prioritystatic void main()— newvoid main(String[] args)— instancevoid 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:
| Feature | Compact Source File | Regular Class |
|---|---|---|
| Class declaration | Auto-generated, hidden | Explicit |
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 earlier | Java 25 | |
|---|---|---|
| Minimum program | class + public static void main(String[]) | Just void main() { } |
| Auto-imports | None | java.base module |
| Top-level methods | Not allowed outside a class | ✅ In compact source files |
extends / implements | Full OOP | Not in compact source files |
| Backwards compat | N/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 →