When you want to start programming using Java, you're most likely use Maven, Gradle, or an Integrated Development Environment (IDE) like IntelliJ or NetBeans. These tools has one thing in common, they're slow!
To start an empty Java project, Gradle takes more than 2 minutes.
BUILD SUCCESSFUL in 2m 38s
3 actionable tasks: 3 executed
Maven is better, although 57 seconds isn't exactly fast either.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 57.824 s
[INFO] ------------------------------------------------------------------------
In a complex Java project, we could argue this overhead is worth it. But, what if you don't want to start a complex Java project? What if you just want to try a Java library, test your programming logic, or trying out a new Java syntax?
For a simple project, Java's build-in program like javac
and java
is enough. You can start write a Java program with a text editor and a terminal. That's it!
In this article, I'll show three examples of using JDK tools and a terminal to run a Java program.
1. Running a self-contained Java program
The first example is a class that takes a String as an input and produce hash as an output. This class doesn't need any third-party library. This class only use Java's build-in classes.
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Md5Util1 {
public static void main(String[] args) throws NoSuchAlgorithmException {
String plainText = args[0];
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(plainText.getBytes());
byte[] digest = md.digest();
// Convert byte array to a hexadecimal String
StringBuilder sb = new StringBuilder(2 * digest.length);
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
System.out.println("MD5 Hash: " + sb);
}
}
Open a terminal to compile and run this program.
$ javac Md5Util1.java
$ java Md5Util1 "Hello, world!"
MD5 Hash: 6cd3556deb0da54bca060b4c39479839
The compilation takes 0.7 second. No need to run an IDE that takes a lot of memory. Nor does waiting the build system finished configuring something. Just write the code!
As a refresher, javac
is JDK's program that compile java
code to bytecode.
$ javac Md5Util1.java
$ ls
Md5Util1.java Md5Util1.class
java
is JDK's program that run a Java class.
$ man java
NAME
java - Java application launcher
SYNOPSIS
java [ options ] class [ argument... ]
Note that we don't use .class
extension, we just use the class name. Instead of java Md5Util1.class
, we use java Md5Util1
.
The argument(s) is passed to the Class's main method (public static void main(String[] args)
). In this case, "Hello, world!" is passed on Md5Util1
main method.
We'll explore the java
options in the next example.
Update:
Since Java 11, you can run java directly like this.
$ java Md5Util1 "Hello, world!"
MD5 Hash: 6cd3556deb0da54bca060b4c39479839
2. Running a JUnit test
Even in a simple project, you need to use a third-party library. Like JUnit. JUnit is the de-facto library for testing Java code. Let's modify the Md5Util1 to make it easier to test.
import java.security.MessageDigest;
public class Md5Util2 {
public static void main(String[] args) throws Exception {
String plainText = args[0];
String output = stringToMd5(plainText);
System.out.println("MD5 Hash: " + output);
}
public static String stringToMd5(String input) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(input.getBytes());
byte[] digest = md.digest();
// Convert byte array to a hexadecimal String
StringBuilder sb = new StringBuilder(2 * digest.length);
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
We've extracted the code for hashing in stringToMd5
method. Now, let's add the test class.
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class Md5Util2Test {
@Test
public void helloWorld() throws Exception {
// Given
String input = "Hello, world!";
String expectedOutput = "6cd3556deb0da54bca060b4c39479839";
// When
String actualOutput = Md5Util2.stringToMd5(input);
// Then
assertEquals(expectedOutput, actualOutput);
}
}
Again, open a terminal to compile and run this program.
$ javac Md5Util2.java Md5Util2Test.java
Md5Util2Test.java:1: error: package org.junit does not exist
import org.junit.Test;
This time, javac
shows an error! javac
doesn't know where to find the org.junit
package. We need to inform javac
where to find the the JUnit package and classes.
Java classes is bundled in a jar
file. So, we should download the junit.jar
file and attach it during compilation. We use -cp
option to include the path of the class (hence, class path).
Download the junit's jar file in the mvnrepository.com (or the junit website). In this example, I use JUnit 4.12.
$ javac -cp junit-4.12.jar Md5Util2.java Md5Util2Test.java
$ ls -al | grep ".class"
Md5Util2.class
Md5Util2Test.class
Now, let's run the test!
$ java -cp .:junit-4.12.jar org.junit.runner.JUnitCore Md5Util2Test
Exception in thread "main" java.lang.NoClassDefFoundError: org/hamcrest/SelfDescribing
at java.base/java.lang.ClassLoader.defineClass1(Native Method)
at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1017)
at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:174)
Oops, another error. This is why we should read the manual 😅.
JUnit require hamcrest.jar to run the test. So, heads over to mvnrepository.com to download the hamcrest.jar. In this example, I use version 1.3.
The class path can be used to in java
command too. Let's try to run the test again.
$ java -cp .:junit-4.12.jar:hamcrest-core-1.3.jar org.junit.runner.JUnitCore Md5Util2Test
JUnit version 4.12
.
Time: 0.01
OK (1 test)
Use colon
to separate classes in the class path. .:junit-4.12.jar:hamcrest-core-1.3.jar
means we add the current path (dot is the current path) and two other jar files to run the program.
When we run a JUnit test, we're actually running org.junit.runner.JUnitCore
class. We pass the target test class to the JUnitCore
arguments. In this case, Md5Util2Test
is the target test class.
At this point, you might be wondering.. Why we don't need to add MessageDigest
in the class path? Java already add JDK's standard classes in the class path. The classes is bundled as rt.jar
file. It's located in the /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/jre/lib
directory.
You can exract the rt.jar
by using jar -xf rt.jar
command.
$ tree -L 1
.
├── META-INF
├── apple
├── com
├── java
├── javax
├── jdk
├── org
├── rt.jar
└── sun
$ cd java
$ tree -L 1
.
├── applet
├── awt
├── beans
├── io
├── lang
├── math
├── net
├── nio
├── rmi
├── security
├── sql
├── text
├── time
└── util
Other than java.security.MessageDigest, you can see other useful Java classes, such as:
- java.math.BigDecimal
- java.time.Duration
- java.util.ArrayList
3. Running a Java program with multiple JAR dependencies
As you see in example 2, some JAR may depends on other JARs. The more JAR we use in the project, the more complicated the compilation and running command is. This is why build tools like Maven or Gradle exist.
That being said, we can still use JDK's tool to run the program. Let's use Retrofit library as example. Retrofit is a popular REST API library that uses Interface
as the API declaration. I use the sample code from Retrofit website verbatim to simplify the example.
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
import java.util.List;
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
The Repo
object from GitHub API is contains lots of information about a repository. In this case, we're only interested in Repo's id
and name
.
public class Repo {
public String id;
public String name;
}
Next, create GitHubRetrofit class that build the Retrofit instance.
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import java.util.List;
public class GitHubRetrofit {
private Retrofit retrofit;
private GitHubService service;
public GitHubRetrofit() {
retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
service = retrofit.create(GitHubService.class);
}
public Repo helloWorldRepo() throws Exception {
Response<List<Repo>> response = service.listRepos("octocat").execute();
return response.body()
.stream()
.filter(repo -> repo.name.equals("Hello-World"))
.findFirst()
.get();
}
}
Finally, we create the test class that test the GitHubRetrofit class.
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GitHubRetrofitTest {
@Test
public void repos() throws Exception {
// Given
CallGitHub callGitHub = new CallGitHub();
String expectedName = "Hello-World";
// When
Repo repo = callGitHub.helloWorldRepo();
// Then
assertEquals(repo.name, expectedName);
}
}
Even with this simple API call example, there are many classes and import statements. So, there should be several JAR files involved. Of course, we need the retrofit, junit, and hamcrest JAR. But, how do we know which JAR files to download in mavenrepository?
This is why we use Maven or Gradle. They manages dependencies for us. Maven uses Project Object Model (POM) to describe the dependencies. The POM file itself is available in the mvnrepository's Retrofit page. For example:
<dependencies>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.9.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
<scope>compile</scope>
</dependency>
</dependencies>
You can use Gradle to list out dependencies too, by using ./gradlew dependencies
command.
+--- com.squareup.retrofit2:converter-gson:2.9.0
| +--- com.squareup.retrofit2:retrofit:2.9.0
| | \--- com.squareup.okhttp3:okhttp:3.14.9
| | \--- com.squareup.okio:okio:1.17.2
| \--- com.google.code.gson:gson:2.8.5
\--- com.squareup.retrofit2:retrofit:2.9.0 (*)
Since this article is about building and running Java without Maven or Gradle, I won't explain more about this topic.
After doing some trial and error, we know that we need the following JAR files. I put it under the /libs
folder.
$ ls
GitHubRetrofit.java
GitHubRetrofitTest.java
GitHubService.java
Repo.java
libs
$ tree
.
├── converter-gson-2.9.0.jar
├── gson-2.8.5.jar
├── hamcrest-core-1.3.jar
├── junit-4.12.jar
├── okhttp-3.14.9.jar
├── okio-1.17.2.jar
└── retrofit-2.9.0.jar
0 directories, 7 files
That's a lot of JARs!
To compile the class, run this command.
$ javac -d classes -cp libs/\* *.java
The -d
option describes the destination directory. Since we're dealing with multiple files, it's tidier to group the class in a separate folder.
$ tree -L 2
.
├── GitHubRetrofit.java
├── GitHubRetrofitTest.java
├── GitHubService.java
├── Repo.java
├── classes
│ ├── GitHubRetrofit.class
│ ├── GitHubRetrofitTest.class
│ ├── GitHubService.class
│ └── Repo.class
└── libs
├── converter-gson-2.9.0.jar
├── gson-2.8.5.jar
├── hamcrest-core-1.3.jar
├── junit-4.12.jar
├── okhttp-3.14.9.jar
├── okio-1.17.2.jar
└── retrofit-2.9.0.jar
2 directories, 15 files
Finally, run the test by using this command.
$ java -cp classes:libs/\* org.junit.runner.JUnitCore GitHubRetrofitTest
JUnit version 4.12
Time: 1.355
OK (1 test)
With several JAR files, we could simplify the command by grouping the Java files, Class files, and JAR files in a separate folder.
In this scenario, this trick works nicely. But for more complex project the build system can handle this in more elegant way. For example, Maven has build a standard directory layout for organizing a Java project. You can execute build and run with a simpler command.
Summary
Don't be scared by the last example. For simple use case, like solving an algorithmic problem or running a test, building and running Java manually is simple and powerful.
Not only you're more aware of the syntax (No autocomplete to help you), you'll also appreciate the blazing fast compilation speed. This enables you to focus more on the code rather than tweaking build system or IDE configuration.
As a bonus, if you're a laptop user like I am, you'll recognize your laptop temperature is colder 😁 (Yay typing!).
Use Maven/Gradle if you need few dependencies. Rather than typing and remembering Java commands, you just need to remember few build system commands like test
or build
.
Finally, use IDE if you need to manage many dependencies (and its transitive dependencies). IDE is also useful in some cases, like method completion (so, you don't have to find Java docs in the internet) or breakpoint for debugging (you can do it with jdb
but it's more complicated).
In conclusion, use the right tool for the right job!
Credit
- Cover photo by Emma Smith
Top comments (2)
Thanks, just what I was looking for. I started out with IDEs and using Gradle and later Maven without ever learning the basics. Now I feel a little more comfortable just using barebones Java tools - and the build tools also make more sense!
I'm glad you found the article helpful! Indeed, for basic use cases, built-in Java tools are often sufficient 👍