The modularity features in Java 9 are together referred to by the name Java Platform Module System (JPMS). It introduces a new language construct to create reusable components called modules. The Java Platform Module System makes it easy for developers to create contained units or components that have clearly established dependencies on other modules. With Java 9 modules, you can group certain types and packages into a module and provide it with the following information:
- Its name: This is a unique name for the module
- Its inputs: What does the module need and use? What's required for the given module to be compiled and run?
- Its outputs: What does this module output or export out to other modules?
I'll explain the input and output configuration shortly, but at a very high level, these three pieces of information are what you typically supply when you create a new module. Whenever developers need to create any components that are meant to be reusable, they can create new Java modules and provide this information to create a unit of code with a clear interface. Since a module can contain both its inputs and outputs specified formally, it adds a whole set of advantages compared to the classpath approach that we've critiqued so far.
Let's now look at the process of creating a module step by step. We'll look at it at a conceptual level now, and we'll cover the syntax in Chapter 2, Creating Your First Java Module. Let's say you want to create a reusable library and you've decided to put your code in a Java 9 module. Here are the steps you need to follow:
- Create a module and give it a name: Every module has a name associated with it, for the obvious purpose of referring to it. You can give a module any name that you'd traditionally give to types. All the rules you are already familiar with regarding Java package names apply here (so certain characters like '/' or '-' aren't allowed, but '_' or '.' are okay). The recommended way to name a module is to use the reverse domain name convention, similar to the way you name your packages. So, for example, if someone in Acme Corp wrote an analytics module, they'd probably name the module com.acme.analytics.
- Define the module inputs: Not many modules can realistically be self-sufficient. You'll often need to import types that aren't in your module. This is where the module input configuration comes into play. When you create a module, you explicitly need to declare which other modules you need for your code to work. You do that specifying which modules your module requires.
- Define the module outputs: We've seen that in a traditional JAR file system, placing Java types in a JAR file doesn't really mean anything and every public type is accessible to every other type in the classpath, irrespective of which JAR it is in. A module behaves differently. By default, every Java type you place in a module is accessible only to other types in the same module. Even if the type is marked public! In order to expose types outside the module, you need to explicitly specify which packages you want to export. From any module, you can only export packages that are in that module. Once you've exported a package, all types in that package are potentially accessible outside the module. This enables every Java module to clearly separate and hide internal packages that are to be used only inside the module and expose only types that are intended to be used externally. If a Java type is in a package that isn't exported, then no other type outside the module can import it, even if the type is public!
Note the difference between the things you export from a module (which are packages) and the things you import or require (which are other modules). Since we are exporting types from a module at a package level, why not require packages too? The reason is simple. When a module requires another module, it automatically gets access to all the packages that that module exports. This way, you don't have to specify every package that your module needs. Just the name of the module you depend on will suffice. We'll look at the access mechanisms in much more detail in Chapter 3, Handling Inter-Module Dependencies.
The following diagram illustrates the input and output definitions of a typical module:
JPMS was designed with two primary goals in mind:
- Strong encapsulation: We've seen the dangers of having every public class accessible to every other class in the classpath. Since every module declares which packages are public and isolates those which are internal, the Java compiler and runtime can now enforce these rules to make sure that none of the internal classes are being used outside the module.
- Reliable configuration: Since every module declares what it needs, the runtime can check whether every module has what it needs well before the application is up and running. No more wishing and hoping that all the required classes are available in the classpath.
You can guess how happy Jack and Amit would be to hear this! Thanks to strong encapsulation, Jack would just need to put all of his StringSorter code in a module and export just his public package. Thus, his internal package would be hidden and not accessible by default. And, thanks to reliable configuration, Amit can always confidently say whether a given set of modules have all their dependencies met before running the application.
In addition to these two core goals, there has been another important goal that the module system was designed for--to be scalable and easy to use even on huge monolithic libraries. As a validation of that, the Java 9 team went ahead and modularized what's pretty much the oldest and biggest Java code base they could get their hands on--the Java Platform itself. This task, something that ended up involving significant effort, was performed under the name Project Jigsaw.