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. Практика: граємося з exports і видимістю
Крок 1: видаляємо exports
У 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 — отримаєте помилку. Уникайте циклів у графі залежностей.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