Can you write a CLI using Java?
Table of content
A recent conversation got me curious about how people feel about writing command line applications in Java.
I knew that Java for many isn’t the first choice when thinking of building a CLI, but I was curious if people consider it an option at all. I started a poll on Twitter. Only 19 people participated, but the outcome was quite clear:
Java is a good choice to write command line applications
Option | Votes |
---|---|
Agree | 5.3% |
Disagree | 68.4% |
No way, are you crazy | 26.3% |
If you asked me several years ago I’d have had a similar reaction, but in 2022 I’m not so sure anymore.
When choosing a programming language for a given problem I tend to look at various aspects. Most importantly if the programming language can meet the applications requirements. I think the biggest weight should be on whether there are good libraries for a given problem domain, but there are some general requirements specific for CLI applications. A few of those come to mind:
- Fast startup time
- Reasonable file size
- Good deployment story and possibility to target the main platforms: MacOS, Linux, Windows
- A good command line library
Startup time ¶
Java is notorious for having a slow startup time, but how bad is it?
Humans perceive anything below 100ms as instant. That makes a good
upper boundary. What would be a lower boundary? Let’s find out by
writing some c
and measuring the time using hyperfine.
Startup of a c
application
¶
#include <stdio.h>
int main()
{
("Hello world\n");
printfreturn 0;
}
> gcc main.c
> hyperfine ./a.out
Benchmark 1: ./a.out
Time (mean ± σ): 0.5 ms ± 0.1 ms [User: 0.4 ms, System: 0.0 ms]
Range (min … max): 0.4 ms … 0.9 ms 4013 runs
That’s pretty quick and kinda where I’d expect it to be considering the work it has to do. How does Java fare in comparison?
Startup of a java
application
¶
I set up a small gradle project with
gradle init --type java-application
and adjusted the
generated App
:
package demo;
public class App {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
I updated the build.gradle
to include a
Main-Class
entry in the manifest of the jar
and built a jar
file using ./gradlew jar
before running hyperfine:
hyperfine "java -jar app/build/libs/app.jar"
The results:
Benchmark 1: java -jar app/build/libs/app.jar
Time (mean ± σ): 92.0 ms ± 13.4 ms [User: 85.7 ms, System: 19.1 ms]
Range (min … max): 63.6 ms … 108.2 ms 27 runs
Thats pretty bad compared to the c
example and close to
the upper boundary.
Startup of a
java
application using GraalVM
¶
Luckily we can do better than that. GraalVM supports ahead of time compilation, significantly reducing the startup time of an application written in Java.
I adjusted the build.gradle
file to include the
GraalVM
native-build-tools:
plugins {
id 'application'
id 'org.graalvm.buildtools.native' version '0.9.11'
}
After including this plug-in it’s possible to generate a native
executable using ./gradlew nativeCompile
.
hyperfine -N ./app/build/native/nativeCompile/app
Benchmark 1: ./app/build/native/nativeCompile/app
Time (mean ± σ): 1.8 ms ± 0.3 ms [User: 0.9 ms, System: 0.7 ms]
Range (min … max): 1.2 ms … 2.6 ms 1349 runs
Still slower than the c
version, but much better. There
should be plenty of legroom for application logic before reaching the
100ms
threshold.
Startup of a go
application
¶
Let’s look at one other contender and create a go
hello
world application:
package main
import "fmt"
func main() {
.Println("Hello world");
fmt}
> go build
> hyperfine -N ./hello
Benchmark 1: ./hello
Time (mean ± σ): 1.3 ms ± 0.2 ms [User: 1.0 ms, System: 0.4 ms]
Range (min … max): 0.8 ms … 1.8 ms 1648 runs
A little bit faster, but not by much. This is comparable to the Java GraalVM native image version.
File size ¶
Disk space has become pretty cheap. I wouldn’t put too much weight on this, but depending on the use case it may matter.
Language | Size |
---|---|
c |
16K |
go |
1.7M |
java (jar) |
809 |
java (native image) |
12M |
Using a tool like upx it would
be possible to get the file size down further.
upx app/build/native/nativeCompile/app
decreases the file
size to 3.5M but increases the startup time to ~80ms, due to the added
decompression overhead.
Deployment ¶
Cross-platform support was one of the original Java “killer”
features. Build the jar
once and run it on any system that
has a JVM
. But startup time for the jar
was
slow and typing java -jar ..
makes for a poor user
interface. Most applications written in Java include shell scripts as
entry point to have a more convenient interface. Unless you force people
to have bash
on Windows you need at least two different
scripts. One for Windows and one for both Linux and MacOS. Far from
ideal.
The alternative we already looked at is building a native image. As we’ve seen this is possible with GraalVM, but without built-in cross-compilation support. This means you’ll have to setup Github Actions or a similar system to build native images for all systems.
The point here would go to Go
, which since 1.5 has
fantastic cross-compilation
support. As far as I’m aware of, among the best of any language.
Command line library ¶
Last but not least, if you built a command line application you
likely want to have good library support to parse command line
arguments. Ideally it also offers some support go generate
bash
and zsh
completions, to avoid having to
do that manually.
You probably also want to follow some command line interface guidelines and have
features like displaying a help page if the user provides no options,
-h
or --help
flag. A good command line library
makes that easy.
Most languages have such a library and Java is no exception. picocli checks pretty much all requirements.
Conclusion ¶
I’d argue that Java is competitive despite not topping out in the given criteria. If there are good Java libraries for your problem domain, and if your team consists of Java engineers, there is no compelling reason to use another language.
Ramping up on tooling and language quirks takes time. If using one language over another would bring significant productivity advantages I’d expect to see this better reflected on the market. We don’t even know for sure if programming languages have measurable impact on defect rates
Here a final overview with a few more languages. Note for Java jar, Python and Lua you’d probably have to count the JVM, Python and Lua interpreter for a fair comparison in file size.
Language | File size | Startup time (mean) |
---|---|---|
c |
16K | 0.5 ms |
go |
1.7M | 1.3 ms |
haskell |
9.9M | 1.2 ms |
java (jar) |
809 | 92.0 ms |
java (native image) |
12M | 1.8 ms |
lua |
21 | 1.2 ms |
python |
45 | 29.3 ms |