Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 17 – Modules, packaging and distribution

The Java Platform Module System (JPMS)

Write module-info.java with requires, exports and opens, compile and run modules on the module path, and read the errors strong encapsulation gives.

  • Advanced
  • 25 minutes
  • Examples run with Java 25.0.4 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 25 minutes free
Free: A module declaration · Compile and run two modules · What strong encapsulation stops · Check yourself. With a pass: 3 in-depth parts and 3 interview answers, about 17 minutes.

What you will learn

  • Write module-info.java with requires, exports and opens
  • Compile and run a modular application on the module path
  • Explain strong encapsulation, opens and --add-opens

Before you start

A module is a named group of packages with a declaration, module-info.java, that says which other modules it needs (requires) and which of its packages other code may use (exports). The Java Platform Module System (JPMS), part of Java since Java 9, checks those declarations when code compiles and again when the JVM starts. A package that a module does not export is invisible to every other module, even its public classes: this is strong encapsulation, and reflection into such a package needs an opens directive. You compile modules with javac --module-source-path and run them from the module path with java --module-path mods --module app/com.example.Main. Use modules for libraries whose internals must stay hidden, and to build small runtime images with jlink; plain JARs on the class path keep working beside them.

This lesson splits the shop’s billing program from the JAR lesson into two modules, runs them on Eclipse Temurin 25.0.4.1, and shows the two compile errors that the module system adds.

A module declaration

The program becomes two modules, each in its own folder with its declaration at the top of its sources. kirana.bill, in bill/src, holds the bill and a formatter in an internal package; kirana.app, in app/src, holds the program and declares that it needs kirana.bill:

bill/src/module-info

bill/src/module-info.java · bill/src/module-info.java

/** The bill model. Its API is the package com.example.kirana.bill; the internal package stays inside the module. */
module kirana.bill {
    exports com.example.kirana.bill;
}

app/src/module-info.java · app/src/module-info.java

/** The shop's program. It reads kirana.bill, and so can use the packages kirana.bill exports. */
module kirana.app {
    requires kirana.bill;
}

bill/src/com/example/kirana/bill/Bill.java

package com.example.kirana.bill;

import com.example.kirana.bill.internal.Paise;
import java.util.ArrayList;
import java.util.List;

/** A shop's bill: lines of goods, each with a quantity and a price in paise. */
public final class Bill {
    public record Line(String item, int quantity, long paise) {}

    private final String shop;
    private final List<Line> lines = new ArrayList<>();

    public Bill(String shop) {
        this.shop = shop;
    }

    public Bill add(String item, int quantity, long paise) {
        lines.add(new Line(item, quantity, paise));
        return this;
    }

    public List<Line> lines() {
        return List.copyOf(lines);
    }

    public long totalPaise() {
        return lines.stream().mapToLong(l -> l.quantity() * l.paise()).sum();
    }

    /** The total in rupees with Indian digit grouping, such as ₹1,01,340.00. */
    public String total() {
        return shop + ": " + Paise.asRupees(totalPaise());
    }
}

app/src/com/example/kirana/app/Main.java

package com.example.kirana.app;

import com.example.kirana.bill.Bill;

public class Main {
    public static void main(String[] args) {
        Bill bill = new Bill("Sharma Kirana Store")
                .add("Basmati rice, 25 kg", 12, 2_450_00)
                .add("Toor dal, 30 kg", 4, 4_875_00)
                .add("Sugar, 50 kg", 10, 2_250_00);
        System.out.println(bill.lines().size() + " lines, " + bill.total());
        System.out.println("Main is in module " + Main.class.getModule().getName()
                + ", Bill in module " + Bill.class.getModule().getName());
    }
}

A declaration holds directives, and these are all of them:

Directive What it does
requires M; Reads module M, so this module’s code may use the packages M exports
requires transitive M; Also makes every module that reads this one read M
requires static M; Needs M to compile; at run time M may be absent
exports P; Lets every module that reads this one use the public types of package P
exports P to M; The same, for the named friend modules only
opens P; Allows reflection on everything in P at run time, private members included
opens P to M; The same, for the named modules only
open module Opens every package of the module for reflection
uses S; and provides S with C; Look up implementations of a service, and offer one (the next lesson)

