The Java Module System
Modules add a boundary above packages: a module declares what it needs and what it exposes, and everything else stays private.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
The problem it addresses
- A
publicclass was visible to everything on the classpath, so internal packages leaked into other people code. - The classpath was a flat list with no declared dependencies, so a missing library failed only when the class was first used.
- The runtime could not be trimmed, so a small application still shipped the whole platform.
The module system, introduced in Java 9, addresses all three by making dependencies and exposure explicit.
A module declaration
// module-info.java, at the root of the source tree
module com.example.notes {
requires java.sql; // what this module needs
requires transitive com.example.core; // and re-exports to its consumers
exports com.example.notes.api; // visible to everyone
exports com.example.notes.spi to com.example.plugins; // visible to one module
opens com.example.notes.model; // deep reflection allowed at runtime
uses com.example.notes.spi.Exporter; // a service consumed
provides com.example.notes.spi.Exporter
with com.example.notes.JsonExporter; // a service supplied
}The directives
| Directive | Meaning |
|---|---|
requires | This module depends on another |
requires transitive | Anyone requiring this module also gets that one |
requires static | Needed at compile time, optional at runtime |
exports | The public types of a package are visible outside |
exports ... to | Visible only to the named modules |
opens | Reflection may reach non public members at runtime |
uses and provides | Service consumer and provider declarations |
A package that is not exported is completely invisible outside the module, no matter howpublicits classes are. This is the central change:publicno longer means universally accessible.
Strong encapsulation in practice
// In a module that does not export com.example.notes.internal
package com.example.notes.internal;
public class Cache { } // public, and still unreachable from other moduleserror: package com.example.notes.internal is not visible
(package com.example.notes.internal is declared in module com.example.notes,
which does not export it)exports against opens
exports | opens | |
|---|---|---|
| Compile time access | Yes, public members | No |
| Runtime access | Yes, public members | Yes, including private via reflection |
| Typical use | An API | Frameworks that map or inject into your objects |
open module com.example.notes { // every package open for reflection
requires java.sql;
}Compiling and running
javac -d out --module-source-path src $(find src -name "*.java")
java --module-path out --module com.example.notes/com.example.notes.Main
java --list-modules
java --describe-module com.example.notes
jdeps --module-path out out/com.example.notesBuilding a small runtime
jlink --module-path $JAVA_HOME/jmods:out
--add-modules com.example.notes
--output runtime --strip-debug --compress=2 --no-header-filesjlink produces a runtime image containing only the modules actually required. A simple service can drop from a full JDK to a few tens of megabytes, which matters for container images.
Services
// In the API module
package com.example.notes.spi;
public interface Exporter {
String format();
String export(Note note);
}// In the consuming module
module com.example.notes {
uses com.example.notes.spi.Exporter;
}
ServiceLoader<Exporter> exporters = ServiceLoader.load(Exporter.class);
for (Exporter exporter : exporters) {
System.out.println(exporter.format());
}// In a provider module
module com.example.notes.json {
requires com.example.notes;
provides com.example.notes.spi.Exporter with com.example.notes.json.JsonExporter;
}The consumer never names an implementation. Adding a provider module to the module path is enough for it to be discovered.
Migration
| Term | Meaning |
|---|---|
| Named module | Has a module-info.java |
| Automatic module | A plain jar on the module path; its name is derived, and it reads everything |
| Unnamed module | Everything on the classpath; it reads all modules |
The classpath still works exactly as before, which is why modules can be adopted gradually or not at all. Many applications remain on the classpath and use only the platform modules, and that is a legitimate choice.
# Temporary escape hatches during migration
java --add-exports java.base/sun.nio.ch=ALL-UNNAMED
java --add-opens java.base/java.lang=ALL-UNNAMEDThese flags reopen a package so older code keeps working. They are a migration aid, not a design, and each one should be tracked as debt.
Rules to remember
- Split packages are forbidden: two modules may not contain the same package.
- Cyclic dependencies between modules are rejected at compile time.
- A module reads only what it requires; there is no implicit access.
java.baseis required implicitly by every module.
Common mistakes
- Expecting a
publicclass in an unexported package to be usable. - Forgetting
opensand finding that a framework cannot reflect into your model classes. - Creating a split package during migration.
- Using
--add-openspermanently instead of fixing the dependency. - Assuming modules are mandatory. They are not.
Best practices
- Export a small API package and keep everything else internal.
- Use
opens ... torather than opening a package to everyone. - Use
requires transitiveonly when your API exposes types from that module. - Use
jlinkwhen image size matters. - Prefer services to hard coded implementation names.
- Adopt modules where the boundary is genuinely useful, and do not force them elsewhere.
Practice
- Write a two module project where one exports an API and the other consumes it.
- Make a public class unreachable by not exporting its package, and read the compiler error.
- Explain when
opensis required rather thanexports. - Define a service interface, one provider, and load it with
ServiceLoader. - Use
jlinkto build a runtime image and compare its size with the full JDK.
Conclusion
A module declares what it requires and what it exposes, turning public from "visible to everything" into "visible where I said". Use it to enforce a real API boundary and to ship a smaller runtime, and adopt it where it earns its keep.