Создание собственных пакетов в Java
Создавайте собственные пакеты Java: структура директорий, объявление package и компиляция.
Читать чужие пакеты — одно дело. Создать собственный — три небольших шага: выбрать имя, организовать директории и добавить нужное объявление package в начало каждого файла. Компилятор и JVM следуют строгим соглашениям, и после нескольких раз это становится привычкой. Если вы только знакомитесь с пакетами, начните с обзора пакетов Java; эта глава посвящена созданию собственных.
Три правила
Всё, что вы пишете внутри пакета, должно соблюдать ровно три правила:
- Первый не-комментарный оператор файла — это объявление
package. Никакой другой оператор не может предшествовать ему. - Путь к файлу отражает имя пакета.
com.example.util.Stringsдолжен находиться по путиcom/example/util/Strings.javaотносительно какого-либо корня исходных файлов. - Файл называется по имени публичного класса верхнего уровня.
Strings.javaдляpublic class Strings.
Это всё. Остальное — соглашения.
Выбор имени пакета
Компилятор проверяет лишь три правила выше, но несколько соглашений об именовании помогают избежать коллизий и придерживаться идиоматического стиля:
- Только строчные буквы.
com.w3docs.greet, но неcom.W3Docs.Greet. Прописные буквы в именах пакетов допустимы, но нарушают соглашение, которого ожидают все Java-разработчики, и создают проблемы на файловых системах, нечувствительных к регистру. - Используйте обратный порядок домена. Если вы владеете
w3docs.com, ваши пакеты начинаются сcom.w3docs. Это гарантирует глобальную уникальность, чтобы вашGreeterне конфликтовал с чужимGreeterв classpath. - Без ключевых слов Java. Сегмент пакета не может быть зарезервированным словом. Если бы ваш домен был
int.example.com, нельзя было бы написатьcom.example.int— пришлось бы экранировать, напримерcom.example.int_. - Без ведущих цифр в сегменте.
com.3m.tapeнедопустимо, потому что3m— не валидный идентификатор; распространённое решение:com._3m.tape.
Если не объявить пакет вообще, классы попадут в пакет по умолчанию — подходит для разовых набросков, но классы из него нельзя импортировать в пакетный код, поэтому в реальных проектах избегайте этого.
Минимальный пример
Допустим, вы хотите создать небольшой утилитарный пакет com.w3docs.greet. Вот его структура:
project/
└── src/
└── com/
└── w3docs/
└── greet/
├── Greeter.java
├── Greeting.java
└── Main.javaGreeting.java:
package com.w3docs.greet;
public record Greeting(String language, String text) {}Greeter.java:
package com.w3docs.greet;
public class Greeter {
public Greeting greet(String name) {
return new Greeting("en", "Hello, " + name + "!");
}
}Main.java:
package com.w3docs.greet;
public class Main {
public static void main(String[] args) {
System.out.println(new Greeter().greet("Ada").text());
}
}Все три файла начинаются с одной строки package com.w3docs.greet;. Поскольку они находятся в одном пакете, Greeter и Main могут использовать Greeting без import — импортировать нужно только типы из других пакетов.
Компиляция и запуск из командной строки
Из корня project/ стандартные команды выглядят так:
# Compile every .java file under src/ into a parallel tree under out/
javac -d out $(find src -name "*.java")
# Run a main class by its fully-qualified name, telling the JVM where out/ is.
java -cp out com.w3docs.greet.Main-d out указывает javac, куда помещать скомпилированные файлы .class; при этом структура директорий пакета сохраняется. -cp out (classpath) — способ, которым java находит их во время выполнения. Подробнее об этом рассказывает следующая глава про classpath в Java.
Если объявление пакета не совпадает с расположением файла относительно корня исходных файлов, javac принимает его (пакет определяется объявлением, а не путём) — но java потерпит неудачу во время выполнения с ошибкой NoClassDefFoundError. Всегда держите их в соответствии.
Подпакеты не являются вложенными
Может показаться, что com.example «содержит» com.example.util, но для Java-компилятора это два несвязанных пакета, которые случайно имеют общий префикс имени. Класс в com.example не имеет особого доступа к членам с пакетной видимостью из com.example.util. Наследования пакетов не существует.
Что подпакеты дают — это дерево директорий, удобное для навигации, и логичное место для группировки связанного кода. В большинстве реальных проектов подпакеты организованы по функциональным областям (auth, billing, parser) или по слоям (controller, service, repository) — но ни тот, ни другой подход не даёт подпакету дополнительного доступа.
Единицы компиляции и package-info.java
Файл .java также называется единицей компиляции и может содержать:
- Одно объявление
package(или ни одного — для пакета по умолчанию). - Любое количество объявлений
import. - Любое количество объявлений типов, из которых не более одного может быть
public, и оно должно совпадать с именем файла.
Существует также специальный файл package-info.java, единственная задача которого — хранить Javadoc и аннотации уровня пакета:
/**
* Greeting utilities for the W3Docs Java book.
*/
@NullMarked
package com.w3docs.greet;Никаких типов — только комментарий, необязательные аннотации и строка package. Такой файл встречается в хорошо задокументированных библиотеках.
Рабочий пример
Эта программа объявляет два типа, которые в реальном проекте находились бы в одном пакете, и использует их вместе. Исполняемый виджет здесь сглаживает структуру файлов, чтобы вы видели всю схему в одном листинге, но в реальном проекте каждый публичный тип жил бы в своём собственном файле .java в структуре директорий, показанной выше.
Что дальше
Пользовательские пакеты охватывают код, который вы пишете сами. Однако большую часть времени вы будете опираться на пакеты JDK — классы стандартной библиотеки для коллекций, ввода-вывода, работы со временем и так далее. Следующая глава — это обзор тех из них, с которыми вы будете работать чаще всего. Перейдите к встроенным пакетам Java.