Index

Packages and Modular Design

Java Object-Oriented Design

9.1 Organizing Code with Packages

The Purpose and Benefits of Packages

In Java, packages serve as a fundamental way to organize and structure your code. Think of packages as folders or namespaces that group related classes and interfaces together. They help in managing large codebases by:

By structuring your code into packages, you create a modular and maintainable project that scales well as complexity grows.

Declaring Packages and Placing Classes

To declare a package, use the package keyword as the very first statement in your Java source file. For example:

package com.example.utils;

public class StringUtils {
    public static boolean isEmpty(String s) {
        return s == null || s.isEmpty();
    }
}

This declaration states that the class StringUtils belongs to the package com.example.utils.

Important notes:

Example Project Structure with Multiple Packages

Consider a small project organized like this:

project-root/
 └─ src/
     ├─ com/
     │   └─ example/
     │       ├─ model/
     │       │    └─ User.java
     │       ├─ service/
     │       │    └─ UserService.java
     │       └─ utils/
     │            └─ StringUtils.java
     └─ Main.java

This hierarchical structure improves clarity, helps prevent class name conflicts, and enforces logical separation of concerns.

Conventions for Package Naming

Java package names follow a widely adopted convention based on the reverse domain name of your organization or project. For example:

Using reverse domain names ensures uniqueness across packages worldwide, reducing the risk of naming collisions when combining code from different sources.

Additional tips:

Package Visibility and Encapsulation

Packages also influence access control in Java. Access modifiers such as public, protected, and private interact with package structure to control visibility:

Using package-private visibility promotes encapsulation at the package level, allowing you to hide implementation details and expose only what is necessary for other parts of your program.

Summary

Packages are essential in Java for organizing your code into coherent, manageable modules. They prevent naming collisions, promote encapsulation, and reflect your project’s logical structure. By following package naming conventions and placing classes appropriately in directories that match package names, you ensure your code is clear, maintainable, and scalable.

Index

9.2 Access Control Across Packages

Java’s access control mechanisms are central to building well-encapsulated and maintainable software. When working with multiple packages, understanding how Java’s access modifiers govern visibility across package boundaries becomes critical for designing robust APIs and preserving internal implementation details.

Overview of Java Access Modifiers

Java provides four primary access levels for classes, methods, and variables:

Package-Private (Default) Access and Its Importance

The default or package-private access level is often overlooked but is essential for encapsulation at the package level. When you omit any modifier, Java treats the class or member as package-private. This means:

This behavior encourages grouping related classes into the same package so they can interact freely, while hiding implementation details from the outside world.

Cross-Package Access Examples

Let’s consider a simple example with two packages: com.example.model and com.example.service.

// File: com/example/model/User.java
package com.example.model;

public class User {
    String username;            // package-private field
    protected String email;     // protected field
    private int id;             // private field

    public User(String username, String email, int id) {
        this.username = username;
        this.email = email;
        this.id = id;
    }

    String getUsername() {       // package-private method
        return username;
    }

    protected String getEmail() {
        return email;
    }

    private int getId() {
        return id;
    }
}

Now, in another package:

// File: com/example/service/UserService.java
package com.example.service;

import com.example.model.User;

public class UserService extends User {
    public UserService(String username, String email, int id) {
        super(username, email, id);
    }

    public void printUserDetails() {
        // System.out.println(username); // Compile error: package-private not visible outside com.example.model
        System.out.println(email);       // Allowed: protected visible to subclass
        // System.out.println(id);       // Compile error: private
        System.out.println(getEmail());  // Allowed: protected method accessible
        // System.out.println(getUsername()); // Compile error: package-private method not visible
    }
}

Key takeaways from the example:

This illustrates how package boundaries and inheritance interact with access control modifiers.

Design Considerations for Exposing APIs Across Packages

When designing your Java applications, especially large ones split across multiple packages or modules, carefully deciding which members and classes to expose is vital.

1. Use public sparingly for APIs: Only classes and methods that are truly part of your external API should be public. Overusing public access risks exposing internal details, leading to fragile code dependent on implementation specifics.

2. Favor package-private for internal collaboration: Related classes within the same package can freely share package-private methods and fields, promoting cohesion without exposing internals to the outside.

