Blog

Hello, Java 25: Run a Java File Without the Ceremony

Java 25 runs a single source file with one command and a bare void main, with no class wrapper to write first. Learn what the JDK contains, why Java 25 is the version to learn, and how to read compiler errors and exceptions.

A Java program used to need five lines of ceremony before it could print a word. Java 25 cut that down to three. You write a void main(), call IO.println, and run the file with one command. The old form still works, and you’ll see it in almost every existing codebase, so this post shows both and explains what each removed piece was for.

This post covers what the JDK is and which version to get. Then it writes a first program, looks at what java Main.java does, reads a line of input, tries jshell, and reads a compile error and a runtime exception. Every program below was run on Java 25, and its output is pasted from the run. To run one yourself, save it as Main.java and run java Main.java.

The JDK: a compiler, a runtime and a set of tools

The JDK, or Java Development Kit, is the one download you need to write and run Java. It holds three kinds of thing:

  • The compiler, javac. It turns your .java source files into .class files full of bytecode, a compact set of instructions that isn’t tied to any one processor.
  • The runtime, started with java. It’s the Java Virtual Machine (JVM) plus the standard library. The JVM loads bytecode and runs it, and it compiles the busy parts to machine code while the program runs.
  • The tools. jshell for trying code line by line, jar for packaging, javadoc for documentation, jlink for building a trimmed-down runtime, and more.

Older tutorials tell you to install a JRE, the Java Runtime Environment, to run programs, and a JDK only to write them. That split mostly isn’t a separate download any more. Since Java 11, the OpenJDK project hasn’t published a standalone JRE. Some vendors and Linux distributions still package one, but the JDK contains everything the JRE did, so install the JDK.

Installing and checking

Install the JDK package from your operating system’s package manager, such as openjdk-25-jdk on Ubuntu, or download Eclipse Temurin 25 from adoptium.net, which keeps publishing updates for long-term support releases. Then two commands tell you which runtime and which compiler are on your path:

$ java -version
openjdk version "25.0.4" 2026-07-21
$ javac -version
javac 25.0.4

java -version prints two more lines naming the exact build, which differ between vendors, so only the first line is shown. Both numbers should start with 25. If they don’t match, you have more than one JDK installed and your path mixes them. Fix that first, or you’ll get errors that make no sense.

Why this series uses Java 25

Java ships a new version every six months, in March and September, and every two years one of them is marked LTS, for long-term support. An LTS release gets security and bug fixes for years. The others get fixes for six months, until the next version replaces them.

The recent LTS releases are 17, 21 and 25, with 11 and 8 before them. Most companies run an LTS release, so that’s what you’ll meet at work.

Java 25 is the current LTS, released in September 2025. It’s also the first LTS with the short program form this post uses. Everything in this series is final in Java 25, except one preview feature in the part on virtual threads, which says so. If you’re on Java 21, most of the series still applies, but the first program below won’t compile.

Your first Java program

A Java 25 program can be three lines long. Save this as Main.java:

void main() {
    IO.println("Hello, Java 25");
}

It prints:

Hello, Java 25

void main() is where the program starts, and void means it gives nothing back. IO.println prints a line of text. The braces { } hold the method’s body.

This is called a compact source file. Compact source files and the IO class became final features in Java 25. Ask the compiler to treat the same file as Java 24 and it refuses:

$ javac --release 24 Main.java
Main.java:1: error: implicitly declared classes are not supported in -source 24
void main() {
^
  (use -source 25 or higher to enable implicitly declared classes)
1 error

The message calls it an “implicitly declared class”, which is the old name from when the feature was in preview. The idea is the same: you didn’t write a class, so the compiler wrote one for you.

The same program before Java 25

For most of Java’s history, the smallest program looked like this:

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, Java 25");
    }
}

It prints:

Hello, Java 25

Java 25 still runs the long form, and you’ll read it in books, answers online and most existing projects. Here’s what each piece was doing:

  • public class Main { ... }: all Java code lives inside a class. The compact form still has one, because the compiler wraps your file in a class named after the file.
  • public on the class and the method: it makes them visible from outside, so the launcher is allowed to call main. From Java 25, the launcher no longer insists on public, so the compact form leaves it out.
  • static: the method belongs to the class, not to an object, so the launcher can call it without creating a Main first. From Java 25, main can be an ordinary method and the launcher creates the object for you.
  • String[] args: the words typed after the program’s name on the command line. You can still ask for them with void main(String[] args), but if you don’t use them, you don’t have to declare them.
  • System.out.println: System is a class, out is its standard output stream, and println prints a line to that stream. IO.println does the same job with less typing.

A compact source file also gets the whole java.base module imported automatically, so List, Map and friends are ready to use with no import line:

void main() {
    List<String> names = List.of("Ada", "Grace");
    IO.println(names);
}

It prints:

[Ada, Grace]

The same lines inside public class Main fail with cannot find symbol until you add import java.util.List;. This series uses the compact form throughout.

What java Main.java actually does

Running java Main.java compiles your source file in memory and then runs the result, all in one step. To watch it, take a program that declares a small record next to main. Records are covered in their own part. For now, read one as a named value with a method:

record Greeting(String name) {
    String text() {
        return "Hello, " + name;
    }
}

void main() {
    var g = new Greeting("Ada");
    IO.println(g.text());
}

It prints:

Hello, Ada

Run it in an empty folder and list the files before and after. Nothing new appears on disk:

$ ls
Main.java
$ java Main.java
Hello, Ada
$ ls
Main.java

The other way to run it is the one Java always had: compile with javac, which writes .class files, then point java at the compiled class:

$ javac -d classes Main.java
$ ls classes
Main$Greeting.class  Main.class
$ java -cp classes Main
Hello, Ada

-d classes tells javac where to put its output. -cp classes sets the class path, the list of places java looks for compiled classes. Notice that the last command names a class, Main, not a file.

Look at the file names. javac made one .class file per class, and the record became Main$Greeting.class. The $ means it’s nested: in a compact source file, everything you declare lives inside the class the compiler wrote for you. Leave out -cp classes and java Main looks in the current folder, finds no Main.class, and fails with Could not find or load main class Main.

So when do you still need javac? When you want compiled files to keep. A real application compiles once, packages its classes into a JAR and ships that. java Main.java compiles again on every run, which suits learning and small scripts. The part on packages, modules and JARs builds a project the long way.

Explain it like I’m ten

Imagine a recipe written in French, and a cook who only reads a special kitchen code.

javac is a translator who turns the whole recipe into kitchen code once and writes it on a card. You keep the card, and any cook in any kitchen can use it again and again.

java Main.java is a translator who reads the French recipe, translates it in their head and hands it straight to the cook. The cake gets made, but no card is left behind. Next time, the translating starts again from scratch.

The precise version

In both cases the same compiler does the same work. java Main.java runs the compiler inside the launcher, keeps the resulting classes in memory, loads them into the JVM and calls main. The javac route writes those same classes to disk, and a later java command loads them from there.

The “kitchen code” is bytecode, and the “cook” is the JVM. Bytecode doesn’t depend on your processor or operating system. A .class file compiled on Linux runs on a Mac or Windows JVM of the same version or newer.

Where the analogy breaks: a cook follows the card step by step, forever. The JVM doesn’t. It starts by interpreting bytecode, watches which methods run most, and compiles those into real machine code while the program is running. So there are two compilers: javac before the program runs, and the JVM’s just-in-time compiler during the run. The part on the JVM covers the second one.

A program in more than one file

The launcher can also run a program spread over several source files. You still name only the file with main, and it compiles the other files it needs from the same folder. Running a single source file directly arrived in Java 11. Running several arrived in Java 22.

$ cat Main.java
void main() {
    IO.println(Greeter.greet("Ada"));
}
$ cat Greeter.java
class Greeter {
    static String greet(String name) {
        return "Hello, " + name + ", from another file";
    }
}
$ java Main.java
Hello, Ada, from another file
$ ls
Greeter.java  Main.java

Main.java used Greeter, so the launcher found Greeter.java, compiled both in memory and ran the program. Again, no .class files were left behind.

Reading a line of input with IO.readln

IO.readln reads one line of text that the user types and returns it as a String. If you pass it a string, it prints that first as a prompt:

void main() {
    String name = IO.readln("What's your name? ");
    IO.println("Hello, " + name);
}

That program waits for a person to type, so its output can’t be checked automatically. Here are real runs, with the input piped in from the shell instead of typed:

$ echo Ada | java Main.java
What's your name? Hello, Ada
$ java Main.java < /dev/null
What's your name? Hello, null

The prompt and the greeting share a line. At a keyboard, the Enter you type is echoed and moves the cursor down. Piped input isn’t echoed, so nothing does. The program’s own output is the same either way.

The second run had no input at all, and readln returned null, which is Java’s value for “no object”. Joining null to a string gives the text null, so the program printed Hello, null instead of crashing. A real program should check for it.

Trying code in jshell

jshell is Java’s interactive shell. You type an expression or a statement, press Enter, and it shows you the result straight away, with no file and no main:

$ jshell
|  Welcome to JShell -- Version 25.0.4
|  For an introduction type: /help intro

jshell> 2 + 3
$1 ==> 5

jshell> 10 / 4
$2 ==> 2

jshell> 10 / 4.0
$3 ==> 2.5

jshell> var name = "Ada"
name ==> "Ada"

jshell> "Hello, " + name
$5 ==> "Hello, Ada"

jshell> name.toUpperCase()
$6 ==> "ADA"

jshell> /exit
|  Goodbye

Each result gets a name. If you didn’t name it, jshell makes one up, like $1, and you can use that name in later lines. Commands that start with a slash talk to jshell itself: /vars lists your variables, /help lists the rest, and /exit quits.

jshell doesn’t make you end a line with a semicolon, but a source file does. Notice 10 / 4 too. Dividing two whole numbers gives a whole number and drops the remainder. That surprises people coming from JavaScript or Python, and it’s exactly the kind of small question jshell answers quickly. The part on values and types explains it.

Compile errors: the build stops before anything runs

