Last Updated on July 23, 2024

Java modules, introduced in Java 9, released September 2017, represent a fundamental shift in how Java applications are structured and organized. 

They are essentially named, self-describing collections of related code and data.

Java Modularity is result of JSR 376 submission to  Project Jigsaw.

Java Modules vs Packages

Java packages are a fundamental concept for organizing and structuring your code. They serve as containers for related classes, interfaces, and other packages.

Example:

package com.codeline.mypackage; 
public class MyClass { // ... }

Java provides several built-in packages that contain essential classes for various functionalities:

  • java.lang: Contains fundamental classes like String, Math, System, etc.
  • java.util: Provides utility classes for data structures, collections, date/time operations, etc.
  • java.io: Contains classes for input/output operations.
  • java.net: Provides classes for network programming.
  • java.awt: Contains classes for creating graphical user interfaces.
  • javax.swing: Provides a set of GUI components for creating desktop applications.

Java packages are essential for organizing your codebase, preventing naming conflicts, and improving code reusability.

By understanding packages, you can write better-structured and more maintainable Java applications.

Java access modifiers control the visibility or accessibility of classes, methods, variables, and constructors. They determine which parts of your code can access specific elements.

By default, any access modifier is omitted and that makes them accessible only within the same package.

We can explicitly use the following access modifiers to control accessibility.:

  • public: Accessible from anywhere.
  • protected: Accessible within the same package or subclasses.
  • private: Accessible only within the declaring class.

As applications grew in size and complexity, managing dependencies and understanding the overall structure became increasingly difficult.

The classpath could lead to unexpected conflicts and runtime errors due to multiple versions of the same library or inconsistent classpath configurations.

In essence, there were issues with code organization, scalability, maintainability, security, and performance for large-scale Java applications. 

The need for a more structured and controlled approach to dependency management became apparent and by introducing another layer of abstraction  modularity addressed many of the challenges associated with traditional classpath-based development.

Java Modules promote modularity by encapsulating related code and dependencies within a single unit. This enhances code reusability, maintainability, and testability.

By Implementing Stronger Encapsulation  Modules can control which packages and types are accessible to other modules, improving code organization and reducing unintended dependencies.

Modules explicitly declare their dependencies in what is called Declarative Dependency Management, on other modules, ensuring clarity and avoiding runtime errors caused by missing dependencies.

The module path replaces the traditional classpath, allowing the JVM to load modules efficiently and verify dependencies.

Example:

// Module A

module com.codeline.moduleA {

    exports com.example.moduleA.publicPackage;

}

// Module B

module com.codeline.moduleB {

    requires com.example.moduleA;

}

In this example:

  • Public elements in com.codeline.moduleA.publicPackage can be accessed from module B.
  • Public elements in other packages within module A are not accessible to module B.

In essence, modules add a new level of control over access modifiers by introducing the concept of package exports. This allows for finer-grained control over code visibility and promotes better encapsulation.

Java SE Modules

When we install Java 9+ (in our case java 22), 

There is modular structure (System Modules) of java release that can be organized in several categories:

  1. Core Modules: These are fundamental modules that provide the core Java SE API, such as java.base, java.sql, java.net, etc.
  2. Platform Modules: These modules extend the core platform with additional functionalities, like java.desktop, java.xml, etc.
  3. JDK-Specific Modules: These modules are specific to the JDK and might not be present in a JRE, such as jdk.compiler, jdk.crypto.ec, etc.

We can see the modular structure of Java itself and list all available modules in specific release by running:

$java --list-modules

[email protected]
[email protected]
[email protected]
[email protected]
........

Anatomy of a Java Module

Descriptor

The module descriptor file is named module-info.java and is placed at the root of a module’s source code directory.

We construct the module with a declaration whose body is either empty or made up of module directives:

module myModuleName {

    // all directives are optional

}

A Java module name is a unique identifier that defines a module. It’s similar to a package name but serves a distinct purpose within the modular system.

When naming, It’s recommended to follow the reverse domain name convention (e.g., com.codeline.mymodule) for better organization and avoiding naming collisions.

Example:

module com.codeline.mymodule {

  // ... module body

}

Requires directive

Requires in a Java module descriptor specifies a dependency on another module. It’s essential for defining the relationships between modules in your application.

Requires static,  declares a dependency that is only needed at compile time. The module itself doesn’t need to be present at runtime. This is useful for tools or libraries used during the build process.

Requires transitive, automatically includes the dependencies of the required module. This can simplify dependency management but might introduce additional dependencies that are not explicitly needed.

Example:

module com.codeline.mymodule
 { 
  requires java.base; 
  requires static module.name;
  requires transitive com.google.common.guava; 
}

Exports directive

The exports directive in a Java module descriptor specifies which packages within the module are accessible to other modules. 

It’s a crucial component for controlling the module’s public API and enforcing encapsulation.

Example:

module com.example.mymodule 
{ 
  exports com.codeline.mymodule.api; 
  exports com.codeline.mymodule.api to com.othermodule;
}

Java also allows you to specify which modules can access an exported package using the “to” keyword

This restricts access to the com.codeline.mymodule.api package to the com.othermodule.

Opens directive

The opens directive in a Java module descriptor allows reflective access to all types (including private members) within a specific package from other modules at runtime.

Example:

module com.codeline.mymodule 
{ 
  opens com.codeline.mymodule.internal; 
}

In this example, the com.codeline.mymodule.internal package is opened to all modules, allowing them to use reflection to access its types and members.

Uses directive

The uses directive in a Java module descriptor declares a service that the module consumes. It’s part of the service loader mechanism, enabling modules to discover and use service implementations at runtime.

Example:

module com.codeline.mymodule 
{ 
  uses java.util.logging.Logger; 
}

In this example, the com.codeline.mymodule module declares its use of the java.util.logging.Logger service.

Provides directive

The provides directive in a Java module descriptor declares a service implementation that the module offers.

It’s part of the service loader mechanism, allowing modules to provide implementations for services used by other modules.

Example:

module com.codeline.myservice 
{ 
  provides java.util.logging.Logger with com.example.mymodule.MyLogger; 
}

The com.codeline.myservice module provides an implementation of the java.util.logging.Logger service.

Scroll to Top