Chapter 8: Packages and Modules
As Java projects grow, organization becomes essential. Classes alone are not enough to keep large systems manageable, so Java provides two complementary mechanisms for structuring code: packages and modules. Packages group related classes and interfaces into named namespaces, preventing conflicts and improving clarity. Modules, introduced in Java 9, build on this idea by defining explicit boundaries and dependencies between those packages, turning a collection of classes into a self-contained, versionable unit.
Packages are the foundation of code organization in Java. Every class belongs to a package, whether explicitly declared or implicitly placed in the unnamed default package. Packages make code easier to navigate, reuse, and distribute. They also form the basis for access control between classes, since Java’s visibility rules extend across package boundaries.
Modules take this one step further. A module declares which packages it exports for use by other modules, and which other modules it depends on. This structure gives large applications a clear architecture, where internal implementation details remain hidden and only well-defined interfaces are exposed. The Java Platform Module System (JPMS) enforces these relationships at both compile time and runtime, improving reliability and security.
Together, packages and modules transform a flat collection of source files into a layered, maintainable ecosystem. Whether you are writing a small utility or a multi-module application, understanding how these features interact will help you design software that scales cleanly and compiles with confidence.
Package structure and naming
A package in Java is a namespace that groups related classes, interfaces, and subpackages together under a common name. Packages provide organization, prevent name clashes, and form the basis for access control across different parts of a program. They mirror the folder structure on disk, creating a clear correspondence between code and file layout.
You declare a package at the top of a source file using the package keyword, followed by its fully qualified name. For example:
package com.example.project.utils;
public class MathHelper {
public static int add(int a, int b) {
return a + b;
}
}
This declaration tells the compiler that MathHelper belongs to the package com.example.project.utils. The source file must therefore be stored in a matching folder structure:
com/
example/
project/
utils/
MathHelper.java
When compiled, the output .class file is placed in the same structure within the output directory. This physical organization ensures that large codebases remain predictable and navigable.
com.company.project), which guarantees global uniqueness across organizations.
Default and explicit packages
If you do not declare a package at the top of a source file, the class belongs to the default package. While this can be useful for very small programs or examples, it should be avoided in professional code because classes in the default package cannot be imported into other packages.
// default package (no declaration)
public class Hello { … }
Explicitly naming every package makes your code more modular and avoids future conflicts as projects grow. It also allows tools and IDEs to organize files correctly and manage dependencies more effectively.
Package naming conventions
Java’s package naming conventions follow clear, global rules that make code from different sources work together seamlessly:
- Use all lowercase letters, with words separated by periods (
.). - Begin with your organization’s reversed Internet domain name (for example,
com.exampleororg.apache). - Follow with project or module identifiers, then specific functionality or components.
- Avoid using Java keywords or existing standard library package names.
package com.nixonpublishing.thisisjava.ch08.examples;
Package naming consistency allows developers to understand a project’s structure immediately and reduces the risk of class name collisions across different libraries.
Organizing packages by responsibility
Large projects often use packages to separate code by purpose or functionality. For example, a typical application might be divided as follows:
com.example.app
├─ model // Data structures and domain classes
├─ view // User interface components
├─ controller // Application logic and flow
├─ util // Helper and utility classes
└─ test // Unit tests and resources
This structure mirrors the logical architecture of the program, allowing teams to work on different components independently while maintaining a coherent layout. IDEs and build tools (such as Maven or Gradle) recognize these conventions automatically.
Importing classes and static imports
To use a class or interface from another package, you must either refer to it by its fully qualified name or import it into your source file. Imports make code more readable by allowing you to refer to classes directly without repeatedly writing their full package paths.
import com.example.project.utils.MathHelper;
public class Calculator {
public static void main(String[] args) {
int sum = MathHelper.add(5, 7);
System.out.println(sum);
}
}
Without the import statement, you would need to use the full package name each time:
int sum = com.example.project.utils.MathHelper.add(5, 7);
Imports do not copy code or create dependencies beyond what the compiler already needs. They are purely a convenience feature that tells the compiler where to find class definitions when translating source code.
Wildcard imports
You can use the asterisk (*) to import all public classes from a package:
import com.example.project.utils.*;
This allows access to all public members of that package without listing them individually. However, wildcard imports are resolved only at compile time—they do not import subpackages or increase load time.
While convenient, wildcard imports can sometimes obscure where a class comes from, especially when multiple packages contain classes with the same name.
Automatic imports
Every Java source file automatically imports two groups of classes:
- All classes in the same package.
- All classes from
java.lang(for example,String,System,Math).
This means you can use core classes without explicit imports:
String text = "Hello";
System.out.println(Math.sqrt(9));
However, anything outside java.lang or your current package must be imported explicitly.
Static imports
Introduced in Java 5, static imports let you import static members (fields or methods) from a class so that they can be used without qualifying their class name. This can make code cleaner in cases where static constants or utility methods are used frequently.
import static java.lang.Math.PI;
import static java.lang.Math.pow;
public class Circle {
public static double area(double radius) {
return PI * pow(radius, 2);
}
}
Without static imports, you would need to write Math.PI and Math.pow() each time. Static imports are especially common in testing frameworks like JUnit, where they allow cleaner assertions:
import static org.junit.jupiter.api.Assertions.assertEquals;
assertEquals(10, calculate());
Import order and conflicts
If two imported packages contain classes with the same name, you must refer to the ambiguous class by its fully qualified name. For example, both java.util and java.sql contain a class named Date:
import java.util.*;
import java.sql.*;
Date d = new Date(); // error: ambiguous
java.util.Date d1 = new java.util.Date(); // correct
java.sql.Date d2 = new java.sql.Date(0); // correct
Java does not allow importing two classes with identical names into the same namespace, so qualified names must be used whenever such conflicts occur.
Encapsulation across packages
Encapsulation is one of the key principles of object-oriented design, and in Java it extends beyond individual classes to include entire packages. Packages act as visibility boundaries, allowing you to control which parts of your code are accessible to other parts of the program. This helps keep internal details hidden while exposing only the elements intended for external use.
Access control in Java is managed through access modifiers: public, protected, private, and package-private (the default when no modifier is used). These determine how fields, methods, and classes can be seen or used from other packages.
| Modifier | Visible within same class | Same package | Subclass (different package) | Unrelated package |
public | ✔ | ✔ | ✔ | ✔ |
protected | ✔ | ✔ | ✔ | ✖ |
(default) (no modifier) | ✔ | ✔ | ✖ | ✖ |
private | ✔ | ✖ | ✖ | ✖ |
By combining these modifiers with package structure, you can design APIs that are clear and safe to use, while preventing access to internal implementation classes that are not meant to be part of the public interface.
public for types and methods that form part of your intended API, and keep supporting classes package-private whenever possible. Less exposure means fewer dependencies and a more stable design.
Package-private access
Any class, method, or field declared without an explicit access modifier has package-private visibility. This means it can be accessed freely by other classes in the same package but not by classes outside it, even if they are subclasses.
package com.example.shapes;
class CircleHelper { // package-private
static double circumference(double r) {
return 2 * Math.PI * r;
}
}
public class Circle {
double radius;
public double area() {
return Math.PI * radius * radius;
}
}
Here, CircleHelper is invisible outside com.example.shapes but fully accessible to other classes within that package. This technique keeps utility or implementation details internal to the package.
protected instead.
Protected access and subclassing
The protected modifier allows visibility within the same package and also to subclasses in other packages. This makes it useful for creating extensible class hierarchies while still keeping some encapsulation.
package com.example.animals;
public class Animal {
protected void speak() {
System.out.println("Generic animal sound");
}
}
package com.example.animals.mammals;
import com.example.animals.Animal;
public class Dog extends Animal {
public void bark() {
speak(); // allowed: protected method in superclass
System.out.println("The dog barks");
}
}
Although speak() is not public, the subclass can still call it because it inherits from Animal. However, a non-subclass in another package would not be able to access it.
Public APIs and internal classes
When designing libraries or large systems, it is best practice to separate public API classes (those intended for use by other packages or developers) from internal implementation classes (those used only within the package). You can enforce this separation through access modifiers and careful package structure.
com.example.library
├─ api
│ └─ Library.java
└─ internal
└─ LibraryHelper.java
Only the classes in com.example.library.api are public, while those in internal remain package-private. External code imports only from the api package, keeping internal details hidden and safe from misuse.
java.util vs sun.*).
Encapsulation and the module system
Before Java 9, package-level encapsulation was the strongest available boundary. With the introduction of the Java Platform Module System (JPMS), modules can now explicitly control which packages are visible to other modules, providing a more robust form of encapsulation at a higher level.
When you declare a module, you can export only the packages you wish to make accessible, while keeping the rest private to that module. This ensures that even public classes are not globally visible unless their packages are explicitly exported.
Java modules
Modules were introduced in Java 9 as part of the Java Platform Module System (JPMS). They provide a higher-level way to organise code than packages, allowing you to define clear boundaries between components, declare dependencies explicitly, and control which packages are accessible to other parts of a program.
Each module has a special descriptor file named module-info.java, placed in the root of its source hierarchy. This file defines the module’s name, the packages it exports, and the other modules it requires.
module com.example.library {
exports com.example.library.api;
requires java.sql;
}
In this example, the module is named com.example.library. It makes the package com.example.library.api available to other modules and declares a dependency on the java.sql module from the Java standard library. All other packages remain hidden by default, even if they contain public classes.
export become accessible outside the module, which means internal packages stay private even if their classes are public.
Structure of a modular project
A typical modular project mirrors the hierarchy of its packages, with the module-info.java file sitting alongside the top-level package directory:
src/
com.example.library/
module-info.java
com/
example/
library/
api/
Library.java
internal/
Helper.java
The compiler uses module-info.java to verify dependencies and ensure that code cannot access non-exported packages. This explicit structure helps prevent accidental coupling between unrelated parts of large systems.
The requires and exports directives
The requires directive declares a dependency on another module. It ensures that the dependent module is available both during compilation and at runtime.
requires java.xml;
requires com.example.utils;
The exports directive declares which packages in the current module are visible to other modules:
exports com.example.library.api;
All other packages remain inaccessible outside the module, even if they contain public classes. This enforces strong encapsulation and makes module boundaries explicit.
Transitive dependencies
Sometimes, a module depends on another module that itself requires additional modules. To make those secondary dependencies automatically available to modules further up the chain, use the requires transitive directive.
module com.example.library {
requires transitive com.example.utils;
exports com.example.library.api;
}
This ensures that any module depending on com.example.library also has access to com.example.utils without having to declare it explicitly.
Opening packages for reflection
Some libraries, such as frameworks or dependency injectors, use reflection to access private members. Because modules restrict access by default, such frameworks can no longer freely reflect on all classes. To allow reflection selectively, you can use the opens directive.
opens com.example.library.internal to com.fasterxml.jackson.databind;
This grants reflective access to the specified package, but only for the named module (com.fasterxml.jackson.databind in this case). You can also open a package to all modules:
opens com.example.library.internal;
opens sparingly. It should only be applied when reflection is essential, as it weakens module encapsulation and can make systems harder to maintain.
Combining exports and opens
It is common to export API packages and open internal ones for reflection. A single module-info.java file can contain both directives:
module com.example.library {
exports com.example.library.api;
opens com.example.library.internal to com.fasterxml.jackson.databind;
requires java.sql;
}
This configuration exposes only the API package for normal use, while permitting controlled reflective access to internal classes during serialization or runtime inspection.
module-info.java file acts as a manifest for the entire codebase. It defines dependencies, visibility, and reflective permissions all in one place, ensuring modular projects remain explicit and predictable.
Compiling and running modular programs
Working with modules changes how Java source code is compiled, packaged, and executed. Unlike traditional classpath-based projects, modular programs use a module path, which explicitly lists the locations of compiled modules. This separation allows the compiler and runtime to verify dependencies and prevent unwanted access to non-exported packages.
Compiling a modular project
To compile a modular project, use the javac command with the --module-source-path option. This tells the compiler where to find the module source directories. Each module must have its own folder containing a module-info.java file and its source packages.
project/
└─ src/
├─ com.example.library/
│ ├─ module-info.java
│ └─ com/example/library/api/Library.java
└─ com.example.app/
├─ module-info.java
└─ com/example/app/Main.java
To compile both modules into a separate output directory named mods:
javac -d mods --module-source-path src $(find src -name "*.java")
This command compiles every source file within the src tree, preserving the module structure inside mods. Each module will have its own directory in the output, for example:
mods/
├─ com.example.library/
└─ com.example.app/
requires statements. The compiler ensures that all required modules are available on the module path.
Running modular applications
To run a modular program, use the java command with the --module-path option, specifying where compiled modules are located, followed by the --module (or -m) option to identify which module and main class to launch.
java --module-path mods -m com.example.app/com.example.app.Main
The syntax after -m consists of the module name, a slash, and the fully qualified class name containing the main() method. The JVM verifies dependencies and ensures that all required modules are present before execution begins.
Combining modular and non-modular code
During the transition to modular development, projects may mix modular and legacy code. The Java module system supports this through the unnamed module and the automatic module features.
- Unnamed module: Classes placed directly on the classpath belong to the unnamed module. They can read all named modules, but named modules cannot read them in return.
- Automatic module: A JAR placed on the module path without a
module-info.javafile is treated as an automatic module. Its name is derived from its JAR filename, and all its packages are exported automatically.
These mechanisms simplify migration from older codebases while gradually introducing modularity.
java --module-path mods:libs -m com.example.app/com.example.app.Main
Here, mods contains compiled named modules, and libs contains third-party automatic modules. The JVM resolves both during startup.
module-info.java for each component to ensure predictable behaviour and strong encapsulation.
Packaging and distributing modules
Once compiled, a module can be packaged into a standard JAR file. To make it modular, include the compiled module-info.class file in the JAR’s root directory:
jar --create --file dist/com.example.library.jar -C mods/com.example.library .
This produces a modular JAR that other projects can reference via the module path. You can verify the module’s metadata using the jar or jdeps tools:
jar --describe-module --file dist/com.example.library.jar
To run a modular JAR directly:
java --module-path dist -m com.example.library/com.example.library.Main
module-info.class file is treated as an automatic module when placed on the module path. For full compatibility, always include an explicit module descriptor.
Advantages of modular execution
- Improved dependency management and clearer architecture.
- Smaller runtime images using
jlinkto include only required modules. - Enhanced encapsulation and security through controlled package exports.
- More predictable compilation and runtime behaviour across environments.
By compiling and running programs as modules, you gain control over both structure and dependencies. The module system transforms how large Java applications are built, shared, and maintained, bringing consistency and clarity to every stage of development.