Compile and run two modules

kirana.app requires kirana.bill and may use its exported package; an import from its internal package is a compile error.kirana.apppackage com.example.kirana.appkirana.billjava.baseread by every modulecom.example.kirana.billexported: Billcom.example.kirana.bill.internalnot exported: Paiseusesrequires kirana.bill;import ...Bill: allowedimport ...Paise:compile error

Readability and exports between the lesson's two modules

Text description of the diagram

The diagram shows the two modules of the lesson and the platform module they both read.

  1. kirana.app, at the top, holds the package com.example.kirana.app. Its declaration says requires kirana.bill, so it reads that module.
  2. kirana.bill holds two packages. com.example.kirana.bill, with the class Bill, is exported. com.example.kirana.bill.internal, with the formatter Paise, is not exported; Bill uses it from inside the module.
  3. A solid arrow from kirana.app to the exported package is labelled "import Bill: allowed". A dashed arrow from kirana.app to the internal package is labelled "import Paise: compile error", because a package that is not exported is not visible outside its module, even though Paise is a public class.
  4. kirana.bill, like every module, reads java.base, which every module depends on without saying so.
Two modules: compiled, run, packed as modular JARs and described Python · modules_session.py
from session import run

# How many modules the JDK itself has; then two modules compiled with one javac command, each from its own folder,
# run from the output folder, packed as modular JARs, described, and run again from the JARs.
run(
    """
java --list-modules | wc -l
javac -d out --module-source-path kirana.bill=bill/src --module-source-path kirana.app=app/src --module kirana.app
find out -name "*.class" | sort
java --module-path out --module kirana.app/com.example.kirana.app.Main
jar --create --file mods/kirana.bill.jar -C out/kirana.bill .
jar --create --file mods/kirana.app.jar --main-class com.example.kirana.app.Main -C out/kirana.app .
jar --describe-module --file mods/kirana.bill.jar
jar --describe-module --file mods/kirana.app.jar
java --module-path mods --module kirana.app
"""
)

Output

# openjdk version "25.0.4.1" LTS
$ java --list-modules | wc -l
      69
$ javac -d out --module-source-path kirana.bill=bill/src --module-source-path kirana.app=app/src --module kirana.app
$ find out -name "*.class" | sort
out/kirana.app/com/example/kirana/app/Main.class
out/kirana.app/module-info.class
out/kirana.bill/com/example/kirana/bill/Bill.class
out/kirana.bill/com/example/kirana/bill/Bill$Line.class
out/kirana.bill/com/example/kirana/bill/internal/Paise.class
out/kirana.bill/module-info.class
$ java --module-path out --module kirana.app/com.example.kirana.app.Main
3 lines, Sharma Kirana Store: ₹71,400.00
Main is in module kirana.app, Bill in module kirana.bill
$ jar --create --file mods/kirana.bill.jar -C out/kirana.bill .
$ jar --create --file mods/kirana.app.jar --main-class com.example.kirana.app.Main -C out/kirana.app .
$ jar --describe-module --file mods/kirana.bill.jar
kirana.bill jar:mods/kirana.bill.jar!/module-info.class
exports com.example.kirana.bill
requires java.base mandated
contains com.example.kirana.bill.internal

$ jar --describe-module --file mods/kirana.app.jar
kirana.app jar:mods/kirana.app.jar!/module-info.class
requires java.base mandated
requires kirana.bill
contains com.example.kirana.app
main-class com.example.kirana.app.Main

