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
public API 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(...) → use java.util.Scanner or java.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
main public static and add the String[] args parameter.
- Keep instance style: leave
main as 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
static and call them directly from main.
Step 5: Add Access Modifiers Where Appropriate
public for the class and main.
private for 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
final field became private static final (a constant, conventionally uppercase).
println(...) became System.out.println(...).
- Helper methods became
private static.
- Compile with
javac TipCalculator.java and run with java 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.
main is a tiny bootstrap that instantiates the class and calls run().
- 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.
main method is public static void main(String[] args).
- All
println / readln calls use standard System.out / Scanner APIs (or explicit import java.lang.IO; if you want to keep them).
- Access modifiers reviewed (
public, private, package-private).
- Constants renamed to
UPPER_SNAKE_CASE and marked private 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.out calls.
- Upgrade
main to the traditional public 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.