Add jep512 module: compact source files and instance main methods on JDK 25 and 27

Runnable sources, broken examples and captured transcripts for the ankurm.com JEP 512 article,
including the measured launch order and a private-constructor difference between 25 and 27.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a
This commit is contained in:
Claude
2026-09-24 10:32:47 +00:00
parent 6b5a918278
commit e0cb6e83e2
34 changed files with 490 additions and 0 deletions
+8
View File
@@ -56,6 +56,14 @@ Timings, RSS, GC counts and the Vector species are machine dependent; treat them
| 16 | [Stream Gatherers (JEP 485)](docs/16-gatherers.md) |
| 17 | [Scoped values vs ThreadLocal (JEP 506)](docs/17-scoped-values-migration.md) |
## Standalone article modules (no `docs/` folder)
These modules keep only sources, a `run.sh` and their transcripts (`output/`). The explanation lives in the article itself, in collapsible sections.
| Module | Article | What it demonstrates |
|---|---|---|
| [`jep512/`](jep512) | Compact Source Files and Instance Main Methods in Java 25 (JEP 512) | `java Hello.java` on JDK 21 vs 25 vs 27, the class `javac` builds around `void main()`, implicit `java.base` imports, `java.lang.IO`, the measured launch order, a private-constructor difference between 25 and 27 |
## Captured output (`docs/output/`)
| File | Produced by | What it is |
+18
View File
@@ -0,0 +1,18 @@
# jep512 — Compact Source Files and Instance Main Methods (JEP 512, final in Java 25)
Companion code for the ankurm.com article **Compact Source Files and Instance Main Methods in Java 25 (JEP 512)**. All explanation lives in the
article; this folder holds only the runnable sources and the transcripts they produced.
```bash
JDK25=/path/to/jdk-25 JDK27=/path/to/jdk-27 [JDK21=/path/to/jdk-21] ./run.sh # regenerates output/*.txt
java src/Hello.java # or just run one
```
| Folder | What is in it |
|---|---|
| `src/` | programs that run (`java src/Name.java`) |
| `broken/` | sources that fail on purpose; the compiler's own message is captured in `output/06-*.txt` and `output/07-*.txt` |
| `multi/` | `App.java` + `Greeter.java`: the source launcher finding a second file in the same directory |
| `output/` | every transcript quoted in the article (`01`-`08`) |
Machine-independent apart from the JDK build lines. Tested on Temurin 25.0.4.1+1 and Temurin 27+35.
+4
View File
@@ -0,0 +1,4 @@
class CtorWithArgs {
CtorWithArgs(int x) {}
void main() { IO.println("instance main, but no zero-argument constructor"); }
}
+3
View File
@@ -0,0 +1,3 @@
void main() {
println("hello");
}
+1
View File
@@ -0,0 +1 @@
void greet() { IO.println("hi"); }
+4
View File
@@ -0,0 +1,4 @@
void main() {
HttpClient client = HttpClient.newHttpClient();
IO.println(client);
}
+4
View File
@@ -0,0 +1,4 @@
void main() {
Target t = new Target();
t.main();
}
+4
View File
@@ -0,0 +1,4 @@
class PrivateCtor {
private PrivateCtor() {}
void main() { IO.println("instance main() reached through a PRIVATE zero-argument constructor"); }
}
+3
View File
@@ -0,0 +1,3 @@
class PrivateMain {
private static void main() { IO.println("private static main"); }
}
+1
View File
@@ -0,0 +1 @@
void main() { IO.println("Target"); }
+5
View File
@@ -0,0 +1,5 @@
package demo;
void main() {
IO.println("a package declaration in a compact file");
}
+1
View File
@@ -0,0 +1 @@
void main(int count) { IO.println("count = " + count); }
+3
View File
@@ -0,0 +1,3 @@
void main() {
IO.println(Greeter.greet("multi-file source launcher"));
}
+3
View File
@@ -0,0 +1,3 @@
class Greeter {
static String greet(String who) { return "Hello, " + who + " (from a second file)"; }
}
+22
View File
@@ -0,0 +1,22 @@
$ cat src/Hello.java
// The smallest complete Java 25 program. No class, no public, no static, no String[] args.
void main() {
IO.println("Hello, world");
}
$ java src/Hello.java (JDK 25: OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
Hello, world
$ java src/Hello.java (JDK 27: OpenJDK Runtime Environment Temurin-27+35 (build 27+35))
Hello, world
$ ls src/*.class
ls: cannot access 'src/*.class': No such file or directory
$ java src/Hello.java (JDK 21: OpenJDK Runtime Environment (build 21.0.10+7-Ubuntu-124.04))
src/Hello.java:2: error: unnamed classes are a preview feature and are disabled by default.
void main() {
^
(use --enable-preview to enable unnamed classes)
1 error
error: compilation failed
+22
View File
@@ -0,0 +1,22 @@
$ javac -d out src/Hello.java && java -cp out Hello
Hello, world
$ javap -p -cp out Hello
Compiled from "Hello.java"
final class Hello {
Hello();
void main();
}
$ javap -v -cp out Hello | grep -E 'major|flags'
major version: 69
flags: (0x0030) ACC_FINAL, ACC_SUPER
$ java Reflect (asks the loaded class)
name : Hello
final : true
public : false
package : '' (unnamed)
superclass : java.lang.Object
declared : [void Hello.main()]
constructors: [Hello()]
+25
View File
@@ -0,0 +1,25 @@
$ java src/Members.java
HELLO!
apple, banana, cherry
{apple=5, banana=6, cherry=6}
2.10
Point[x=3, y=4] is 5.0 from the origin
calls so far: 1
$ java src/WithArgs.java one two
args.length = 2
arg: one
arg: two
$ java src/InstanceMainInClass.java
hello from an instance field
this class is InstanceMainInClass, final? false
$ java src/NeedsImport.java (java.net.http is outside java.base: a normal import fixes it)
default HTTP version: HTTP_2
$ java src/NeedsModuleImport.java (or import the whole module)
default HTTP version: HTTP_2
$ java multi/App.java (App.java + Greeter.java in one directory)
Hello, multi-file source launcher (from a second file)
+22
View File
@@ -0,0 +1,22 @@
$ printf 'Ankur\n30\n' | java src/ReadLn.java
Your name? Your age? Hello Ankur, 30 is a good age to learn Java.
no newline after this, newline after this
$ java src/IoInAnyClass.java
IO.println works in a normal class with no import
$ java src/StaticImportIO.java (import static java.lang.IO.* makes the bare form work)
println without the IO. prefix, after import static java.lang.IO.*
$ java broken/NoIoPrefix.java (JDK 27: same error as on 25)
broken/NoIoPrefix.java:2: error: cannot find symbol
println("hello");
^
symbol: method println(String)
location: class NoIoPrefix
1 error
error: compilation failed
$ printf '' | java src/ReadLn.java (stdin closed: what does readln return?)
Your name? Your age? Hello null, null is a good age to learn Java.
no newline after this, newline after this
+22
View File
@@ -0,0 +1,22 @@
== OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
SS_i0 declares: static void main(String[] a) void main() -> ran: static main(String[])
S0_iS declares: static void main() void main(String[] a) -> ran: instance main(String[])
SS_S0 declares: static void main(String[] a) static void main() -> ran: static main(String[])
IS_I0 declares: void main(String[] a) void main() -> ran: instance main(String[])
-- same four classes, compiled first and launched as a class (java -cp out NAME)
SS_i0 -> ran: static main(String[])
S0_iS -> ran: instance main(String[])
SS_S0 -> ran: static main(String[])
IS_I0 -> ran: instance main(String[])
== OpenJDK Runtime Environment Temurin-27+35 (build 27+35)
SS_i0 declares: static void main(String[] a) void main() -> ran: static main(String[])
S0_iS declares: static void main() void main(String[] a) -> ran: instance main(String[])
SS_S0 declares: static void main(String[] a) static void main() -> ran: static main(String[])
IS_I0 declares: void main(String[] a) void main() -> ran: instance main(String[])
-- same four classes, compiled first and launched as a class (java -cp out NAME)
SS_i0 -> ran: static main(String[])
S0_iS -> ran: instance main(String[])
SS_S0 -> ran: static main(String[])
IS_I0 -> ran: instance main(String[])
+68
View File
@@ -0,0 +1,68 @@
$ java broken/NoMain.java (JDK 25)
broken/NoMain.java:1: error: compact source file does not have main method in the form of void main() or void main(String[] args)
void greet() { IO.println("hi"); }
^
1 error
error: compilation failed
$ java broken/NoIoPrefix.java (JDK 25)
broken/NoIoPrefix.java:2: error: cannot find symbol
println("hello");
^
symbol: method println(String)
location: class NoIoPrefix
1 error
error: compilation failed
$ java broken/NotInJavaBase.java (JDK 25)
broken/NotInJavaBase.java:2: error: cannot find symbol
HttpClient client = HttpClient.newHttpClient();
^
symbol: class HttpClient
location: class NotInJavaBase
broken/NotInJavaBase.java:2: error: cannot find symbol
HttpClient client = HttpClient.newHttpClient();
^
symbol: variable HttpClient
location: class NotInJavaBase
2 errors
error: compilation failed
$ java broken/WithPackage.java (JDK 25)
broken/WithPackage.java:1: error: compact source file should not have package declaration
package demo;
^
1 error
error: compilation failed
$ javac -d out broken/Other.java broken/Target.java (JDK 25; Target.java is a compact file)
broken/Other.java:2: error: Target is abstract; cannot be instantiated
Target t = new Target();
^
broken/Other.java:3: error: cannot find symbol
t.main();
^
symbol: method main()
location: variable t of type Target
2 errors
$ java broken/Other.java (JDK 25; source launcher, Target.java sits next to it)
broken/Other.java:2: error: Target is abstract; cannot be instantiated
Target t = new Target();
^
1 error
error: compilation failed
$ java broken/WrongParam.java (JDK 25)
broken/WrongParam.java:1: error: compact source file does not have main method in the form of void main() or void main(String[] args)
void main(int count) { IO.println("count = " + count); }
^
1 error
error: compilation failed
$ java broken/PrivateMain.java (JDK 25)
error: can't find main(String[]) or main() method in class: PrivateMain
$ java broken/CtorWithArgs.java (JDK 25)
error: can't find no argument constructor in class: CtorWithArgs
@@ -0,0 +1,8 @@
$ java broken/PrivateCtor.java (OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS))
instance main() reached through a PRIVATE zero-argument constructor
$ java broken/PrivateCtor.java (OpenJDK Runtime Environment Temurin-27+35 (build 27+35))
error: no non-private zero argument constructor found in class PrivateCtor
remove private from existing constructor or define as:
public PrivateCtor()
+26
View File
@@ -0,0 +1,26 @@
$ javap -p java.lang.IO
Compiled from "IO.java"
public final class java.lang.IO {
private static java.io.BufferedReader br;
private java.lang.IO();
public static void println(java.lang.Object);
public static void println();
public static void print(java.lang.Object);
public static java.lang.String readln();
public static java.lang.String readln(java.lang.String);
static synchronized java.io.BufferedReader reader();
}
$ javap -p -c java.lang.IO | grep -E 'public static|System.out|readLine|IOError'
public static void println(java.lang.Object);
0: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
public static void println();
0: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
public static void print(java.lang.Object);
0: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
10: invokevirtual #31 // Method java/io/PrintStream.flush:()V
public static java.lang.String readln();
3: invokevirtual #40 // Method java/io/BufferedReader.readLine:()Ljava/lang/String;
8: new #48 // class java/io/IOError
13: invokespecial #50 // Method java/io/IOError."<init>":(Ljava/lang/Throwable;)V
public static java.lang.String readln(java.lang.String);
Executable
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
# JEP 512 (Compact Source Files and Instance Main Methods): final in JDK 25, no --enable-preview.
# Regenerates every file in output/. Needs JDK 25 and JDK 27; a JDK 21 is optional (for the "before" transcript).
# JDK25=/path JDK27=/path [JDK21=/path] ./run.sh
set -uo pipefail
unset JAVA_TOOL_OPTIONS
HERE="$(cd "$(dirname "$0")" && pwd)"
JDK25="${JDK25:-/opt/jdks/jdk-25.0.4.1+1}"; JDK27="${JDK27:-/opt/jdks/jdk27}"; JDK21="${JDK21:-}"
OUT="$HERE/output"; mkdir -p "$OUT"; B="$(mktemp -d)"; trap 'rm -rf "$B"' EXIT
cd "$HERE"
v() { "$1/bin/java" -version 2>&1 | sed -n 2p; } # runtime line only, no vendor banner noise
# 01 - the source launcher: one command, no javac, no class file left behind
{
echo "\$ cat src/Hello.java"; cat src/Hello.java; echo
echo "\$ java src/Hello.java (JDK 25: $(v $JDK25))"; "$JDK25/bin/java" src/Hello.java 2>&1
echo; echo "\$ java src/Hello.java (JDK 27: $(v $JDK27))"; "$JDK27/bin/java" src/Hello.java 2>&1
echo; echo "\$ ls src/*.class"; ls src/*.class 2>&1 | sed 's/^/ /'
if [ -n "$JDK21" ]; then
echo; echo "\$ java src/Hello.java (JDK 21: $(v $JDK21))"; "$JDK21/bin/java" src/Hello.java 2>&1
fi
} > "$OUT/01-source-launcher.txt"
# 02 - javac produces an ordinary class file; javap shows what the compiler invented
{
echo "\$ javac -d out src/Hello.java && java -cp out Hello"
"$JDK25/bin/javac" -d "$B/o" src/Hello.java 2>&1 && "$JDK25/bin/java" -cp "$B/o" Hello 2>&1
echo; echo "\$ javap -p -cp out Hello"; "$JDK25/bin/javap" -p -cp "$B/o" Hello 2>&1
echo; echo "\$ javap -v -cp out Hello | grep -E 'major|flags'"
"$JDK25/bin/javap" -v -cp "$B/o" Hello 2>&1 | grep -E 'major version|^ flags' | head -2
echo; echo "\$ java Reflect (asks the loaded class)"
"$JDK25/bin/javac" -cp "$B/o" -d "$B/o" src/Reflect.java 2>&1 && "$JDK25/bin/java" -cp "$B/o" Reflect 2>&1
} > "$OUT/02-javac-and-javap.txt"
# 03 - the class body: members, nested types, implicit imports
{
echo "\$ java src/Members.java"; "$JDK25/bin/java" src/Members.java 2>&1
echo; echo "\$ java src/WithArgs.java one two"; "$JDK25/bin/java" src/WithArgs.java one two 2>&1
echo; echo "\$ java src/InstanceMainInClass.java"; "$JDK25/bin/java" src/InstanceMainInClass.java 2>&1
echo; echo "\$ java src/NeedsImport.java (java.net.http is outside java.base: a normal import fixes it)"; "$JDK25/bin/java" src/NeedsImport.java 2>&1
echo; echo "\$ java src/NeedsModuleImport.java (or import the whole module)"; "$JDK25/bin/java" src/NeedsModuleImport.java 2>&1
echo; echo "\$ java multi/App.java (App.java + Greeter.java in one directory)"; "$JDK25/bin/java" multi/App.java 2>&1
} > "$OUT/03-members-and-imports.txt"
# 04 - the IO helper
{
echo "\$ printf 'Ankur\n30\n' | java src/ReadLn.java"; printf 'Ankur\n30\n' | "$JDK25/bin/java" src/ReadLn.java 2>&1
echo; echo "\$ java src/IoInAnyClass.java"; "$JDK25/bin/java" src/IoInAnyClass.java 2>&1
echo; echo "\$ java src/StaticImportIO.java (import static java.lang.IO.* makes the bare form work)"; "$JDK25/bin/java" src/StaticImportIO.java 2>&1
echo; echo "\$ java broken/NoIoPrefix.java (JDK 27: same error as on 25)"; "$JDK27/bin/java" broken/NoIoPrefix.java 2>&1 | head -8
echo; echo "\$ printf '' | java src/ReadLn.java (stdin closed: what does readln return?)"
printf '' | "$JDK25/bin/java" src/ReadLn.java 2>&1
} > "$OUT/04-io-helper.txt"
# 05 - which main wins. Generate every pair of mains that can coexist and ask the launcher.
gen() { # name, body
printf 'class %s {\n%s\n}\n' "$1" "$2" > "$B/$1.java"
}
gen SS_i0 ' static void main(String[] a) { IO.println("static main(String[])"); }
void main() { IO.println("instance main()"); }'
gen S0_iS ' static void main() { IO.println("static main()"); }
void main(String[] a) { IO.println("instance main(String[])"); }'
gen SS_S0 ' static void main(String[] a) { IO.println("static main(String[])"); }
static void main() { IO.println("static main()"); }'
gen IS_I0 ' void main(String[] a) { IO.println("instance main(String[])"); }
void main() { IO.println("instance main()"); }'
{
for j in "$JDK25" "$JDK27"; do
echo "== $(v $j)"
for c in SS_i0 S0_iS SS_S0 IS_I0; do
printf '%-8s declares: %-58s -> ran: ' "$c" "$(grep -o '\(static \)\?void main([^)]*)' "$B/$c.java" | tr '\n' ' ')"
"$j/bin/java" "$B/$c.java" 2>&1 | head -1
done
echo "-- same four classes, compiled first and launched as a class (java -cp out NAME)"
rm -rf "$B/cls"; mkdir -p "$B/cls"; "$j/bin/javac" -d "$B/cls" "$B"/SS_i0.java "$B"/S0_iS.java "$B"/SS_S0.java "$B"/IS_I0.java 2>&1
for c in SS_i0 S0_iS SS_S0 IS_I0; do
printf '%-8s -> ran: ' "$c"; "$j/bin/java" -cp "$B/cls" "$c" 2>&1 | head -1
done
echo
done
} > "$OUT/05-launch-order.txt"
# 06 - every way to get it wrong, with the compiler's own words
{
for f in NoMain NoIoPrefix NotInJavaBase WithPackage Other WrongParam PrivateMain CtorWithArgs; do
if [ "$f" = Other ]; then
echo "\$ javac -d out broken/Other.java broken/Target.java (JDK 25; Target.java is a compact file)"
"$JDK25/bin/javac" -d "$B/x" broken/Other.java broken/Target.java 2>&1 | head -12
echo; echo "\$ java broken/Other.java (JDK 25; source launcher, Target.java sits next to it)"
"$JDK25/bin/java" broken/Other.java 2>&1 | head -12
else
echo "\$ java broken/$f.java (JDK 25)"; "$JDK25/bin/java" broken/$f.java 2>&1 | head -12
fi
echo
done
} > "$OUT/06-compile-errors.txt"
# 07 - the one behaviour that differs between 25 and 27
{
for j in "$JDK25" "$JDK27"; do
echo "\$ java broken/PrivateCtor.java ($(v $j))"
"$j/bin/java" broken/PrivateCtor.java 2>&1 | head -6; echo
done
} > "$OUT/07-private-constructor-25-vs-27.txt"
# 08 - what java.lang.IO actually is
{
echo "\$ javap -p java.lang.IO"; "$JDK25/bin/javap" -p java.lang.IO 2>&1
echo; echo "\$ javap -p -c java.lang.IO | grep -E 'public static|System.out|readLine|IOError'"
"$JDK25/bin/javap" -p -c java.lang.IO 2>&1 | grep -E 'public static|System.out|readLine|IOError|flush'
} > "$OUT/08-io-class.txt"
echo "wrote $OUT"
+6
View File
@@ -0,0 +1,6 @@
// The same program the way every tutorial before Java 25 wrote it, for the side-by-side in the post.
public class Classic {
public static void main(String[] args) {
System.out.println("Hello, world");
}
}
+4
View File
@@ -0,0 +1,4 @@
// The smallest complete Java 25 program. No class, no public, no static, no String[] args.
void main() {
IO.println("Hello, world");
}
+9
View File
@@ -0,0 +1,9 @@
// Instance main does not need a compact file. An ordinary class works too, and then it can use its own fields.
class InstanceMainInClass {
private final String greeting = "hello from an instance field";
void main() {
IO.println(greeting);
IO.println("this class is " + getClass().getName() + ", final? " + java.lang.reflect.Modifier.isFinal(getClass().getModifiers()));
}
}
+6
View File
@@ -0,0 +1,6 @@
// IO lives in java.lang, so IO.println needs no import in an ordinary class either.
public class IoInAnyClass {
public static void main(String[] args) {
IO.println("IO.println works in a normal class with no import");
}
}
+25
View File
@@ -0,0 +1,25 @@
// A compact source file is a whole class body: fields, methods, and nested types all sit at the top level.
// Nothing here is imported. List, Map, TreeMap, Collectors and BigDecimal all live in java.base.
String greeting = "hello";
int calls = 0;
record Point(int x, int y) {
double distance() { return Math.sqrt(x * x + y * y); }
}
String shout(String s) {
calls++;
return s.toUpperCase() + "!";
}
void main() {
List<String> words = List.of("banana", "apple", "cherry");
Map<String, Integer> lengths = new TreeMap<>();
for (String w : words) lengths.put(w, w.length());
IO.println(shout(greeting));
IO.println(words.stream().sorted().collect(Collectors.joining(", ")));
IO.println(lengths);
IO.println(new BigDecimal("1.10").add(BigDecimal.ONE));
IO.println(new Point(3, 4) + " is " + new Point(3, 4).distance() + " from the origin");
IO.println("calls so far: " + calls);
}
+8
View File
@@ -0,0 +1,8 @@
// java.net.http is a separate module from java.base, so HttpClient is NOT implicitly imported.
// A normal single-type import fixes it, exactly as in any other Java file.
import java.net.http.HttpClient;
void main() {
HttpClient client = HttpClient.newHttpClient();
IO.println("default HTTP version: " + client.version());
}
+7
View File
@@ -0,0 +1,7 @@
// The other fix: import the whole module (JEP 511, final in Java 25). One line covers java.net.http.
import module java.net.http;
void main() {
HttpClient client = HttpClient.newHttpClient();
IO.println("default HTTP version: " + client.version());
}
+8
View File
@@ -0,0 +1,8 @@
// java.lang.IO has three static methods: print, println and readln.
void main() {
String name = IO.readln("Your name? ");
String age = IO.readln("Your age? ");
IO.println("Hello " + name + ", " + age + " is a good age to learn Java.");
IO.print("no newline after this, ");
IO.println("newline after this");
}
+16
View File
@@ -0,0 +1,16 @@
// What did the compiler make of a compact source file? Ask the class itself.
import java.lang.reflect.Modifier;
import java.util.Arrays;
public class Reflect {
public static void main(String[] args) throws Exception {
Class<?> c = Class.forName("Hello");
IO.println("name : " + c.getName());
IO.println("final : " + Modifier.isFinal(c.getModifiers()));
IO.println("public : " + Modifier.isPublic(c.getModifiers()));
IO.println("package : '" + c.getPackageName() + "' (unnamed)");
IO.println("superclass : " + c.getSuperclass().getName());
IO.println("declared : " + Arrays.stream(c.getDeclaredMethods()).map(m -> m.toString()).sorted().toList());
IO.println("constructors: " + Arrays.toString(c.getDeclaredConstructors()));
}
}
+6
View File
@@ -0,0 +1,6 @@
// The bare println form works only if you import IO's static methods yourself.
import static java.lang.IO.*;
void main() {
println("println without the IO. prefix, after import static java.lang.IO.*");
}
+5
View File
@@ -0,0 +1,5 @@
// The String[] parameter is still allowed; it is just no longer required.
void main(String[] args) {
IO.println("args.length = " + args.length);
for (String a : args) IO.println(" arg: " + a);
}