Модули
Пакеты раскладывают классы по папкам, но не мешают использовать любой public-класс из любого пакета — даже тот, что задуман как внутренняя деталь. Модуль (Java 9) — следующий уровень: это группа пакетов с описанием, какие пакеты она открывает наружу и какие другие модули ей нужны. Сам JDK тоже разбит на модули: java.base с основными классами, java.sql для баз данных, java.net.http с HTTP-клиентом и другие.
module-info.java
Модуль описывает файл module-info.java в корне его папки. exports открывает пакет другим модулям, requires объявляет зависимость. Пакеты, не указанные в exports, снаружи не видны, даже если классы в них public:
// src/shop.core/module-info.java
module shop.core {
exports shop.core.api;
}Модуль shop.core открывает только пакет shop.core.api. Имя модуля обычно совпадает с именем его главного пакета. Модуль java.base подключается сам, писать requires java.base не нужно.
Проект из двух модулей
Сделаем библиотеку цен shop.core и приложение shop.app. Каждый модуль лежит в своей папке внутри src:
src/
├── shop.core/
│ ├── module-info.java
│ └── shop/core/
│ ├── api/PriceCalculator.java
│ └── internal/Discounts.java
└── shop.app/
├── module-info.java
└── shop/app/Main.javaОткрытый класс библиотеки пользуется внутренним классом скидок:
// src/shop.core/shop/core/api/PriceCalculator.java
package shop.core.api;
import shop.core.internal.Discounts;
public class PriceCalculator {
public static double finalPrice(double price, String customer) {
return price - price * Discounts.percentFor(customer) / 100;
}
}// src/shop.core/shop/core/internal/Discounts.java
package shop.core.internal;
public class Discounts {
public static int percentFor(String customer) {
return customer.equals("Анна") ? 10 : 0;
}
}Приложение объявляет, что ему нужен shop.core, и импортирует класс из открытого пакета. Класс в именованном модуле обязан лежать в пакете, поэтому Main здесь записан в классической форме, а не компактным файлом:
// src/shop.app/module-info.java
module shop.app {
requires shop.core;
}// src/shop.app/shop/app/Main.java
package shop.app;
import shop.core.api.PriceCalculator;
public class Main {
public static void main(String[] args) {
IO.println("Анна платит " + PriceCalculator.finalPrice(50, "Анна") + " €");
IO.println("Олег платит " + PriceCalculator.finalPrice(50, "Олег") + " €");
}
}Сборка и запуск
javac с ключом --module-source-path находит модули в папке src, а --module перечисляет, какие собрать. Запускают с --module-path — путём, где лежат готовые модули, — и указывают модуль и главный класс через /:
javac -d out --module-source-path src --module shop.core,shop.app
java --module-path out --module shop.app/shop.app.MainРезультат:
Анна платит 45.0 €
Олег платит 50.0 €В папке out появятся две папки — out/shop.core и out/shop.app — с файлами .class и скомпилированными module-info.class.
Модули JDK
Команда java --list-modules выводит все модули JDK, а --describe-module показывает описание одного из них:
java --describe-module java.net.httpРезультат (номер версии у вас может отличаться):
java.net.http@25.0.4.1
exports java.net.http
requires java.base mandated
contains jdk.internal.net.http
contains jdk.internal.net.http.common
contains jdk.internal.net.http.frame
contains jdk.internal.net.http.hpack
contains jdk.internal.net.http.websocketВидно, что модуль открывает один пакет, а пять внутренних (contains) прячет. В компактном файле все модули JDK доступны сразу, а в своём модуле их нужно объявить: requires java.net.http;, requires java.sql;.
Нужны ли модули
Код без module-info.java по-прежнему работает: он попадает в безымянный модуль (unnamed module) и запускается через обычный -cp (classpath), как в уроке Пакеты и модификаторы доступа. Так устроено большинство приложений на Spring. Модули чаще встречаются в библиотеках и больших системах, где важно жёстко отделить открытый API от внутренностей. Ещё два слова, которые стоит узнавать: requires transitive передаёт зависимость дальше тем, кто подключил ваш модуль, а opens разрешает библиотекам вроде Jackson обращаться к классам пакета через рефлексию.
Частая ошибка
Если Main импортирует класс из пакета, который модуль не экспортирует, — например, import shop.core.internal.Discounts;, — сборка остановится, хотя класс public:
src/shop.app/shop/app/Main.java:3: error: package shop.core.internal is not visible
import shop.core.internal.Discounts;
^
(package shop.core.internal is declared in module shop.core, which does not export it)
1 errorПохожая ошибка возникает, если забыть requires shop.core; в module-info.java приложения — компилятор подскажет, что модуль shop.app не читает (does not read) shop.core. Решение — добавить пакет в exports или зависимость в requires.