Lesson Completion
Back to course

Java Module System: Strong Encapsulation and Modularity

Beginner
12 minutes4.8Java

1. The Hook (The "Byte-Sized" Intro)

  • In a Nutshell: Java Platform Module System (JPMS) (Java 9+, Project Jigsaw) introduces modules—strong encapsulation units. module-info.java declares dependencies. requires (dependencies), exports (public API).
  • Benefits: Strong encapsulation (hide internals), reliable configuration (explicit dependencies), improved security, smaller runtime (jlink custom JRE). Modular JDK (java.base, java.sql, etc.).
  • Migration: classpath → module path.
  • Key: Modules > packages in hierarchy!

Think of apartment building. Before modules = open floor (no walls, everything accessible, messy). After modules = separate apartments with locked doors (exports = public entrance, requires = utilities needed). Can't access neighbor's bedroom (strong encapsulation). Building manager knows which apartments need electricity (reliable configuration)!


2. Conceptual Clarity (The "Simple" Tier)

💡 The Analogy

  • Module: Apartment with controlled access
  • exports: Front door (public API)
  • requires: Utilities needed (dependencies)
  • module-info.java: Apartment contract/lease

Module Hierarchy

graph TD A[JDK] --> B[java.base module] A --> C[java.sql module] A --> D[java.xml module] E[Your Application] --> F[com.myapp module] F -->|requires| B F -->|requires| C F --> G[exports com.myapp.api] F --> H[internal packages<br/>not exported] style B fill:#2E7D32 style F fill:#F57C00 style G fill:#2E7D32 style H fill:#C2185B

3. Technical Mastery (The "Deep Dive")

Module System Benefits

BenefitDescription
Strong EncapsulationHide internal APIs, only export what's needed
Reliable ConfigurationExplicit dependencies, fail-fast at startup
Improved SecuritySmaller attack surface
Smaller Runtimejlink creates custom JRE with only needed modules
Better PerformanceJVM optimizations with module metadata

4. Interactive & Applied Code

java
// ======================================== // MODULE 1: com.myapp.core // File: src/com.myapp.core/module-info.java // ======================================== module com.myapp.core { // Dependencies requires java.base; // Implicit, always available requires java.sql; // Need JDBC // Public API (exported) exports com.myapp.core.api; exports com.myapp.core.model; // Internal packages (NOT exported) // com.myapp.core.internal - hidden from outside } // File: src/com.myapp.core/com/myapp/core/api/Service.java package com.myapp.core.api; public class Service { public void doSomething() { System.out.println("Public API method"); InternalHelper.help(); // OK: same module } } // File: src/com.myapp.core/com/myapp/core/internal/InternalHelper.java package com.myapp.core.internal; public class InternalHelper { public static void help() { System.out.println("Internal helper (not exported)"); } } // ======================================== // MODULE 2: com.myapp.client // File: src/com.myapp.client/module-info.java // ======================================== module com.myapp.client { requires com.myapp.core; // Depends on core module requires java.base; } // File: src/com.myapp.client/com/myapp/client/Main.java package com.myapp.client; import com.myapp.core.api.Service; // ✅ OK: exported // import com.myapp.core.internal.InternalHelper; // ❌ ERROR: not exported! public class Main { public static void main(String[] args) { Service service = new Service(); service.doSomething(); // InternalHelper.help(); // ❌ COMPILE ERROR: package not visible! } } // ======================================== // EXAMPLE: Transitive Dependencies // ======================================== // module-info.java for library module module com.myapp.library { requires transitive java.sql; // Transitive: clients get java.sql too exports com.myapp.library; } // module-info.java for client module com.myapp.client { requires com.myapp.library; // Gets java.sql transitively! // No need to: requires java.sql; } // ======================================== // EXAMPLE: Opens (Reflection Access) // ======================================== module com.myapp.reflection { // Normal export (compile-time access only) exports com.myapp.api; // Opens for reflection (runtime access) opens com.myapp.internal.dto; // Frameworks can reflect // Or open entire module // open module com.myapp.reflection { ... } } // ======================================== // EXAMPLE: Services (Dependency Injection) // ======================================== // Service interface package com.myapp.service; public interface Logger { void log(String message); } // module-info.java (service provider) module com.myapp.logger.impl { requires com.myapp.service; provides com.myapp.service.Logger with com.myapp.logger.impl.ConsoleLogger; } // module-info.java (service consumer) module com.myapp.app { requires com.myapp.service; uses com.myapp.service.Logger; // Use service } // Client code import java.util.ServiceLoader; import com.myapp.service.Logger; ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class); Logger logger = loader.findFirst().orElseThrow(); logger.log("Hello from module system!");

Compiling and Running Modules:

bash
# Directory structure: # src/ # com.myapp.core/ # module-info.java # com/myapp/core/api/Service.java # com.myapp.client/ # module-info.java # com/myapp/client/Main.java # Compile modules javac -d mods/com.myapp.core \ src/com.myapp.core/module-info.java \ src/com.myapp.core/com/myapp/core/api/*.java javac --module-path mods -d mods/com.myapp.client \ src/com.myapp.client/module-info.java \ src/com.myapp.client/com/myapp/client/*.java # Run modular application java --module-path mods -m com.myapp.client/com.myapp.client.Main # Create custom runtime with jlink jlink --module-path mods:$JAVA_HOME/jmods \ --add-modules com.myapp.client \ --output custom-jre # Run with custom JRE ./custom-jre/bin/java -m com.myapp.client/com.myapp.client.Main

5. The Comparison & Decision Layer

Before Modules (Classpath)After Modules (Module Path)
All public classes accessibleOnly exported packages accessible
JAR hell (version conflicts)Reliable configuration
No encapsulationStrong encapsulation
Monolithic JRECustom runtime (jlink)
Runtime errorsCompile-time errors

6. The "Interview Corner" (The Edge)

The "Killer" Interview Question: "What's the difference between exports and opens?" Answer:

  • exports: Compile-time + runtime access (normal usage)
  • opens: Runtime reflection access only (for frameworks)
java
module com.myapp { exports com.myapp.api; // ✅ Public API opens com.myapp.internal.dto; // ✅ For reflection (JSON libs, JPA) } // Client code: import com.myapp.api.Service; // ✅ OK: exported // import com.myapp.internal.dto.User; // ❌ ERROR: not exported // But reflection works: Class<?> cls = Class.forName("com.myapp.internal.dto.User"); // ✅ OK: opened

Pro-Tips:

  1. Unnamed module (classpath):
bash
# Mix modules and classpath java --module-path mods --class-path legacy.jar \ -m com.myapp/com.myapp.Main # Classpath = unnamed module (can access all modules)
  1. Automatic modules (JAR without module-info):
bash
# Put non-modular JAR on module path java --module-path legacy.jar:mods -m com.myapp/Main # JAR name becomes module name: legacy.jar → legacy
  1. Migration strategy:
  • Bottom-up: Start with leaf dependencies, make them modules
  • Top-down: Application first, dependencies as automatic modules

Topics Covered

Java Fundamentals

Tags

#java#programming#beginner-friendly

Last Updated

2025-02-01