1. Экспорт пакетов: кто видит ваш код?
В мире до Java 9 всё было просто. Только вот не очень безопасно. Если класс был public, его мог видеть кто угодно при наличии доступа к JAR или classpath. Это приводило к тому, что внутренние классы и даже целые пакеты могли случайно или намеренно использоваться вне вашего проекта. Да, можно было спрятать что-то с помощью private или package-private, но если класс public — он доступен всем.
С появлением модулей это изменилось. Теперь даже если класс объявлен как public, его нельзя использовать из другого модуля, если пакет, в котором он лежит, не экспортирован явно через module-info.java.
Как это выглядит на практике
Допустим, у вас есть структура проекта:
my-app/
src/
core/
com/example/core/
CoreService.java
InternalHelper.java
module-info.java
app/
com/example/app/
Main.java
module-info.java
В файле core/module-info.java вы пишете:
module core {
exports com.example.core;
}
Тогда только классы из пакета com.example.core будут доступны другим модулям. Всё, что находится в других пакетах (например, com.example.core.internal), будет невидимо — даже если там есть public-классы. Это и есть модульная инкапсуляция.
Мини-демо: «видно/невидимо»
Внутри core:
// com/example/core/CoreService.java
package com.example.core;
public class CoreService {
public void doWork() {
System.out.println("Work done!");
}
}
Внутри app:
// com/example/app/Main.java
package com.example.app;
import com.example.core.CoreService;
public class Main {
public static void main(String[] args) {
CoreService service = new CoreService();
service.doWork();
}
}
Если вы уберёте строку exports com.example.core; из core/module-info.java, то при компиляции получите ошибку:
error: package com.example.core is not visible
Даже если CoreService — public!
2. Импорт зависимостей: requires и строгая изоляция
В JPMS нельзя просто так взять и использовать класс из другого модуля. Нужно явно объявить зависимость через requires.
Пример
В файле app/module-info.java:
module app {
requires core;
}
Теперь модуль app может использовать всё, что экспортирует модуль core.
Если вы забудете написать requires core;, получите ошибку компиляции:
error: package com.example.core is not visible
или
error: cannot access CoreService
Важный момент
- requires работает на уровне модулей, а не пакетов.
- Вы не можете «импортировать» только один пакет — только весь экспортируемый модуль.
3. Сравнение: модули против public/private
Модули добавляют новый уровень инкапсуляции, который находится «над» классами и пакетами.
| Уровень | Что регулирует? | Как работает? |
|---|---|---|
| private | Доступ внутри класса | Только внутри одного файла |
| package-private (по умолчанию) | Доступ внутри пакета | Все классы в одном пакете |
| public | Доступ для всех | Любой код в любом месте |
| module | Доступ между модулями | Только экспортированные пакеты |
Ключевая идея:
- Класс может быть public, но если его пакет не экспортирован, он доступен только внутри модуля.
- Экспорт пакета через exports — как «окно», через которое виден ваш код другим модулям.
Пример: спрятанная реализация
// com/example/core/internal/SecretSauce.java
package com.example.core.internal;
public class SecretSauce {
public void addMagic() {}
}
Если вы НЕ экспортируете пакет com.example.core.internal в module-info.java, то ни один внешний модуль не сможет использовать этот класс, даже если он public!
4. Пример: core и app — API и реализация
Рассмотрим типовой сценарий: модуль core предоставляет API, модуль app — использует его.
Структура:
core/
com/example/core/
CoreAPI.java
com/example/core/impl/
CoreImpl.java
module-info.java
app/
com/example/app/
Main.java
module-info.java
core/module-info.java:
module core {
exports com.example.core; // Только API!
// Не экспортируем com.example.core.impl
}
app/module-info.java:
module app {
requires core;
}
com/example/core/CoreAPI.java:
package com.example.core;
public interface CoreAPI {
void doSomething();
}
com/example/core/impl/CoreImpl.java:
package com.example.core.impl;
import com.example.core.CoreAPI;
public class CoreImpl implements CoreAPI {
@Override
public void doSomething() {
System.out.println("Doing something!");
}
}
com/example/app/Main.java:
package com.example.app;
import com.example.core.CoreAPI;
// import com.example.core.impl.CoreImpl; // Это вызовет ошибку компиляции!
public class Main {
public static void main(String[] args) {
// CoreImpl impl = new CoreImpl(); // Ошибка! Пакет не экспортируется.
// Можно использовать только то, что видно через API.
}
}
Результат:
- Модуль app видит только то, что экспортирует core.
- Реализация (impl) спрятана, даже если классы там — public.
5. Практика: играем с export и видимостью
Шаг 1: удаляем export
В core/module-info.java закомментируйте строку:
// exports com.example.core;
Теперь попробуйте скомпилировать проект. Ожидайте ошибку компиляции в модуле app — он не увидит классы из com.example.core.
Шаг 2: экспортируем только API
Верните строку exports com.example.core;, но не экспортируйте com.example.core.impl. Попробуйте в модуле app импортировать класс из impl. Снова получите ошибку компиляции — всё честно!
Шаг 3: public-класс в неэкспортируемом пакете
Создайте public-класс в неэкспортируемом пакете. Попробуйте использовать его из другого модуля — не получится. Это и есть модульная инкапсуляция в действии.
6. Как это выглядит в IDE и «на пальцах»
- При попытке импортировать класс из неэкспортируемого пакета IDE подсветит ошибку.
- Подсказка объяснит, что пакет не экспортируется модулем.
- Добавьте exports для нужного пакета — ошибка исчезнет.
Схема: уровни доступа
+---------------------+
| МОДУЛЬ |
| (module-info.java) |
+---------------------+
|
v
+---------------------+
| ПАКЕТЫ |
| (package, export) |
+---------------------+
|
v
+---------------------+
| КЛАССЫ |
| (public/private) |
+---------------------+
|
v
+---------------------+
| МЕТОДЫ/ПОЛЯ |
| (public/private) |
+---------------------+
7. Важные нюансы и особенности
Экспорт «только для друзей»: exports ... to
Иногда нужно экспортировать пакет только для определённых модулей (например, для тестов или специальных расширений):
exports com.example.core.internal to my.special.module, my.test.module;
Теперь только указанные модули увидят этот пакет.
Можно ли экспортировать несколько пакетов?
Да! Просто добавляйте новые строки exports:
exports com.example.core;
exports com.example.core.api;
Можно ли экспортировать «всё»?
Нет. В модульной системе Java вы всегда явно указываете, что экспортировать. Это и есть её сила.
8. Типичные ошибки при работе с модульной инкапсуляцией
Ошибка №1: Ожидание, что public-класс всегда виден.
Если пакет не экспортирован через module-info.java, класс будет невидим для других модулей, даже если он public. Это самая частая «ловушка» для новичков.
Ошибка №2: Забыли объявить requires.
Если ваш модуль использует классы из другого модуля, но не написал requires, получите ошибку компиляции. Не забывайте явно описывать зависимости.
Ошибка №3: Попытка экспортировать один и тот же пакет из двух модулей.
В JPMS каждый пакет может быть экспортирован только одним модулем. Если нарушить это правило, компилятор вас остановит.
Ошибка №4: Нарушение структуры — module-info.java не в том месте.
Файл module-info.java должен лежать в корне исходников модуля, иначе модуль не будет распознаваться.
Ошибка №5: Циклические зависимости между модулями.
Если модуль A требует B, а B требует A — получите ошибку. Избегайте циклов в графе зависимостей.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