3. Leverage protected for inheritance hierarchies: When designing abstract classes or frameworks meant to be extended, use protected members to allow subclasses to access necessary methods or fields, but keep them hidden from unrelated classes.

4. Encapsulate with private: Always restrict fields and helper methods to private unless broader access is explicitly needed. This minimizes unintended usage and makes future changes safer.

5. Provide controlled access with getters/setters: Even when fields are private, exposing them via public or protected getters and setters lets you enforce validation, logging, or other logic—offering a flexible interface without compromising encapsulation.

Summary

Understanding Java’s access modifiers in the context of package boundaries helps create well-structured, maintainable codebases:

This nuanced access control supports modular design by protecting implementation details, encouraging clear boundaries, and guiding API exposure. By carefully combining these modifiers, you ensure that your Java application is robust, secure, and easier to evolve.

Index

9.3 Introduction to Java Modules (module-info.java)

With the release of Java 9, the Java Platform Module System (JPMS) was introduced to address the growing complexity of large-scale applications and to enhance modularity beyond traditional packages. Modules provide a higher level of structure, allowing developers to explicitly declare dependencies and control the visibility of packages across the application, improving maintainability, security, and performance.

What Is the Java Platform Module System (JPMS)?

JPMS, often referred to as the Java Modules System, is a framework that organizes Java code into modules—self-describing collections of packages with explicit dependencies. It complements packages by grouping related packages and resources together under a module boundary.

Modules solve problems that packages alone cannot:

The Module Descriptor: module-info.java

At the heart of JPMS is the module descriptor, a special file named module-info.java placed at the root of the module source folder. This file declares:

This descriptor enables the compiler and runtime to enforce module boundaries and dependencies.

Basic Syntax of module-info.java

Here is a simple example of a module descriptor:

module com.example.library {
    exports com.example.library.api;
    requires java.logging;
}

If a package within the module is not exported, it remains inaccessible outside the module, even if public classes exist in that package. This enforces strong encapsulation at the module level.

Example: Simple Modular Project Structure

Suppose we have a project with two modules:

Each module has its own module-info.java.

com.example.utils/module-info.java:

module com.example.utils {
    exports com.example.utils.helpers;
}

com.example.app/module-info.java:

module com.example.app {
    requires com.example.utils;
}

The com.example.utils module exports the com.example.utils.helpers package, allowing com.example.app to use those classes. The com.example.app module declares that it requires com.example.utils.

Benefits of Using Modules in Large Projects

1. Improved Encapsulation Modules explicitly control what is exposed outside their boundaries, preventing accidental access to internal packages. This is a stronger guarantee than package-private access since it applies even if classes are public.

2. Reliable Configuration At both compile-time and runtime, the system verifies module dependencies, reducing the infamous "classpath hell" problems where classes fail to load due to missing dependencies.

3. Better Maintainability Clearly defined module boundaries help teams understand and manage dependencies, making large codebases easier to navigate and evolve.

4. Performance Optimizations The module system can optimize which parts of the application are loaded, improving startup time and reducing resource usage.

Running a Modular Application

To compile and run modular applications, you use the javac and java commands with module-specific options.

Assuming the source files are structured as:

/project-root
  /com.example.utils
    /src
      /module-info.java
      /com/example/utils/helpers/Utility.java
  /com.example.app
    /src
      /module-info.java
      /com/example/app/Main.java

Compile modules:

javac -d mods/com.example.utils com.example.utils/src/module-info.java com.example.utils/src/com/example/utils/helpers/Utility.java

javac --module-path mods -d mods/com.example.app com.example.app/src/module-info.java com.example.app/src/com/example/app/Main.java

Run the application:

java --module-path mods -m com.example.app/com.example.app.Main

Here, --module-path specifies where compiled modules are located, and -m specifies the module and main class to run.

Summary

The Java Platform Module System, introduced in Java 9, marks a significant evolution in organizing and structuring Java applications beyond packages. By defining explicit module boundaries with module-info.java, developers gain fine-grained control over what is visible and what is hidden, making large codebases more modular, secure, and easier to maintain.

Incorporating modules encourages strong encapsulation, reliable dependency management, and sets the foundation for scalable Java applications. While the initial learning curve can be steep, understanding and applying JPMS is essential for modern Java development, especially for enterprise-scale projects.

Index