Exploring Java 9's Module System
Java 9 came out a few months ago and it has brought some of the biggest changes to the Java language in a very long time. In this post we will take an introductory look at the new module system Java 9 delivered to us.
We'll explore the module system by creating a small app that will randomly generate a list of integers and then sort them using an object that implements the Sorter
interface. Each implementation of this interface will be provided to our application by a different module. We'll have one that provides a MergeSorter
, another that gives us a QuickSorter
and finally one that provides a HeapSorter
.
The focus of our post will be on the module system and how we bring these modules together though and not on the actual sorting algorithms.
Let's create the app module first. Create a folder inside of your project's src directory called com.abnormallydriven.sorter.common
, after that we'll create App.java
in the package com.abnormallydriven.sorter.app
just as we would if we were building this app without the module system. We'll give the class a static main method so that we can launch the app and just print out a simple message to start things off.
package com.abnormallydriven.sorter.app;
public class App {
public static void main(String[] args){
System.out.println("Hello Modular World!");
}
}
Now comes the new part where we will create a module. At the root of our com.abnormallydriven.sorter.common
folder alongside the com
folder we will need to create a file named module-info.java. This is our module declaration file. It will contain the declaration of our module as well as important information about any other modules that we expect as dependencies in order to compile and run. Initially our file will look very simple
module com.abnormallydriven.sorter.app {
}
Now that we have a module declared and some code written, we can compile our module.
javac --module-path out -d out/com.abnormallydriven.sorter.app src/com.abnormallydriven.sorter.app/module-info.java \ src/com.abnormallydriven.sorter.app/com/abnormallydriven/sorter/app/App.java
This will tell the java compiler to compile our module and place it in its "exploded" form in the out directory at our project root. It also includes the --module-path
option pointing to the out directory. For now you can think of the module path as something akin to the classpath only on steroids and without the hell that comes with a really complicated classpath. It is basically telling the compiler that any modules we need in order to compile our app module should be found in that location.
Now that our module is compiled we can package it into a jar that we can then execute.
jar --create --file mods/sorter-app.jar \
--module-version=1.0 \
--main-class=com.abnormallydriven.sorter.app.App \
-C out/com.abnormallydriven.sorter.app .
Running our newly built jar is almost the same as it has ever been, we only need to include a module path instead of a classpath and then a -m to specifiy the main module.
java --module-path mods -m com.abnormallydriven.sorter.app
Just like that we have a simple Java 9 app using the module system. This isn't very useful though since a module system isn't really necessary if all we have is a single module. With that thought in mind let's build the next module in our app. This module will be where we define our Sorter
interface and we'll call it the common
module.
We'll create a new folder in our src root directory named com.abnormallydriven.common
. This module will have two classes in it, the first, our Sorter
interface will be declared in the com.abnormallydriven.sorter.common
package and it will look like this
package com.abnormallydriven.sorter.common;
public interface Sorter {
public String getName();
public SortResult sort(int[] unsortedData);
}
The second class SortResult
will be a simple data class that is returned by our sort method.
package com.abnormallydriven.sorter.common;
public class SortResult {
public final long sortTimeMillis;
public final int[] unsortedData;
public final int[] sortedData;
public SortResult(long sortTime, int[] unsortedData, int[] sortedData){
this.sortTimeMillis = sortTime;
this.unsortedData = unsortedData;
this.sortedData = sortedData;
}
}
Once again everything we've done so far in creating our common module should be pretty familiar to you if you have previous Java experience, but just like our app module we'll create a module-info.java file at the top of our module's directory along side the com
folder and that is where the module system magic will come into play. We want our common module to make the Sorter
and SortResult
types available to other modules, to do that we'll need an exports statement in our module declaration.
module com.abnormallydriven.sorter.common {
exports com.abnormallydriven.sorter.common;
}
The exports statement will let the module system know that anything declared in our common package should be made available to code outside of the common module. In our simple example our common module consists of only that package but in a more complex app we might have many packages in our common module that we wish to keep hidden from the rest of the world. Classes that are only important to the inner workings of our module. This is the most important and most powerful feature of the module system. We can now truly encapsulate our code in a way that was not possible before Java 9.
Now that we have our common module we can compile and package it in the same manner that we did our app module.
javac -d out/com.abnormallydriven.sorter.common src/com.abnormallydriven.sorter.common/module-info.java \
src/com.abnormallydriven.sorter.common/com/abnormallydriven/sorter/common/SortResult.java \
src/com.abnormallydriven.sorter.common/com/abnormallydriven/sorter/common/Sorter.java
jar --create --file mods/sorter-common.jar \
--module-version=1.0 \
-C out/com.abnormallydriven.sorter.common .
We will then make a modification to our app module's module-info.java file to state that we want to make use of the common module.
module com.abnormallydriven.sorter.app {
requires com.abnormallydriven.sorter.common;
uses com.abnormallydriven.sorter.common.Sorter;
}
You can see that we have two new lines in our file. The first one is the requires statement. It tells the module system that our app module requires the common module as a dependency. This means that if the compiler can't find our common module then we can't compile our app module.
Then we have a uses statement. This states that our module uses the com.abnormallydriven.sorter.common.Sorter
service. This means that the app module expects to be able to use the ServiceLoader
object to get implementations of the Sorter interface provided by other module's on the module path that export implementations of Sorter
.
In our app's main method we'll add the following code to show how we might do that
Iterable<Sorter> sorters = ServiceLoader.load(Sorter.class);
int[] unsortedItems = getRandomlySortedArray();
for(Sorter sorter : sorters){
System.out.println("Now Sorting with..." + sorter.getName());
SortResult result = sorter.sort(unsortedItems);
System.out.println("Sort finished in: " + result.sortTimeMillis + " milliseconds.");
System.out.println(Arrays.toString(result.unsortedData));
System.out.println(Arrays.toString(result.sortedData));
System.out.println("*************");
System.out.println("");
}
This is another key component of the module system. We can now make use of the implementations provided by our dependency module's without actually knowing what exactly they are. Our simple app will eventually have 3 sorters available to it, but it won't actually need to know them by their class name.
With this in mind lets go create our mergesorter module and take a look at how we can provide an implementation of Sorter
.
We'll create the com.abnormallydriven.sorter.mergesort
folder in our src directory
and create the MergeSorter
class in the com.abnormallydriven.sorter.mergesort
package. This class will implement our Sorter
interface.
public class MergeSorter implements Sorter {
@Override
public String getName(){
return "Merge Sort";
}
@Override
public SortResult sort(int[] unsortedData){
System.out.println("Performing Merge Sort");
int[] workingArray = Arrays.copyOf(unsortedData, unsortedData.length);
long startTime = System.currentTimeMillis();
workingArray = mergeSort(workingArray);
long endTime = System.currentTimeMillis();
return new SortResult(endTime - startTime, unsortedData, workingArray);
}
private int[] mergeSort(int[] workingArray){
if(workingArray.length == 1){
return workingArray;
}
int middleIndex = workingArray.length / 2;//0 4 => 2, 0 5 => 2
int[] leftHalf = mergeSort(Arrays.copyOfRange(workingArray, 0, middleIndex));
int[] rightHalf = mergeSort(Arrays.copyOfRange(workingArray, middleIndex, workingArray.length));
return merge(leftHalf, rightHalf);
}
private int[] merge(int[] left, int[] right){
int[] mergedArray = new int[left.length + right.length];
int currentMergedArrayIndex = 0;
int currentRightIndex = 0;
int currentLeftIndex = 0;
while(currentRightIndex < right.length && currentLeftIndex < left.length){
if(right[currentRightIndex] <= left[currentLeftIndex]){
mergedArray[currentMergedArrayIndex++] = right[currentRightIndex++];
} else {
mergedArray[currentMergedArrayIndex++] = left[currentLeftIndex++];
}
}
while(currentRightIndex < right.length){
mergedArray[currentMergedArrayIndex++] = right[currentRightIndex++];
}
while(currentLeftIndex < left.length){
mergedArray[currentMergedArrayIndex++] = left[currentLeftIndex++];
}
return mergedArray;
}
}
Now let's look at the important part, the module-info.java file for our merge sorting module.
module com.abnormallydriven.sorter.mergesort{
requires com.abnormallydriven.sorter.common;
provides com.abnormallydriven.sorter.common.Sorter
with com.abnormallydriven.sorter.mergesort.MergeSorter;
}
First we require the common module because we'll need that in order to use the Sorter
interface. We've seen this before when we required it for our app module. The provides statement is new though. This is how our merge sorter module will provide an implementation of the Sorter
interface so that our app module can make use of it via the ServiceLoader
.
We would be able to repeat this process for our quick sort and heap sort modules, each of them providing another implementation of the Sorter
interface that could then be used in our application module. That's all for our very simple introduction to the module system.
If you'd like to learn more then I highly recommend you take a look at Java 9 Modularity. It provides a very thorough walk through of the entire module system including how to bring old projects into the module system and how to take advantage of all the new tools that come with the module system. There are many more new features that we didn't discuss in this post that can make your Java 9 apps smaller faster and more maintainable.
Thanks for reading and Happy New Year!