$ java --module-path mods --module kirana.app
3 lines, Sharma Kirana Store: ₹71,400.00
Main is in module kirana.app, Bill in module kirana.bill

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 modules_session.py

  • The JDK is modules too. This JDK 25 lists 69 modules, from java.base to the jdk.* tools.
  • --module-source-path kirana.bill=bill/src says where a module’s sources are. javac’s documentation calls this module-specific form the simpler one when only a few modules are involved. --module kirana.app compiled that module and the module it requires, each into its own folder below out, named after the module.
  • --module kirana.app/com.example.kirana.app.Main names a module and its main class. At run time each class reports its own module, so Main and Bill really ran as two modules.
  • A modular JAR is a JAR with module-info.class at its root. jar --describe-module reads it back: exports lists the API, contains lists the packages kept inside, and requires java.base mandated is the dependence every module has without writing it. With --main-class recorded, --module kirana.app was enough to start the program.

What strong encapsulation stops

Two copies of the program are compiled against the packed kirana.bill: one imports the formatter from the internal package, and one has a declaration without requires:

An import from a package that is not exported, and a missing requires Python · encapsulation_session.py
from session import run

# kirana.bill is compiled on its own and packed as a modular JAR; then two versions of the program are compiled
# against it: one that uses the internal package, and one whose declaration leaves out the requires directive.
run(
    """
javac -d out/kirana.bill $(find bill/src -name "*.java")
jar --create --file mods/kirana.bill.jar -C out/kirana.bill .
javac -d leaky-out --module-path mods $(find leaky/src -name "*.java")
javac -d noread-out --module-path mods $(find noread/src -name "*.java")
""",
    fails=(
        'javac -d leaky-out --module-path mods $(find leaky/src -name "*.java")',
        'javac -d noread-out --module-path mods $(find noread/src -name "*.java")',
    ),
)

Output

# openjdk version "25.0.4.1" LTS
$ javac -d out/kirana.bill $(find bill/src -name "*.java")
$ jar --create --file mods/kirana.bill.jar -C out/kirana.bill .
$ javac -d leaky-out --module-path mods $(find leaky/src -name "*.java")
leaky/src/com/example/kirana/app/Main.java:4: error: package com.example.kirana.bill.internal is not visible
import com.example.kirana.bill.internal.Paise;
                              ^
  (package com.example.kirana.bill.internal is declared in module kirana.bill, which does not export it)
1 error
[exit status 1]
$ javac -d noread-out --module-path mods $(find noread/src -name "*.java")
noread/src/com/example/kirana/app/Main.java:3: error: package com.example.kirana.bill is not visible
import com.example.kirana.bill.Bill;
                         ^
  (package com.example.kirana.bill is declared in module kirana.bill, but module kirana.app does not read it)
1 error
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 encapsulation_session.py

The two errors name two different rules. Exports: Paise is a public class, but its package is “declared in module kirana.bill, which does not export it”, so outside that module the class might as well not exist. Across modules, public means public to the modules that may see the package. Readability: Bill is in an exported package, but “module kirana.app does not read it”, because nothing requires kirana.bill. The JVM checks exports again at run time, for code that javac never saw.

Reflection follows its own rule. exports allows reflection on public members only, and code that reads private fields, as many mapping and serialisation libraries do, needs the package opened to it with opens. The JDK’s own internal packages have been closed to such reflection since Java 17 (JEP 403). The --add-opens option opens one package for one run, and --add-exports does the same for compile-time and run-time access to public types. Code on the class path lives in the unnamed module, which reads every module. A plain JAR on the module path becomes an automatic module, which exports and opens all its packages.

