Introducing Unnamed Classes and Simplified Main Methods in Java
Java has long been known for its verbosity, especially when it comes to writing simple scripts or small programs. With Java 21’s introduction of simplified main methods, developers can now write more concise and readable entry points for their applications. The feature is primarily to teach Java by just making it easier to start.
The Traditional Main Method
Traditionally, Java required a verbose main method declaration in an enclosing class:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
The New Simplified Approach
Now, Java allows a more streamlined syntax that reduces boilerplate code:
void main() {
System.out.println("Hello, World!");
}
No surrounding public class
, no public static
and no String[] args
- much easier to parse for a new
starter to the language.
This can be stripped back further from Java 24 using the new static methods from the IO class:
void main() {
IO.println("Hello, World!");
}
Scripting Superpowers
The new main method syntax comes with some exciting features that make Java more script-friendly:
- Direct Execution
You can now run Java files directly without explicit compilation as
java Helloworld.java
- Shell Script Compatibility
An ingenious trick allows you to make Java files executable - paste this comment into the top of the
java source file
///usr/bin/env java "$0" "$@" ; exit $?
. When added to the top of your Java file, this line:- Makes the file directly executable
- Remains valid Java code
- Works across most shell environments
///usr/bin/env java "$0" "$@" ; exit $? void main() { IO.println("Hello, World!"); }
and then:
$ chmod +x Helloworld.java $ ./Helloworld.java
Practical Benefits
- Easier for Beginners
- Reduced boilerplate
- More approachable syntax
- Lower entry barrier for new developers
- Scripting Workflow Improvements
- Direct file execution
- No need for separate compilation step
- Seamless integration with shell scripts
Third Party Libraries
How do you include these? Just standard Java classpath. Rather than create a fairly heavyweight maven or gradle project for a simple script lets just add the dependencies into the current directory and using the 3rd party jpm package manager. Install it and then install jfiglet to make a banner.
///usr/bin/env java "$0" "$@" ; exit $?
import com.github.lalyos.jfiglet.FigletFont;
void main(String[] args) {
try {
if (args.length == 0) {
print(FigletFont.convertOneLine("Hello world!"));
} else {
print(FigletFont.convertOneLine(args[0]));
}
} catch (java.lang.Exception e) {
System.err.println(e);
}
}
$ java --enable-preview -cp deps/* Helloworld.java
_ _ _ _ _ _ _
| | | | ___| | | ___ __ _____ _ __| | __| | |
| |_| |/ _ \ | |/ _ \ \ \ /\ / / _ \| '__| |/ _` | |
| _ | __/ | | (_) | \ V V / (_) | | | | (_| |_|
|_| |_|\___|_|_|\___/ \_/\_/ \___/|_| |_|\__,_(_)
You’ll notice in this version how the flexible main
signature can accept cli arguments as well so we can use it
to specify the text to output.
$ java --enable-preview -cp deps/* Helloworld.java "It is ten o'clock"
___ _ _ _ _ _ _
|_ _| |_ (_)___ | |_ ___ _ __ ___ ( ) ___| | ___ ___| | __
| || __| | / __| | __/ _ \ '_ \ / _ \|/ / __| |/ _ \ / __| |/ /
| || |_ | \__ \ | || __/ | | | | (_) | | (__| | (_) | (__| <
|___|\__| |_|___/ \__\___|_| |_| \___/ \___|_|\___/ \___|_|\_\
We need the enable-preview
switch until it is officially released in Java 25.
Performance
It is not super quick as there is still a hidden compile step going on under the covers but as a way to write scripts it works very well.
$ time java --enable-preview -cp deps/* Helloworld.java
_ _ _ _ _ _ _
| | | | ___| | | ___ __ _____ _ __| | __| | |
| |_| |/ _ \ | |/ _ \ \ \ /\ / / _ \| '__| |/ _` | |
| _ | __/ | | (_) | \ V V / (_) | | | | (_| |_|
|_| |_|\___|_|_|\___/ \_/\_/ \___/|_| |_|\__,_(_)
real 0m0.385s
user 0m0.811s
sys 0m0.084s
Comparing the two methods to make this run we can see that normal pre-compiling with javac
and then
executing with java
is faster.
$ javac --enable-preview --release 24 -cp deps/* Helloworld.java
$ time java --enable-preview -cp deps/*:. Helloworld
_ _ _ _ _ _ _
| | | | ___| | | ___ __ _____ _ __| | __| | |
| |_| |/ _ \ | |/ _ \ \ \ /\ / / _ \| '__| |/ _` | |
| _ | __/ | | (_) | \ V V / (_) | | | | (_| |_|
|_| |_|\___|_|_|\___/ \_/\_/ \___/|_| |_|\__,_(_)
real 0m0.083s
user 0m0.105s
sys 0m0.038s
Graal Native Image
As an aside how about seeing how fast we can make it run as a native image. Do the following:
- Compile the class as before
javac --enable-preview --release 24 -cp deps/* Helloworld.java
- Run the Graal native image tool
native-image --enable-preview -H:IncludeResources=".*/*.flf" -cp deps/*:. Helloworld
In addition to passing the dependencies as classpath arguments plus the classfile we also need to tell the native compiler that we want to include resources in this case files in the dependency jar needed at runtime. Miss these and the input stream used to read them fails.
$ time ./helloworld
_ _ _ _ _ _ _
| | | | ___| | | ___ __ _____ _ __| | __| | |
| |_| |/ _ \ | |/ _ \ \ \ /\ / / _ \| '__| |/ _` | |
| _ | __/ | | (_) | \ V V / (_) | | | | (_| |_|
|_| |_|\___|_|_|\___/ \_/\_/ \___/|_| |_|\__,_(_)
real 0m0.017s
user 0m0.006s
sys 0m0.011s
Very quick but we obviously trade the scripting benefits.