Java 25’s compact source files and instance main methods (JEP 512) are fantastic for scripts and quick experiments. But as your program grows — adding tests, multiple types, or reuse from other files — it’s time to graduate to a regular class. The good news: the migration is almost entirely mechanical.
Let me walk you through it step by step.
1. Recognize When It’s Time to Migrate
Convert a compact file into a regular class when you notice any of these signs:
- The file exceeds ~100 lines or handles multiple concerns
- You need to reuse methods from other files
- You want to write unit tests targeting individual methods
- You’re introducing multiple top-level types (records, enums, nested classes)
- You need
publicAPI for other packages to consume - You want to package the code into a JAR or deploy it
2. Start With a Compact Source File
Here’s a typical compact program we’ll migrate:
final String title = "Tip Calculator";
void main() {
banner();
double bill = 84.50;
double tipPct = 0.18;
double tip = tipAmount(bill, tipPct);
double total = bill + tip;
printLine("Bill", bill);
printLine("Tip", tip);
printLine("Total", total);
}
double tipAmount(double bill, double pct) {
return round2(bill * pct);
}
double round2(double v) {
return Math.round(v * 100.0) / 100.0;
}
void banner() {
println("=== " + title + " ===");
}
void printLine(String label, double value) {
println(String.format("%-6s: %8.2f", label, value));
}
Save it as TipCalculator.java and run with java TipCalculator.java.
3. The Mechanical Migration Steps
Follow these steps in order — none of them require rethinking logic:
Step 1: Wrap Everything in a Class Declaration
Add a public class ClassName { ... } around all your code. The file name must match the class name (TipCalculator.java → class TipCalculator).
Step 2: Restore Explicit Imports
Compact files auto-import java.base and expose top-level IO methods (println, print, readln). A regular class needs explicit imports or fully qualified calls.
Replace:
println(...)→System.out.println(...)print(...)→System.out.print(...)readln(...)→ usejava.util.Scannerorjava.io.Console
Step 3: Promote main to the Traditional Form
The classic entry point signature is:
public static void main(String[] args)
You have two options:
- Simplest: make
mainpublic staticand add theString[] argsparameter. - Keep instance style: leave
mainas an instance method (still valid in Java 25) — but the traditional form is more familiar to most Java developers.
Step 4: Decide Which Members Stay Instance vs. Become Static
- Instance fields/methods → keep as instance members and create the object from
main. - Pure utility methods (no shared state) → mark
staticand call them directly frommain.
Step 5: Add Access Modifiers Where Appropriate
publicfor the class andmain.privatefor internal helpers.- Package-private (no modifier) for methods you may want to unit-test from the same package.
4. Two Migration Styles
Style A: Static-Method Class (Best for Utilities)
If your program is essentially a collection of pure functions, make everything static:
public class TipCalculator {
private static final String TITLE = "Tip Calculator";
public static void main(String[] args) {
banner();
double bill = 84.50;
double tipPct = 0.18;
double tip = tipAmount(bill, tipPct);
double total = bill + tip;
printLine("Bill", bill);
printLine("Tip", tip);
printLine("Total", total);
}
// ---------- domain logic ----------
private static double tipAmount(double bill, double pct) {
return round2(bill * pct);
}
private static double round2(double v) {
return Math.round(v * 100.0) / 100.0;
}
// ---------- output helpers ----------
private static void banner() {
System.out.println("=== " + TITLE + " ===");
}
private static void printLine(String label, double value) {
System.out.println(String.format("%-6s: %8.2f", label, value));
}
}
Notes:
- The
finalfield becameprivate static final(a constant, conventionally uppercase). println(...)becameSystem.out.println(...).- Helper methods became
private static. - Compile with
javac TipCalculator.javaand run withjava TipCalculator.
Style B: Instance-Based Class (Best for Stateful Programs)
If your program has meaningful state or you plan to inject configuration, prefer an instance-based design:
public class TipCalculator {
private final String title;
public TipCalculator(String title) {
this.title = title;
}
public static void main(String[] args) {
new TipCalculator("Tip Calculator").run();
}
public void run() {
banner();
double bill = 84.50;
double tipPct = 0.18;
double tip = tipAmount(bill, tipPct);
double total = bill + tip;
printLine("Bill", bill);
printLine("Tip", tip);
printLine("Total", total);
}
// ---------- domain logic ----------
double tipAmount(double bill, double pct) {
return round2(bill * pct);
}
double round2(double v) {
return Math.round(v * 100.0) / 100.0;
}
// ---------- output helpers ----------
private void banner() {
System.out.println("=== " + title + " ===");
}
private void printLine(String label, double value) {
System.out.println(String.format("%-6s: %8.2f", label, value));
}
}
Notes:
- A constructor accepts
title, enabling multiple configurations. mainis a tiny bootstrap that instantiates the class and callsrun().- The domain methods have package-private access, making them easy to unit-test.
5. A Side-by-Side Comparison
| Aspect | Compact source file | Regular class |
|---|---|---|
| Class declaration | Implicit | Explicit public class Name { ... } |
| File name | Any (java Foo.java) |
Must match class name |
main signature |
void main() |
public static void main(String[] args) |
| Imports | Auto (java.base) |
Explicit import statements |
| I/O helpers | println, print, readln |
System.out.println, Scanner, etc. |
| Access modifiers | Optional | Explicit (public, private, …) |
| How to run | java Foo.java |
javac Foo.java then java Foo |
| Testability | Limited | Full JUnit / Mockito support |
| Multiple types | Discouraged | Fully supported |
6. Final Checklist
Before you consider the migration complete:
- File name matches the public class name.
mainmethod ispublic static void main(String[] args).- All
println/readlncalls use standardSystem.out/ScannerAPIs (or explicitimport java.lang.IO;if you want to keep them). - Access modifiers reviewed (
public,private, package-private). - Constants renamed to
UPPER_SNAKE_CASEand markedprivate static final. - Compiles cleanly with
javac— no more relying on the source-launcher. - Consider extracting records, enums, or helper classes into their own files.
- Add unit tests for the newly exposed methods.
Summary
Migrating from a compact source file to a regular class in Java 25 is a mechanical, low-risk refactor:
- Wrap the code in
public class Name { ... }. - Restore explicit imports and
System.outcalls. - Upgrade
mainto the traditionalpublic static void main(String[] args)signature. - Decide between a static-utility style or an instance-based design.
- Add proper access modifiers and constants.
The beauty of Java 25 is that you can prototype with a compact file and graduate to a full class only when the program actually needs it — no rewrites, just a scaffolding upgrade.
