Module System in Java9

Module System in Java9

The Module System, also known as Project Jigsaw, was introduced in Java 9 to address the challenges associated with building and maintaining large-scale Java applications. Prior to Java 9, the Java platform relied on a flat classpath, where all classes were accessible to each other, leading to issues such as:

  1. Namespace Pollution: occurs when there are conflicts in the names of packages, classes, or other identifiers within a program. In a non-modular Java application (pre-Java 9), where all classes are placed in a shared, flat classpath, there is a single global namespace. This global namespace can lead to naming conflicts, making it challenging to maintain and scale software systems.

    Example of Namespace Pollution without Modules:

    Consider two libraries, com.example.libraryA and com.example.libraryB, both containing a class named Utility:

     // com.example.libraryA.Utility
     package com.example.libraryA;
    
     public class Utility {
         public static void doSomething() {
             System.out.println("Library A Utility");
         }
     }
    
     // com.example.libraryB.Utility
     package com.example.libraryB;
    
     public class Utility {
         public static void doSomething() {
             System.out.println("Library B Utility");
         }
     }
    

    Now, if an application uses both libraries in a non-modular environment:

     // Non-modular application
     import com.example.libraryA.Utility;
     import com.example.libraryB.Utility;
    
     public class MyApp {
         public static void main(String[] args) {
             Utility.doSomething();  // Which Utility class should be used? LibraryA or LibraryB?
         }
     }
    

    In this case, there is a naming conflict. The application cannot differentiate between the Utility class from com.example.libraryA and com.example.libraryB because they share the same global namespace.

    Solving Namespace Pollution with Modules:

    Java 9 introduced the module system, which addresses namespace pollution by introducing a module-specific namespace. Each module has its own isolated namespace, preventing naming conflicts between modules. Modules explicitly declare what they export (make public) and what they require (dependencies).

    Module Definition for Library A:

     // com.example.libraryA/module-info.java
     module com.example.libraryA {
         exports com.example.libraryA;
     }
    

    Module Definition for Library B:

     // com.example.libraryB/module-info.java
     module com.example.libraryB {
         exports com.example.libraryB;
     }
    

    Now, in a modular application, the application can explicitly state its dependencies:

     // Modular application
     import com.example.libraryA.Utility;
    
     public class MyApp {
         public static void main(String[] args) {
             Utility.doSomething();  // Resolves to Utility class from Library A
         }
     }
    

    In this case, the modular system prevents namespace pollution because each module operates within its own scope, and dependencies are explicitly declared. The application can use the Utility class from com.example.libraryA without any naming conflicts. This enhances code readability, maintainability, and avoids the issues associated with namespace pollution.

  2. Jar Hell: refers to the challenges and conflicts that arise when managing dependencies in a non-modular Java application, particularly when dealing with conflicting versions of JAR (Java Archive) files. This situation occurs when different parts of an application or its dependencies rely on different versions of the same library, leading to runtime errors, classloading issues, and unpredictable behavior.

    Example of Jar Hell without Modules:

    Consider two libraries, LibraryA and LibraryB, each depending on different versions of the same third-party library, CommonLibrary:

     - MyApp
       - lib
         - LibraryA.jar (depends on CommonLibrary v1.0)
         - LibraryB.jar (depends on CommonLibrary v2.0)
         - CommonLibrary v1.0.jar
         - CommonLibrary v2.0.jar
    

    Now, if the application MyApp uses both LibraryA and LibraryB, it will face Jar Hell issues:

     // Non-modular application
     import com.example.libraryA.UtilityA;
     import com.example.libraryB.UtilityB;
    
     public class MyApp {
         public static void main(String[] args) {
             UtilityA.doSomethingA();
             UtilityB.doSomethingB();
         }
     }
    

    Here, LibraryA and LibraryB have different dependencies on CommonLibrary, leading to a classloading conflict. At runtime, the application may encounter errors such as LinkageError or NoSuchMethodError due to the mismatched versions of CommonLibrary.

    Solving Jar Hell with Modules:

    With the introduction of the module system in Java 9, the problem of Jar Hell is mitigated. Modules allow for explicit declaration of dependencies and encapsulation of internal details.

    Module Definition for Library A:

     // com.example.libraryA/module-info.java
     module com.example.libraryA {
         requires com.example.commonLibraryA;
         exports com.example.libraryA;
     }
    

    Module Definition for Library B:

     // com.example.libraryB/module-info.java
     module com.example.libraryB {
         requires com.example.commonLibraryB;
         exports com.example.libraryB;
     }
    

    Now, each module explicitly declares its dependency on a specific version of the CommonLibrary. The application, as a module, can also declare its dependencies:

    Module Definition for MyApp:

     // MyApp/module-info.java
     module com.example.myApp {
         requires com.example.libraryA;
         requires com.example.libraryB;
     }
    

    This approach ensures that each module operates with its declared dependencies, and there is no ambiguity or conflict in the classpath. The module system prevents the mixing of different versions of the same library, resolving Jar Hell issues.

    In summary, modules provide a clear and explicit way to manage dependencies, avoiding the complexities and conflicts associated with Jar Hell. They contribute to improved modularity, encapsulation, and maintainability in Java applications.

  3. Limited Encapsulation: refers to the challenge of achieving proper encapsulation and information hiding in Java applications prior to Java 9. In a non-modular environment, classes often expose their internal details to the entire classpath, leading to reduced control over access and potential information leaks. This makes it harder to enforce boundaries and separation of concerns within the codebase.

    Example of Limited Encapsulation without Modules:

    Consider a scenario where there is a package com.example.internal containing a class with internal details that should not be exposed:

     // com.example.internal.InternalClass
     package com.example.internal;
    
     public class InternalClass {
         private int sensitiveData;
    
         public InternalClass(int sensitiveData) {
             this.sensitiveData = sensitiveData;
         }
    
         public int getSensitiveData() {
             return sensitiveData;
         }
     }
    

    In a non-modular environment, any class from any package can access InternalClass:

     // Non-modular application
     import com.example.internal.InternalClass;
    
     public class LimitedEncapsulationExample {
         public static void main(String[] args) {
             InternalClass internalObj = new InternalClass(42);
    
             // Accessing sensitive data directly
             int data = internalObj.getSensitiveData();
             System.out.println("Sensitive Data: " + data);
         }
     }
    

    Here, limited encapsulation allows any class in the classpath to access the sensitive data of InternalClass, violating the principles of encapsulation and information hiding.

    Solving Limited Encapsulation with Modules:

    Java 9 introduces the concept of modules, providing a mechanism for stronger encapsulation. Modules explicitly declare which packages are accessible (exported) and which are not. This allows for better control over the visibility of internal details.

    Module Definition for Internal Module:

     // com.example.internal/module-info.java
     module com.example.internal {
         exports com.example.internal;
     }
    

    In this example, the module definition file specifies that the package com.example.internal is exported, making its public types accessible to other modules. However, if InternalClass had other internal packages or classes that were not exported, they would remain encapsulated.

    Module Definition for MyApp:

     // MyApp/module-info.java
     module com.example.myApp {
         requires com.example.internal;
     }
    

    Now, the application module explicitly declares its dependency on the com.example.internal module. It can use the exported types but cannot access non-exported internal details.

     // Modular application
     import com.example.internal.InternalClass;
    
     public class EncapsulationSolvedExample {
         public static void main(String[] args) {
             InternalClass internalObj = new InternalClass(42);
    
             // Accessing sensitive data using public API
             int data = internalObj.getSensitiveData();
             System.out.println("Sensitive Data: " + data);
         }
     }
    

    With modules, encapsulation is enhanced, and internal details of a module are hidden by default. Classes that are not explicitly exported are inaccessible outside the module, addressing the issue of limited encapsulation in non-modular Java applications.

    The Java 9 Module System was introduced to provide a solution to these challenges by introducing the concept of modules. A module is a collection of related packages and resources, along with a module descriptor that specifies dependencies and access rules.

    Example: Creating a Simple Module

    Consider a scenario where you have a library for handling greetings, and you want to create a module for it:

    1. Module Definition File (module-info.java):

       // module-info.java
       module com.example.greeting {
           exports com.example.greeting;
       }
      

      In this example, the module definition file specifies that the module com.example.greeting exports the package com.example.greeting. Exporting a package makes its public types accessible to other modules.

    2. Greeting Class in the Module:

       // Greeting.java
       package com.example.greeting;
      
       public class Greet {
           public static void sayHello() {
               System.out.println("Hello, Java 9 Module!");
           }
       }
      

      The Greet class is part of the com.example.greeting module.

    3. Using the Module in Another Module or Application:

       // MyApp.java
       import com.example.greeting.Greet;
      
       public class MyApp {
           public static void main(String[] args) {
               Greet.sayHello();
           }
       }
      

      In this example, the MyApp class in a different module or application imports and uses the Greet class from the com.example.greeting module.

Benefits of Module System:

  1. Encapsulation: Modules allow you to encapsulate the internal details of your code, exposing only what is necessary. This helps in creating well-defined and maintainable APIs.

  2. Dependency Management: Modules explicitly declare their dependencies, allowing the module system to enforce these dependencies. This eliminates the issues related to Jar Hell and simplifies dependency management.

  3. Namespace Isolation: Each module has its own namespace, reducing the chance of naming conflicts between packages and classes.

  4. Readability and Maintainability: Modules improve code organization, making it easier to understand and maintain large codebases.

  5. Security: Modules provide better control over what can be accessed externally, improving security by restricting access to internal implementation details.

The Java 9 Module System brings a more modular and scalable architecture to Java applications, addressing longstanding challenges related to dependencies, encapsulation, and namespace management. It helps developers create more maintainable and reliable software systems.