-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
181: Understanding the Java Platform Module System (JPMS)
You've probably spent years just throwing JARs onto a classpath and letting Java figure it out at runtime. But since Java 9, we have the Java Platform Module System (JPMS). For a lot of developers, JPMS feels like a wall they keep running into. You’ll be writing a perfectly valid class, you'll import it, and then the compiler will scream at you—not because the class doesn't exist, but because it's "not visible."
The Mystery of the Invisible Package
Imagine you're building a small system with two parts: a payment-engine that handles the logic and a payment-app that runs the main loop. You've organized your code into modules. In your payment-engine module, you have a utility class that calculates taxes:
// File: src/payment.engine/com/payment/utils/TaxCalculator.java
package com.payment.utils;
public class TaxCalculator {
public static double calculate(double amount) {
return amount * 0.15;
}
}
Now, you try to use this in your payment-app module:
// File: src/payment.app/com/payment/main/Main.java
package com.payment.main;
import com.payment.utils.TaxCalculator;
public class Main {
public static void main(String[] args) {
System.out.println("Tax: " + TaxCalculator.calculate(100));
}
}
On the surface, this is standard Java. But when you try to compile this as a module, you get a punch in the face from the compiler: package com.payment.utils is not visible. You check your imports, you check your folder structure, and everything looks correct. The class is public, the method is public, yet Java is acting like the class doesn't even exist.
Opening the Gates with module-info.java
Here is the thing about JPMS: public no longer means "available to everyone." In the old classpath world, if a class was public, any other class on the classpath could touch it. In the module world, a public class is only available if the package containing it is explicitly exported by its module, and the consuming module explicitly requires that module.
To fix this, we need to define the boundaries. First, we tell the payment-engine that it's okay for other people to use the utils package. We do this by creating a module-info.java file in the root of the module's source folder:
// File: src/payment.engine/module-info.java
module payment.engine {
exports com.payment.utils;
}
That exports keyword is the key. Without it, the com.payment.utils package is encapsulated—hidden from the rest of the world, even if the classes inside it are marked public. I actually find this incredibly useful for hiding "internal" helper classes that you don't want other developers relying on.
But we're only halfway there. The payment-app still doesn't know it's allowed to look at the payment-engine. We need to declare the dependency in the app's own module descriptor:
// File: src/payment.app/module-info.java
module payment.app {
requires payment.engine;
}
Now, when you compile, the magic happens. The compiler sees that payment.app requires payment.engine, and payment.engine explicitly allows access to com.payment.utils. The bridge is built, and your code finally runs.
Handling Internal Logic with Encapsulation
You might be wondering, "Why go through all this trouble?" I've seen too many legacy projects where a developer used a "public" class that was actually meant to be internal, and then six months later, they couldn't change that class because fifty other classes were depending on it. JPMS solves this.
If you had a package called com.payment.engine.internal containing complex math that should never be touched by the Main class, you simply don't export it. Even if every class in that package is public, the JPMS will block any attempt to import them from outside the module. It's a hard wall that prevents architectural drift.
📋 Practical Task
Exercise: Modularizing a Legacy Utility Library
You have been handed a small project that was written using a flat classpath, and your lead engineer wants it converted to the Java Platform Module System to improve encapsulation.
The Setup:
Create two separate module directories: com.service.core and com.service.client.
- In
com.service.core:- Create a package
com.service.core.apicontaining a public classServiceManagerwith a public methodString getStatus(). - Create a package
com.service.core.internalcontaining a public classInternalConfigwith a public methodString getConfigValue(). - Create a
module-info.javafile that exports theapipackage but does not export theinternalpackage.
- Create a package
- In
com.service.client:- Create a
module-info.javafile that requirescom.service.core. - Create a
Mainclass that attempts to callServiceManager.getStatus()(this should work). - Create a second attempt in the
Mainclass to callInternalConfig.getConfigValue().
- Create a
Goal: Successfully compile the project such that the api call works, but the internal call triggers a compilation error. Once you see the error, comment out the illegal call to allow the project to build. This confirms your encapsulation is working as intended.
There are no comments for now.