When I first started learning Java, I was bombarded with a number of words and acronyms that all sounded the same – JDK, JRE, JVM, J2EE, J2SE and so on….. what do they all mean?
This can often be intimidating for beginners, who often just skip ahead and jump straight into an IDE that hides all the details behind a pretty UI.
But I think it’s important to understand the ecosystem, so that you can understand what’s going on under the hood, and build better software.
Let’s start with the most basic building block of your Java application:
Bytecode
The first thing you should know is that the code you write in Java compiles into something called bytecode.
Bytecode is similar to binary or machine code, but unlike machine code, it can’t be executed directly by the CPU. Instead, it needs to be interpreted by a special program called a Java Virtual Machine.
For example, consider the following program stored in the file HelloWorld.java
:
package com.sohamkamani;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
We can use the javac
command to compile this program:
$ javac HelloWorld.java
This will generate a new file called HelloWorld.class
in the same directory, which contains the bytecode for our program.
💡 When you install Java on your machine, it’ll come bundled with a number of commands that you can use, like
javac
,javap
, andjava
. We’ll be using these commands throughout this article.
You can even see this bytecode by running the javap
command:
$ javap -c HelloWorld.class
Compiled from "HelloWorld.java"
public class com.sohamkamani.HelloWorld {
public com.sohamkamani.HelloWorld();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String Hello World!
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
Bytecode is not supposed to be human-readable, so don’t worry if you don’t understand what’s going on here. The important thing to note is that this is low level code that will be executed by a Java Virtual Machine.
Java Virtual Machine (JVM)
As we saw in the previous section, bytecode is not executable by the CPU. Instead, it needs to be interpreted by a special program – which is the Java Virtual Machine.
The JVM is a program that runs on your machine, and is responsible for executing your Java code. It’s also responsible for a number of other things, like memory management, garbage collection, and security.
You can think of the JVM as a translator that takes your bytecode and converts it into something that the CPU can understand.
But why do we need Bytecode and the JVM in the first place? Can’t we just compile our Java code into machine level code directly like other compiled languages?
The main value of the JVM is that it allows us to write code that can run on any machine, regardless of the underlying hardware or operating system. This means that as long as we have the JVM installed, we can be sure that our code will run the same way on any machine.
This is a huge advantage over other compiled languages like C or C++, where you need to compile your code separately for each platform, which can lead to a lot of compatibility issues.
Java Runtime Environment (JRE)
In our previous example, we ran this line of Java code:
System.out.println("Hello World!");
We often take this for granted, but let’s think about what’s actually happening here.
Here, System.out.println
is the function that prints our message to the console. But we never defined this function anywhere in our code. So where does it come from?
The answer is that it’s part of the Java Runtime Environment (JRE). The JRE is a collection of libraries and components that are responsible for making sure your Java code executes efficiently.
Note that the JRE also includes the JVM, so when you install the JRE on your machine, you’ll also get the JVM.
The JVM here, can be thought of as just an interpreter, reading instructions and executing them in the native OS. The program lifecycle itself, like scheduling, memory management, and security, is handled by the JRE.
Java Development Kit (JDK)
While the JRE is enough to run Java code, it doesn’t include the tools you need to develop Java code. For that, you’ll need the Java Development Kit (JDK).
For example, the javac
command that we used to compile our code is part of the JDK. The JDK also includes a number of other tools that you can use to develop Java code, like the javadoc
command for generating documentation, or the jshell
command for running Java code interactively.
The JDK also includes the JRE, so when you install the JDK on your machine, you’ll also get the JRE and, in turn, the JVM.
How Java Runs Your Code
Let’s go back to the example we saw earlier and see how the JDK, JRE, and JVM work together to run our code.
First, let’s see the directory structure of our project:
└── src
└── com
└── sohamkamani
└── HelloWorld.java
You’ll often see this directory structure in Java projects. The src
directory contains the source code for our project, and the HelloWorld.java
file contains the code we saw earlier.
When working with Java, the package name of your class should match the directory structure of your project. So in this case, the package name of our class is com.sohamkamani
, which means that the HelloWorld.java
file should be located in the src/com/sohamkamani
directory.
When we run the javac
command, the JDK will compile our code into bytecode, and save it in a file called HelloWorld.class
.
When we run the java
command, the JVM will load the bytecode from the HelloWorld.class
file, and execute it:
$ java -cp ./src/ com.sohamkamani.HelloWorld
The -cp
flag is used to specify the classpath, which is the location where the JVM will look for the bytecode. Or in other words, it is the location after which your folder structure should match the package name.
In this case, we’re telling the JVM to look for the bytecode in the src
directory, and run the com.sohamkamani.HelloWorld
class, which it will expect to find in the src/com/sohamkamani
directory.
What is Java SE and Java EE?
As we’ve seen previously, Java applications can’t be developed in isolation, and in almost all cases, you’ll need to use a number of libraries and components that are provided by the JRE.
Java SE (Standard Edition) and Java EE (Enterprise Edition) are two different sets of libraries that you can use in your Java code.
Java SE is always included with the JRE and JDK, and includes the core libraries that you’ll need to develop most Java applications.
Java EE is a set of libraries that are used for developing enterprise applications, like web applications or distributed applications. It includes a number of additional libraries that you can use in your code.
The word “enterprise” is more of a marketing term, which is used to describe a bunch of functionality needed by a large scale application, such as:
- Security - Authentication, authorization, and encryption, provided by jakarta.security.auth.message
- Concurrency - Multi-threading and parallel processing, provided by jakarta.enterprise.concurrent
- Persistence - Database access and object-relational mapping, provided by jakarta.persistence
- Messaging - Asynchronous communication between components, provided by jakarta.jms
- Web Servers - Web servers and servlet containers, provided by jakarta.servlet
And so on…
This is actually something that I really appreciate about the Java ecosystem – most of the libraries that you’ll need to develop a large scale application are already included in the standard library, so you don’t need to install a bunch of third party libraries to get started.
Java EE is now Jakarta EE
In 2017, Oracle announced that they would be transferring the Java EE platform to the Eclipse Foundation, where it would be developed under the name Jakarta EE.
This is why you’ll see the jakarta
prefix in the Java EE libraries, instead of the javax
prefix that was used in earlier versions.
So basically: J2EE, Java EE, and Jakarta EE are all different versions of the same library.
JAR Files
JAR files are a way to package multiple class files into a single file, which makes it easier to distribute and run your code. But why do we need it?
The Problem with Multiple Class Files
In the first example, we compiled our code into a file called HelloWorld.class
, and we were able to execute the class file directly using the java
command.
In reality though, we’ll have multiple class files in our project, and distributing them as separate files can be tricky.
To illustrate the issue, and why we need JAR files, let’s add a new HelloUniverse
class to our project:
package com.sohamkamani;
public class HelloUniverse {
public static void sayHello() {
System.out.println("Hello Universe!");
}
}
So, our folder structure looks like this:
.
└── src
└── com
└── sohamkamani
├── HelloUniverse.java
└── HelloWorld.java
And we can compile our code using the javac
command:
$ javac src/com/sohamkamani/*.java
This will generate two class files, HelloWorld.class
and HelloUniverse.class
in the same locations:
.
└── src
└── com
└── sohamkamani
├── HelloUniverse.class
├── HelloUniverse.java
├── HelloWorld.class
└── HelloWorld.java
And, we could run our code using the java
command:
$ java -cp ./src/ com.sohamkamani.HelloWorld
Hello World!
Hello Universe!
So far so good. But what if we wanted to distribute our code to someone else? We could just send them the HelloWorld.class
and HelloUniverse.class
files, and they could run it using the java
command, right?
Well, not exactly. These files need to be in the same directory structure as our package name, so we’d have to send them the entire src
directory, which is not ideal.
This gets even more complicated when we start using third party libraries, which will have their own directory structure and package names.
The Solution: JAR Files
This is where JAR files come in. JAR files are a way to package multiple class files into a single file, which makes it easier to distribute and run your code.
To create a JAR file, we can use the jar
command:
$ jar --create --file=app.jar --main-class=com.sohamkamani.HelloWorld -C src .
Let’s look at what each of these options mean:
--create
- Create a new JAR file--file=app.jar
- Specify the name of the JAR file that we want to create--main-class=com.sohamkamani.HelloWorld
- Specify the main class that we want to run. This needs to be the fully qualified name of the class that contains themain
method. Under the hood, this will create a special file namedMETA-INF/MANIFEST.MF
in the JAR file, which will tell the JVM which class to run when we execute the JAR file.-C src .
- Specify the location of the class files that we want to include in the JAR file. In this case, we’re telling thejar
command to look for the class files in thesrc
directory, and include them in the JAR file. Note that we usesrc
because that’s the location after which our folder structure matches the package name.
This will create a new file called app.jar
, which we can run using the java
command:
$ java -jar app.jar
Hello World!
Hello Universe!
Now, if we want someone else to run our code, we can just send them the single app.jar
file, and they can run it using the java
command.
The JAR file is actually just a ZIP file, and you can open it and explore its files using any ZIP utility program.
Third Party Tools
Everything we discussed so far is part of the standard Java ecosystem, and is included with the JDK. But there are also a number of third party tools that you should know about to develop Java applications.
Build Tools
Build tools, like Maven, Gradle, and Ant are used to automate the process of compiling and packaging your code.
Each of these tools have their own features and syntax, but they all work on a similar principle - you specify how you want to build your application in a configuration file, and then run the build tool to compile and package your code automatically.
The configuration file contains information like:
- The location of your Java files
- The location of your third party libraries
- The location where you want to save the compiled code, and packaged JAR file
- The version of the JDK that you want to use
This makes sure that everyone on your team is using the same version of the JDK, and that the code is compiled in the same way – which is especially important when you’re working on a large project with multiple developers.
IDEs
IDEs, like Eclipse, IntelliJ IDEA, and NetBeans are programs that provide an easy to use UI for developing Java applications.
In fact, most people who use an IDE don’t even need to know anything about the Java ecosystem, because the IDE will take care of everything for them.
For example, when you create a new project in IntelliJ IDEA, it will automatically create a build configuration file for you, and you can compile and run your code directly from the UI itself.
Personally, I prefer to use a text editor like VS Code or Sublime Text for writing code, and then use the command line to compile and run my code - if you’re interested, you can see the project structure and workflow that I like to use.
However, if you’re just getting started with Java, I’d recommend using an IDE, because it will make your life a lot easier.
Everything Else
Java has a rich ecosystem of tools and libraries, and there are a number of other tools that you might need to use, depending on the type of application you’re building.
Since Java is such a mature language, that means that there is almost certainly a library for whatever you’re trying to do.
For example, if you’re building a web application, you might want to use a web framework like Spring, or if you’re building a desktop application, you might want to use a GUI framework like JavaFX or Swing.
For unit testing, you might want to use a library like JUnit, and for logging, you might want to use a library like Log4j.