Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Модули

Пакеты раскладывают классы по папкам, но не мешают использовать любой 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.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.