How do I Turn a Small Java 25 Program into a Regular Class?

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:

  1. Wrap the code in public class Name { ... }.
  2. Restore explicit imports and System.out calls.
  3. Upgrade main to the traditional public static void main(String[] args) signature.
  4. Decide between a static-utility style or an instance-based design.
  5. 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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.