A compile error means javac couldn’t turn your source into bytecode, so not a single line of your program runs. A missing semicolon is the classic one:

void main() {
    IO.println("Hello")
    IO.println("Goodbye");
}

The build fails with:

Main.java:2: error: ';' expected
    IO.println("Hello")
                       ^

Read a javac error from left to right. Main.java:2 is the file and the line number. After error: comes what went wrong. The next line repeats your source line, and the caret ^ under it points at the exact spot where the compiler gave up. Here it’s right after the closing parenthesis, where a semicolon belonged.

“Hello” never printed, even though that line came first. With java Main.java, the launcher adds one last line, error: compilation failed, and stops.

Misspelled names give the most common error of all:

void main() {
    String name = "Ada";
    IO.printn("Hello, " + name);
}

The build fails with:

Main.java:3: error: cannot find symbol
    IO.printn("Hello, " + name);
      ^

javac prints two more lines under the caret: symbol: method printn(String) and location: class IO. Together they say “I looked in IO for a method called printn that takes a String, and there isn’t one”. The caret points at the dot, just before the name it couldn’t find. “Symbol” is the compiler’s word for any name: a variable, a method or a class.

When you get a screen full of errors, fix the first one and compile again. One missing brace can confuse the compiler about everything after it.

Runtime exceptions: the program starts, then stops

A runtime exception happens in a program that compiled fine. It starts, runs, and meets something it can’t do. Here the program tries to turn text that isn’t a number into an int:

void main() {
    String input = "forty-two";
    IO.println("Parsing " + input);
    int age = Integer.parseInt(input);
    IO.println("Next year you'll be " + (age + 1));
}

It prints, then stops:

Parsing forty-two
Exception in thread "main" java.lang.NumberFormatException: For input string: "forty-two"

Compare that with the compile errors above. This time the first line did print, because the program was running. Then Integer.parseInt threw a NumberFormatException, nothing caught it, and the program ended with a non-zero exit code. The last line never ran.

The exception line names the thread (main), the exception’s class and a message. Below it, Java prints a stack trace, a list of lines starting with at that show which method called which. The one to look for is the first line that mentions your own file, here at Main.main(Main.java:4). That’s the line in your code where things went wrong.

The compiler checks what it can know from the source: syntax, names and types. It can’t know that "forty-two" isn’t a number, because in a real program that text arrives while the program runs. Handling exceptions gets its own part.

Syntax basics: comments, statements, braces and case

Java’s syntax looks like C, C#, JavaScript and Go, so if you know any of those, this section is a quick check. This program uses all the pieces:

// A line comment runs to the end of the line.

/*
 * A block comment can span lines.
 */
void main() {
    int apples = 3; int pears = 4;   // two statements, one line: legal, but hard to read
    int total = apples
            + pears;                 // one statement, two lines: the semicolon ends it
    IO.println("total = " + total);
    {
        int inner = total * 2;
        IO.println("inner = " + inner);
    }
}

It prints:

total = 7
inner = 14
  • Comments. // runs to the end of the line, and /* ... */ can cover many lines. /** ... */ above a class or method is a documentation comment, read by javadoc.
  • Statements and semicolons. A statement is one instruction, and a semicolon ends it. Line breaks mean nothing to the compiler, so total is one statement over two lines, and the apples line holds two. Java has no automatic semicolon insertion, unlike JavaScript and Go.
  • Braces. { } group statements into a block: a method body, a loop body, or just a block on its own, like the one holding inner. A variable declared inside a block exists only inside it. Indentation is for people, and the compiler ignores it.
  • Case sensitivity. total, Total and TOTAL are three different names. By convention, variables and methods start with a lower-case letter, and classes start with a capital, like String and IO.

Case matters for main too. Capitalise it and the launcher has nothing to start:

void Main() {
    IO.println("Hello");
}

The build fails with:

Main.java:1: error: compact source file does not have main method in the form of void main() or void main(String[] args)

The message names the two forms it would have accepted. Writing string instead of String fails with cannot find symbol, because there’s no class called string.

Older examples online sometimes call a bare println("Hello") with no IO. in front. That worked while compact source files were a preview feature, in Java 23 and 24. In the final Java 25 it fails with cannot find symbol, so write IO.println.

What to remember

  • The JDK holds the compiler (javac), the runtime (java and the JVM) and tools like jshell. Install the JDK, and don’t look for a separate JRE.
  • Java 25 is the current LTS release. Check it with java -version and javac -version, and make sure both say 25.
  • A Java 25 program can be void main() { IO.println("Hello"); } in Main.java. The compiler writes the class around it, and java.base classes like List need no import.
  • java Main.java compiles in memory and runs, leaving no files. javac -d classes Main.java writes .class files, and java -cp classes Main runs them.
  • IO.readln reads a line of input, and returns null when there’s no more input.
  • A compile error stops the build before any line runs. Read the file, the line, the message and the caret. A runtime exception happens in a running program, after earlier lines have already run.
  • Statements end with a semicolon, braces make blocks, and names are case-sensitive.

Java 25 lets you start with three lines, and the compiler still checks every one of them before anything runs.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.