Check yourself

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 Paise is a public class in com.example.kirana.bill.internal, which kirana.bill does not export. kirana.app requires kirana.bill and imports Paise. What happens?

    Choose one answer.

    Show the answer to question 1

    Answer: A compile error: the package is not visible, because kirana.bill does not export it

    Across modules, a public type is accessible only if its package is exported to the module that uses it. The lesson's run printed "is declared in module kirana.bill, which does not export it".

  2. Question 2 of 5 kirana.bill exports com.example.kirana.bill, but kirana.app's declaration has no requires directive. Which error does javac give for import com.example.kirana.bill.Bill?

    Choose one answer.

    Show the answer to question 2

    Answer: The package is not visible, because module kirana.app does not read kirana.bill

    Readability comes from requires. Without it, the exported package is still invisible, and the message says "but module kirana.app does not read it", a different reason from a missing export.

  3. Question 3 of 5 A library calls setAccessible(true) on a private field of a class in your exported package com.example.model. Which directive lets it, and nothing more?

    Choose one answer.

    Show the answer to question 3

    Answer: opens com.example.model to the library's module

    exports grants access to public types and members only. Reflection on private members needs the package opened, and a qualified opens limits it to the one module that needs it.

  4. Question 4 of 5 kirana.bill declares requires transitive rupees. What does transitive change?

    Choose one answer.

    Show the answer to question 4

    Answer: Every module that reads kirana.bill also reads rupees

    requires transitive gives every module that requires kirana.bill an implied dependence on rupees, so code that uses kirana.bill's API, which mentions rupees types, compiles without its own requires. requires static is the compile-time-only form.

  5. Question 5 of 5 How many modules did java --list-modules report for the JDK 25 build used in this lesson?

    Type a number.

    Show the answer to question 5

    Answer: 69

    The first command of the modules session printed 69: java.base, the other java.* modules of Java SE, and the jdk.* modules with the JDK's tools and extras.

Reflection across modules: opens, open modules and --add-opens, failures reproduced Pro: opens with a Pro, Premium or Ultimate pass

Libraries that turn objects into JSON, table rows or forms read private fields by reflection, and an exported package does not let them. This part reproduces that refusal, compares the four ways to allow it, and does the same for the JDK’s own internals.

In this part:

  • A reflective library refused, and two ways to let it in
  • Exports, opens and open modules compared
  • JDK internals since Java 17

4 runnable examples (Inspector.java, Java, Python, Report.java, report/src/module-info.java) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Migrating class-path code: automatic modules, the unnamed module and split packages Pro: opens with a Pro, Premium or Ultimate pass

Most programs move to modules one JAR at a time, while some libraries still have no module declaration. This part reproduces the problems that a migration meets and the rule behind each one.

In this part:

  • A plain JAR on the module path: names and warnings, reproduced
  • The class path is invisible to named modules, and a package lives in one module
  • Three kinds of modules compared

3 runnable examples (Java, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Exercise: split a three-package app into two modules with minimal exports Pro: opens with a Pro, Premium or Ultimate pass

A minimal module exports the packages its users need and nothing more. This exercise asks for exactly that, with checks you can run yourself, and a rubric for the parts a command cannot check.

In this part:

  • The khata app and the task
  • Checks you can run
  • Rubric: what a minimal, correct split shows
  • When another module really needs the internal package

2 runnable examples (Khata.java, Main.java, MemoryStore.java, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

  1. Fresher to mid · Warm-up

    What is the difference between exports and opens in a module declaration?

    exports P makes the public types of package P, with their public and protected members, usable by other modules at compile time and at run time, reflection on those members included. opens P gives no compile-time access at all; at run time it allows reflection on every type and member of P, private ones included, which is what setAccessible(true) needs. A package can be both exported and opened. Both have qualified forms, exports P to M and opens P to M, that limit access to the named modules, and an open module opens every package at once. In practice you export your API and open only the packages a framework reads by reflection, ideally to that framework's module alone.

  2. Mid to senior

    After your application moves to the module path, a JSON library fails with InaccessibleObjectException while it serialises your Order class. Explain what changed, and give the fix you would ship and the fix you would avoid.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    You move an application to the module path and it no longer starts: two library JARs contain the same package. How do you find out which JARs, and how do you fix it without changing either library's source?

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    Plan the move of a server application with 40 JAR files from the class path to modules. Where do you start, what do you measure first, and how does the application stay shippable after every step?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • module-info.java names a module, the modules it requires and the packages it exports.
  • Compile modules with javac --module-source-path and run them with java --module-path … --module m/main.Class.
  • A public class in a package that is not exported cannot be used from another module.
  • Code must read a module, through requires, before it can use that module’s exported packages.
  • Reflection on private members needs opens; --add-opens opens a package for one run.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.