Руководство по пакетам в Java

Введение
Пакеты в Java появились с самого начала, во времена, когда язык еще назывался Oak. Их описание уже присутствует в его ранних спецификациях.
Java-пакеты реализуют так называемое пространство имен (namespace), позволяющее использовать в проекте файлы с одинаковыми именами. Такой подход существует с давних времен во многих языках.
Также с помощью пакетов разработчики структурируют файлы удобным и понятным для себя способом, т. к. чем их больше используется в программе, тем в них сложнее становится ориентироваться. Если они будут размещаться бессистемно, то это рано или поздно приведет к полному запутыванию кодовой базы и дезориентации программистов.
И наконец, с помощью пакетов можно ограничивать доступ (из других пакетов) как целиком к классам, так и к их отдельным элементам.
Об этом и многом другом и пойдет речь в данной статье.
1. Что такое пакет
Пакетом (пространством имен) в Java называется структура вложенных по какому-то признаку папок с размещенными в них классами (интерфейсами, перечислениями, аннотациями), необходимыми проекту.
Вы можете думать о пакетах как о папках на вашем компьютере, в которых хранятся файлы, сгруппированные в соответствии с их функциональностью (назначением). Поскольку проект может состоять из сотен или тысяч классов, имеет смысл поддерживать порядок, помещая их в пакеты.
Например, если какие-то классы размещаются в папке lesson1 по адресу:

ru/topjava/startjava/lesson1/
, то с точки зрения Java они будут храниться в следующем пакете (в качестве разделителя уже используется точка вместо слеша):

ru.topjava.startjava.lesson1
При этом в начале каждого из классов необходимо разместить строку, указывающую на его принадлежность к конкретному пакету, используя ключевое слово package и имя пакета (точка с запятой в конце — обязательна):

package ru.topjava.startjava.lesson1;
Если подобная строка не будет присутствовать в классах, то они автоматически добавятся в безымянный пакет (unnamed package).
Имя пакета ru.topjava.startjava.lesson1 означает следующее (подробно правила именования пакетов рассматриваются в последующих главах):
  • ru.topjava — доменное имя разработчика кода, записанное в реверсивном виде (в нашем случае эта часть указывает на принадлежность к организатору обучения)
  • startjava — код пишется для курса StartJava (имя проекта)
  • lesson1 — классы реализованы в рамках первого урока
Создадим структуру из вложенных папок и файл MyFirstApp.java, отобразив ее в консоли с помощью tree:

> tree /F
src
 └──ru
    └──topjava
       └───startjava
           └──lesson1
                MyFirstApp.java
Код класса, размещенного в пакете ru.topjava.startjava.lesson1, может выглядеть следующим образом:

package ru.topjava.startjava.lesson1;

public class MyFirstApp {

    public static void main(String[] args) {
        System.out.println("Я познаю Java!");
        System.out.println("Я программист.");
    }
}
2. Примеры пакетов
На примере Java API вы можете ознакомиться со структурой стандартных пакетов.
Например, берем java.io — это пакет. Именно так, с использованием точки в качестве разделителя, он используется в Java. Но с точки зрения файловой системы, эта запись трактуется как: в папку под названием java вложена папка с именем io. Вы можете самостоятельно в этом убедиться.
Для этого откройте архив src.zip, найдя его расположение у себя на компьютере, например, с помощью консольной команды where. В нем хранится множество вложенных папок с исходниками.

> where /R C:\ src.zip
C:\Program Files\BellSoft\LibericaJDK-20\lib\src.zip
C:\Program Files\JetBrains\IntelliJ IDEA 2022.3\lib\ant\src.zip
Таких архивов может найтись несколько: все зависит от количества установленных JDK от разных вендоров — выбирайте любой.
Открыв данный архив и перейдя к месту хранения, например, класса String, вы увидите, что пакеты — это обычные вложенные папки с классами (путь к классу с определенного момента совпадает с именем пакета):
Класс String как раз хранится в пакете java.lang, полную информацию о котором вы можете найти на странице его описания.
И таких классов в Java — тысячи (1, 2). Если бы они все размещались бессистемно, то разобраться в них было бы крайне тяжело.
Классы в пакетах обычно группируются исходя из их функциональности, а не разбрасываются хаотично по всему приложению. Разработчики стараются размещать все элементы, относящиеся к одной функции, в один пакет.
Например:
  • java.io содержит классы, отвечающие за ввод/вывод
  • java.math содержит классы, обеспечивающие математические операции
  • java.time содержит классы для работы с датой и временем и т. д.
Тут стоит оговориться, что классы группируются в пакеты не всегда исходя из их назначения, т. к. на конкретных проектах все может быть иначе в силу их структуры и масштабности. Это всегда дискуссионный вопрос, который решается "на месте". Но вам, как начинающим, можно руководствоваться этим простым правилом.
Кроме того, в отдельных папках рекомендуется хранить class-файлы, сторонние библиотеки, изображения, документацию и т. д. Все эти меры упрощают понимание и сопровождение приложения.
Рассмотрим еще примеры.
На картинке ниже изображены два пакета: java.lang и java.util. Важно отметить, что папка src (сокр. от source) не считается за пакет, т. к. является общей (по соглашению) для всех исходников и размещается в корне проекта. По этой причине src не указывается в именах пакетов. В своих проектах вам тоже не нужно будет это делать.
Обратимся теперь за примером к Spring Framework, который изучается на нашей стажировке. Данный фреймворк содержит тысячи классов, разложенных по пакетам.
Например, класс BeanFactory.java размещается в пакете:

org.springframework.beans.factory
ApplicationContext.java — в другом пакете:

org.springframework.context
Из структуры репозитория и имен пакетов видно, что у этих классов корневой папкой для исходников является уже не src, a группа вложенных папок. При этом часть пути до org не будет входить в имя пакетов.

src/main/java/org/springframework/
src/main/kotlin/org/springframework
src/test/java/org/springframework/
src/test/kotlin/org/springframework/
Такая структура характерна для больших проектов с множеством классов, языков программирования, наличием Unit-тестов и т. д.
Данная структура является стандартом для приложений, использующих систему сборки проекта под названием Maven (Gradle). На данном этапе достаточно просто об этом знать, не вдаваясь в подробности про системы сборок.
3. Назначение пакетов
3.1. Структурирование проекта
На картинке ниже java-файлы беспорядочно свалены в одну папку.
Вместо этого исходники могли бы быть разложены по смыслу в разные папки, например, со следующими названиями:
При этом папки могут содержать подпапки (подпакеты), еще более структурирующие файлы.
В итоге классы можно было бы разложить, например, в следующие пакеты, группируя исходя из их назначения:

ru.topjava.basejava.exception
ru.topjava.basejava.sql
ru.topjava.basejava.storage.serializer
Подобное разделение, безусловно, упрощает поддержку и понимание проекта в целом, а также назначение каждого его класса.
3.2. Ограничение доступа
В Java существует четыре модификатора доступа, одним из которых является package private (его еще называют default или no modifier). У него нет своего ключевого слова. Данный вид доступа применяется по умолчанию, когда никакой из модификаторов не указан явно.
Он предназначен для управления доступностью содержимого пакетов. В пакете можно определить классы, которые будут недоступны за его пределами.
Например, этот класс виден только в пакете, в котором находится сам:

package ru.topjava.example;

class SomeClass {
    void method() {
        ...
    }
}
Такой подход зачастую используется для классов, которые являются частью внутренней реализации какой-то библиотеки и не предназначены для использования сторонними разработчиками. Это просто способ сокрытия того, что не должно быть доступно внешнему коду.
Хоть и нельзя утверждать со 100% уверенностью (зависит от страны, команды и т. д.), но зачастую в коммерческой разработке этот модификатор не используют (хотя в JDK он применяется повсеместно), руководствуясь принципом, что если что-то не написано, то оно забыто. Системы, проверяющие код в автоматическом режиме на соответствие стандартам, его не пропустят.
Да и к тому же модификатор package-private не обеспечивает настоящей инкапсуляции, т. к. мы можем поместить наш класс в пакет к классам с закрытым для кода извне доступом. Затем из нового класса легко получить доступ к содержимому любого класса с пакетным доступом.
3.3. Поддержка классов с одинаковыми именами
Повторяющиеся имена классов в Java в рамках одного проекта — обычное явление, которое не приводит к конфликтам имен только потому, что они хранятся в разных пакетах.
Для того, чтобы обратиться к нужному классу и избежать коллизии при совпадении имен, когда компилятор не может выбрать нужный тип, необходимо указать его полное имя. Оно включает в себя название пакета, где размещается класс, и его непосредственное имя.
Например, полное имя класса Random из стандартного пакета будет java.util.Random. А полное имя вашего класса может быть ru.topjava.startjava.Random. При этом возможности данных типов могут использоваться даже в рамках одного класса без каких-либо проблем.
В Java есть два стандартных класса под названием Date: один хранится в пакете java.sql, а второй — в java.util. Из-за того, что пакеты у них разные, и к каждому классу можно обратиться через его полное имя, ошибок при компиляции не возникнет.
Возможность сослаться через имя пакета на конкретный тип данных и позволяет иметь в одном проекте классы с одинаковыми именами.
Пример использования классов с одинаковыми именами рассматривается в другой главе.
4. Правила создания пакетов
Когда Java набирала свою популярность во времена развития веба, то появилось универсальное правило, смысл которого заключался в следующем: если какая-то компания разрабатывает библиотеки или целое приложение, то у нее наверняка есть свой сайт с доменным именем, например, topjava.ru. А как мы знаем, доменные имена повторяться не могут.
В итоге решили, что для обеспечения уникальности пакетов их имена следует начинать с префикса в виде доменных имен второго уровня их разработчиков, записанных в обратном порядке. Если сайт называется topjava.ru, то пакет будет начинаться с имени ru.topjava.
Такой подход можно объяснить тем, что более информативные и значимые составляющие пакета должны размещаться ближе к классу, который в нем находится. Значимость папок, хранящих класс тем выше, чем они к нему ближе. А в доменных именах — все наоборот, вот и пришлось пойти на такой реверс в именовании пакетов.
Если у вас нет собственного доменного имени, то для создания уникальных имен пакетов придется придумать комбинацию с малой вероятностью повторения (например, название аккаунта на GitHub — com.github.ichimax в моем случае). Но для серьезных проектов все же стоит потратиться для приобретения собственного доменного имени.
В некоторых ситуациях имя домена может быть недопустимым для пакета. В таком случае, если оно содержит символы, не разрешенные для именования, или ключевое слово Java, то его необходимо преобразовать в символ подчеркивания.
Например:

hyphenated-name.example.org → org.example.hyphenated_name
example.int → int_.example
123name.example.com → com.example._123name
Общие правила:
1. имя пакета должно быть простым и отражать общий смысл объединенных в нем классов
2. все слова, входящие в имя пакета, должны писаться строчными буквами без использования camelCase или подчеркиваний, а также в единственном числе:
Имена пакетов пишутся строчными буквами, чтобы избежать конфликта с именами классов или интерфейсов.

java.io
jakarta.enterprise.lang.model
org.hibernate.action.internal
com.apple.quicktime.v3
3. Если какая-либо из частей имени пакета является ключевым словом, начинается с цифры или любого другого символа, который не разрешен в качестве начального символа идентификатора, добавьте к нему префикс в виде символа подчеркивания
4. В Java все стандартные пакеты начинаются с зарезервированных слов java или javax. Это означает, что имена ваших пакетов не могут начинаться с этих слов
5. Для именования пакета желательно использовать короткие слова или сокращения, например, util вместо utilities, lang вместо language или аббревиатуры — awt, http и проч. Только не придумываете свои собственные сокращения, используйте общепринятые
6. Следует избегать более одного слова подряд в элементах имени пакета. Если все же этого не удастся сделать, то они записываются слитно:

org.springframework.kafka.aot
org.hibernate.beanvalidation
gov.whitehouse.socks.mousefinder
7. Имя пакета необходимо отделить от последующего кода пустой строкой
8. Точка с запятой в конце имени пакета — обязательна
9. Пакет объявляется без переноса строки. Ограничение на длину строки на объявление пакета (как и импортов) не распространяется
10. В одном файле может располагаться только один package
11. Нельзя переименовывать пакет, не переименовав папку, в котором хранятся его классы (и наоборот)
12. Если в классе явно не указывается принадлежность к какому-либо пакету, то такой класс попадает в безымянный пакет. При этом безымянная папка не создается. Такие пакеты предназначены только для небольших приложений или когда вы только начинаете процесс разработки. В реальных проектах классы вне пакетов не создаются
13. Классы из безымянного пакета не могут быть импортированы
14. Для маленьких приложений вы можете помещать свои исходные файлы в папку src без создания пакетов
5. Импорт классов
Указывать все время полное имя класса — достаточно утомительное занятие, захламляющее код (хотя без этого, бывает, не обойтись). Для облегчения этого процесса в Java существует возможность подключения нужного функционала с помощью оператора import.
Для того, чтобы использовать в своей программе методы или поля из любого класса, который не располагается в том же пакете, что и ваш класс, необходимо делать импорт.
Например, чтобы воспользоваться методами класса Scanner, следует перед объявлением класса прописать следующее:

import java.util.Scanner;
Данная запись состоит из ключевого слова import, имени пакета и класса, возможности которого вам требуются (точка с запятой в конце — обязательна). Если бы данный способ импорта не был внедрен в Java, то его отсутствие сильно бы захламило код, т. к. пришлось бы каждый раз указывать относительный путь в виде пакета до нужного класса.
В этом случае создание объекта выглядело бы следующим образом:

java.util.Scanner console = new java.util.Scanner(java.lang.System.in);
Это очень громоздко. А с учетом всего сказанного, эту строку можно переписать так:

Scanner console = new Scanner(System.in);
Хочу акцентировать ваше внимание на том, что import нужен для того, чтобы Java-программистам было проще использовать классы из других пакетов, не вводя все время их полные имена, например, String вместо java.lang.String. Данный оператор просто делает код короче и читабельней. Это синтаксический сахар, уменьшающий количество печатаемых вами символов.
Технически import требуется компилятору, чтобы он знал, по какому адресу (в каком пакете) искать используемые в вашей программе классы.
5.1. Способы импорта
Импортировать нужный функционал можно следующими способами:
  • импорт конкретного класса
  • импорт конкретного метода/поля класса
  • импорт всего пакета целиком
Импорт конкретного класса представляет собой наиболее распространенный способ подключения:

import java.util.Arrays;
import org.springframework.beans.BeansException;
import ru.topjava.basejava.storage.ArrayStorage;
Импорт конкретного метода/поля (появился в Java 5) используется для импорта статических членов класса (статический импорт):

import static java.lang.Math.max;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static ru.topjava.basejava.storage.DataStreamSerializer;
Последний способ — подключение всего пакета со всем содержимым. Для этого используется * (символ подстановки):

import java.util.*;
import jakarta.annotation.*;
import org.springframework.beans.factory.config.*
Если взглянуть на примеры из реальных проектов (1, 2), то видно, что какое бы количество классов не импортировалось, в коде не используется импорт всего пакета, а статический импорт применяется преимущественно в классах с тестами. Учитывайте это!
Рассмотрим причины, по которым многие программисты используют импорт конкретного класса, а не всего пакета. Это связано с тем, что явный импорт:
  • показывает, какие внешние классы используются в коде
  • снижает вероятность возникновения коллизий имен, возникающих при импорте пакетов, содержащих классы с одинаковыми именами
  • позволяет избежать проблем, когда в какой-либо пакет разработчик добавляет новый класс (например, в последнюю версию сторонней библиотеки был добавлен новый функционал), а вы при этом использовали *. Это внезапно для вас может привести к ошибкам компиляции из-за конфликта имён, которых раньше не было
5.2. Две неочевидных особенностей импорта
Если классы находятся в одном пакете и используют возможности друг друга, то выполнять import не нужно, т. к. они смогут найти друг друга без каких-либо сложностей.
Это связано с тем, что в каждый класс автоматически импортируются все классы из текущего пакета. Текущий пакет — это пакет, в котором лежит текущий класс.
Кроме того, возможно, вы заметили, что класс String никогда не приходится импортировать. Вы его просто используете, как есть. Все дело в том, что он, да и все остальные классы из пакета java.lang, являются фундаментальными классами и также импортируются компилятором автоматически.
Получается, что в любой класс Java сама импортирует два пакета: текущий и java.lang.
С учетом всего сказанного, импорты класса MyFirstApp, описанного ранее, будут выглядеть так (хоть мы этого и не видим):

package ru.topjava.startjava.lesson1;

import java.lang.*;
import ru.topjava.startjava.lesson1.*;

public class MyFirstApp {

    public static void main(String[] args) {
        System.out.println("Я познаю Java!");
        System.out.println("Я программист");
    }
}
Обратите внимание на строку System.out.println («Я познаю Java!»). Вам не приходится делать импорт, чтобы использовать возможности вывода текста на консоль, т. к. класс System входит в пакет java.lang. Это очень полезная магия!
5.3. Подпакеты
Пакеты, находящиеся внутри другого пакета, называются подпакетами. Например, java.util.concurrent и java.util.function — это подпакеты пакета java.util. Они не импортируются рекурсивно, например, при помощи import java.util.*. Их нужно импортировать явно.
Несмотря на то, что пакеты хоть и выглядят вложенными, на самом деле вы должны рассматривать java.util, java.util.concurrent, java.util.function и т. п., как три разных, не связанных между собой пакета, имена которых имеют общую часть.
5.4. Частые вопросы про import
Очень часто начинающие программисты задаются рядом вопросов, связанных с импортом. На некоторые из них я подготовил ответы:
import работает так же, как директива #include в языках C/C++?
Нет, у них разный механизм работы: import не помещает код импортируемого класса в ваш класс, как это делает #include. В C/C++ же данная директива заменяется перед компиляцией на содержимое файла, на который указывает — происходит обычный копипаст. Затем класс передается компилятору со всеми включенными в него другими файлами, как если бы это был один файл.

Например, на место #include <stdio.h> копируется содержимое файла stdio.h, в то время, как import java.util.Arrays означает, что если в вашем файле нет данного класса, то его необходимо искать по полному имени, указанном в импорте.
— влияет ли работа import на производительность (скорость выполнения) программы?
Работа import (сколько бы их не было) никак не влияет на производительность, т. к. он используется только на этапе компиляции. В class-файлах никаких импортов просто физически нет.
увеличивает ли import размер class-файлов?
Импорт не увеличивает размер байт-кода, т. к. в процессе компиляции компилятор заменяет каждое имя класса на полное имя, а затем удаляет оператор импорта. Таким образом, оператор не присутствует в байт-коде и не включат в место своего объявления код импортируемых классов. Он существует только в исходниках для поиска классов.
—есть ли какая-либо разница между class-файлами при использовании import или при написании полного имени классов?
Class-файлы с использованием импорта или без будут идентичными как по размеру, так и по содержанию.

Вы можете посмотреть байт-код class-файлов, используй javap -c <your class> — никакой разницы между ними не будет. В обоих случаях байт-код будет иметь полные имена, помещенные туда компилятором.
влияет ли import на время компиляции?
Если в классе много импортов, то при компиляции могут возникнуть небольшие задержки, которые будут настолько малы, что о них не стоит беспокоиться.
6. Компиляция и запуск
Компилировать и запускать классы, размещенные в безымянных пакетах, мы уже научились ранее. В этом же разделе пойдет речь о выполнении этих действий для классов, находящиеся в именованных пакетах.
6.1. Создание папки для хранения class-файлов
Взглянем еще раз на картинку, которую мы уже видели ранее, где свалены в одну папку множество файлов:
Обратите внимание, что на ней class-файлы размещаются совместно с java-файлам, чего быть не должно. Обычно class-файлы хранятся в отдельной папке под названием out (bin или classes).
В корне вашего проекта всегда должна находиться отдельная папка для хранения сгенерированных компилятором файлов. Ее создание может выглядеть так:

> md out & ls -a
./  ../  .git/  .gitignore  out/  src/
На самом деле ее можно не создавать вручную, т. к. при первой же компиляции она (со всеми подпапками) создастся автоматически (либо ее сгенерирует ваша среда разработки).
6.2. Стандартный способ компиляции и запуска
6.2.1. Компиляция
Компиляция выполняется по следующей схеме:

javac [options] [source-files]
, где [options] обозначает параметры javac, а [source-files] указывает на исходные файлы, которые требуется скомпилировать.
Подробнее это выглядит так:
  • консоль необходимо открыть в корне вашего проекта (где находится папка src)
  • в ней напишите через пробел:
    • команду javac
    • опцию -d (сокр. от destination), указывающую на папку для размещения сгенерированных class-файлов
    • имя этой папки. В нашем случае — out
    • опцию -verbose (необязательно), если вы хотите видеть, что происходит в процессе компиляции: время компиляции, какие файлы компилируются и т. д.
    • относительный путь до папки с классами и маску вида *.java, чтобы не перечислять каждый класс
Пример компиляции всех классов в пакете:

> javac -d out/ src/ru/topjava/startjava/lesson1/*.java
Пример компиляции конкретного класса

> javac -d out/ src/ru/topjava/startjava/lesson1/MyFirstApp.java
После выполнения команды, благодаря ключу -d, в папке out (как и сама папка, если на момент компиляции ее еще не было) будет создана иерархия папок, повторяющая иерархию для компилируемых классов. При этом class-файлы появятся не в корне out, а по адресу /out/ru/topjava/startjava/lesson1/.
Если программа содержит несколько классов, которые при этом размещаются в разных пакетах, то процесс компиляции несколько изменится.
Рассмотрим пример из трех простых классов, описывающих собаку, ее хозяина и класс запуска. Все эти классы лежат в разных пакетах.

package ru.topjava.clinic.animal;

public class Dog {

    private String name;

    public Dog(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void bark() {
        System.out.println("Bark! Bark! Bark!");
    }
}

package ru.topjava.clinic.person;

import ru.topjava.clinic.animal.Dog;

public class Owner {

    private String name;
    private Dog dog;

    public Owner(String name, Dog dog) {
        this.name = name;
        this.dog = dog;
    }

    public String toString() {
        return name + " и " + dog.getName();
    }
}

package ru.topjava.clinic;

import ru.topjava.clinic.animal.Dog;
import ru.topjava.clinic.person.Owner;

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("Spike");
        Owner owner = new Owner("John", dog);

        System.out.println("Меня и мою собаку зовут " + owner);
        System.out.println("Голос, " + dog.getName());
        dog.bark();
    }
}
Взглянем на получившуюся иерархию:

> tree /F src
src
└───ru
    └───topjava
        └───clinic
            │   Main.java
            │
            ├───animal
            │       Dog.java
            │
            └───person
                    Owner.java
Выполним компиляцию:

javac -d out/ src/ru/topjava/clinic/animal/Dog.java src/ru/topjava/clinic/person/Owner.java src/ru/topjava/clinic/Main.java
Как видно, нам пришлось вручную указать все используемые классы и пути до них, что не всегда бывает удобно. Можно воспользоваться вариантом короче, указав параметр -sourcepath, говорящий компилятору, что все необходимые исходники нужно искать в папке src. При этом обязательно нужно указать путь до класса с методом main(). Компилятор заглянет в класс Main, чтобы узнать, какие зависимости (другие классы) в нем используются, и будет искать их, начиная с папки src и во всех ее подпапках:

javac -d out/ -sourcepath src/ src/ru/topjava/clinic/Main.java
Если не указать -sourcepath, то возникнет ошибка компиляции.
6.2.2. Запуск
Запуск классов выполняется по следующей схеме:

java [options] [mainclass]
, где [options] обозначает параметры команды java, а [mainclass] указывает на полное имя запускаемого класса (не путать с именем файла), содержащего метод main().
Подробнее это выглядит так:
  • консоль по прежнему должна быть открыта в корне вашего проекта
  • в ней напишите через пробел:
    • команду java
    • опцию -verbose (необязательно)
    • опцию -cp (или -classpath), указывающую на папку с сгенерированными class-файлами и внешними библиотеками (обычно в виде отдельных файлов с расширением .jar), к которым программа должна получить доступ во время выполнения
    • имя этой папки. В нашем случае — out
    • полное имя класса (пакет + имя), содержащего метод main (без указания расширения файла .class). Имя пакета необходимо записывать, используя точки
Пример:

> java -cp out/ ru.topjava.startjava.lesson1.MyFirstApp
Вывод:

Я познаю Java!
Я программист.
Запуск программы, состоящей из нескольких классов, ничем не будет отличаться, т. к. нужно указывать только класс, содержащий метод main(). Остальные зависимости будут найдены в папке out автоматически.
Пример:

java -cp out/ ru.topjava.clinic.Main
Вывод:

Меня и мою собаку зовут John и Spike
Голос, Spike
Bark! Bark! Bark!
Упрощенный способ запуска однофайловых программ
Начиная с Java 11, однофайловые программы можно запускать не используя явно этап компиляции. Данный способ мы уже разбирали ранее.
Упрощенный способ запуска многофайловых программ
Начиная с Java 22, многофайловые программы можно запускать, минуя этап явной компиляции.
Давайте попробуем запустить классы из примера про собаку и ее хозяина (рассмотренный ранее), пропустив процесс компиляции.
Вместо двух строк:

javac -d out/ -sourcepath src/ src/ru/topjava/clinic/Main.java
java -cp out/ ru.topjava.clinic.Main
достаточно написать одну, без указания каких-либо параметров (class-файлы не будут созданы):

java src\ru\topjava\clinic\Main.java
Вывод:

Меня и мою собаку зовут John и Spike
Голос, Spike
Bark! Bark! Bark!
Мы запустили класс под названием Main из одноименного файла, который в начале компилируется (в памяти), а затем в нем вызывается метод main(). Поскольку код в этом классе ссылается на класс Dog, средство запуска находит файл Dog.java в файловой системе и компилирует его класс в памяти. Затем тоже самое происходит и с классом Owner.
Что интересно в данном механизме, так это то, что классы не обязательно должны размещаться в одной папке — они могут располагаться в разных пакетах, что и продемонстрировано в данном примере.
Это достигается за счет того, что средство запуска вычисляет размещение всех классов по именам пакетов, прописанным в них.
При этом будут скомпилированы только те классы, которые участвуют в программе, а те, которые просто лежат рядом в тех же папках, но не являются частью компилируемых классов — не будут задействованы.
Существуют некоторые различия между явной компиляцией и упрощенным запуском. В режиме последнего:
  • в файловой системе не создаются class-файл
  • java-файлы могут компилироваться постепенно (по мере их использования), а не все сразу перед началом запуска программы
6.3. Дополнительные способы компиляции и запуска
6.3.1. Options-файлы (файлы аргументов)
Если вы работаете над какой-то программой, и вам надоело все время прописывать пути до классов, а также параметры компиляции и запуска, то есть решение, облегчающее эту работу. Кроме того, данный способ подойдет для компиляции множества классов, когда использование * невозможно, т. к. компилировать требуется не все файлы в папке, а выборочно.
Идея заключается в создании специальных файлов-аргументов, как для компиляции, так и для запуска, содержащих весь необходимый набор параметров и файлов.
Компиляция
Компиляция выполняется по следующей схеме:

> javac @argfile
, где @argfile — имя файла аргументов, которое обязательно должно начинаться с @.
Обычно данный файл, предназначенный для компиляции, называют options, не указывая у него никакого расширения. В нем можно размещать любое количество имен файлов и параметров, используя как абсолютный, так и относительный путь. При этом символ подстановки * такими файлами не поддерживается.
Абсолютный путь — это путь до ваших классов, начинающийся от корня файловой системы. Например, D:\Java\StartJava\src\ru\topjava\clinic\animal\Dog.java.
А относительный путь — это путь до классов, начинающийся от корня проекта. В данном случае src\ru\topjava\clinic\animal\Dog.java.
Если вы размещаете options-файлы в папке с java-файлами (а не в корне), то для указания адреса, например, для папки out, при использовании относительного пути, необходимо использовать следующий синтаксис ..\..\, который позволит подняться до корня проекта.
Например, ..\..\..\..\out, где каждый ..\ переносит на одну папку выше в иерархии папок.
Какой способ выбрать — зависит от ситуации: если предполагается, что options-файлами будут пользоваться и другие люди, то лучше использовать относительный путь. А если только вы — абсолютный.
Создать options можно в любом текстовом редакторе, а размещать в папке с компилируемыми классами или в корне проекта — все зависит от необходимости и удобства.
Разберем пример использования options-файла при компиляции.
Например, пусть классы Dog, Person и Main, разбираемые ранее, хранятся в одной папке src\ru\topjava\clinic\
Необходимо их скомпилировать, указав при этом папку для class-файлов.
С использованием относительного пути:

-d ..\..\..\..\out
Dog.java
Person.java
Main.java
С использованием абсолютного пути:

-d D:\Java\StartJava\out
Dog.java
Person.java
Main.java
Компиляция (консоль должна быть открыта в папке с компилируемыми классами, а не в корне проекта):

> javac @options
Можно и так (тогда в options уже не нужно перечислять файлы):

> javac @options *.java
Если вы работаете с терминалом, для которого символ @ является ключевым словом, например, с Windows PowerShell, то его необходимо экранировать (включая название файла), используя одинарные кавычки:

> javac '@options'
Можно использовать другой вариант, поместив компилируемые классы и параметры в разные файлы (sources и options). Это позволит не писать руками компилируемые файлы, например, если их много, или они хранятся в разных пакетах, а генерировать их содержимое с помощью командной строки. При этом удобнее sources и options размещать уже в корне проекта.
Для разных платформ генерация может быть следующей:
Для macOS/Linux (вы можете использовать этот способ и в Windows, например, в Git Bash):

find ./src/ -type f -name "*.java" > sources
Данную запись можно расшифровать так: найди (find) в папке ./src/ (и в подпапках) среди файлов (-type f) только те, которые имеют расширение *.java (-name «*.java»). Все, что будет найдено, помести (>) в файл sources.
Посмотрим содержимое файла sources:

> cat sources
./src/ru/topjava/clinic/Dog.java
./src/ru/topjava/clinic/Main.java
./src/ru/topjava/clinic/Person.java
Если вам не нужны все классы, которые хранятся в src, то следует указать путь до конкретной папки.
Для Windows:

dir src /S /B *.java > sources
Получи (dir) из папки src и из всех ее подпапок (/S) имена файлов без дополнительной информации (/B), которые имеют расширение *.java. Все что было найдено, помести (>) в файл sources.
При таком подходе каждый раз, когда мы добавляем новый или удаляем существующий исходный файл, нужно перегенерировать содержимое sources.
В итоге компиляция будет следующей:

javac @options @sources
6.3.2. Использование Globstar
В Bash существует опция под названием globstar, позволяющая использовать двойной подстановочный знак ** в качестве команды для рекурсивной обработки. В Windows вы можете попробовать globstar в Git Bash.
Для того чтобы посмотреть, включен ли globstar (по умолчанию он выключен), выполните следующую команду:

> shopt globstar
globstar        off
, где shopt — это сокращение от shell options (параметры оболочки).
Для его включения используйте команду:

> shopt -s globstar
Для отключения используйте команду:

> shopt -u globstar
После включения этой опции компиляция будет выглядеть так:

> javac -d out/ src/**/*.java
Эта запись означает: пройдись по всем подпапкам в src и скомпилируй там все java-файлы.
Если классы в папке src зависят друг от друга (код одной программы размещается в разных пакетах), то компилировать нужно так:

> javac -d out/ -sourcepath src/ src/**/*.java
6.3.3. Использование Pipe
Pipe (канал) — это механизм объединения нескольких команд для получения конечного результата.
Для компиляции можно объединить команды find и javac:

> find ./src/ -type f -name "*.java" -exec javac -d ./out/ '{}' +
Запуск
Запуск с помощью файлов-аргументов выполняется по следующей схеме:

> java @argfile
, где @argfile — имя файла аргументов, которое должно начинаться с символа @. Обычно данный файл, предназначенный для запуска, называют optionsj.
Пример содержимое такого файла с использованием относительного пути:

-cp ..\..\..\..\out
ru.topjava.clinic.Main
С использованием абсолютного пути:

-cp D:\Java\StartJava\out
ru.topjava.clinic.Main
Пример запуска (консоль должна быть открыта в папке с компилируемыми классами, а не в корне проекта):

> java @optionsj
7. Возможные ошибки и их решение
При использовании пакетов на первых порах могут возникать ошибки, на поиск решения которых может уходить значительное время. Для его экономии и облегчения освоения данной темы была написана эта глава в формате «вопрос-ответ».
Общее правило: если при компиляции или запуске у вас возникают ошибки, которые не связаны с кодом, то попробуйте закомментировать строку с оператором package и повторить свои действия. Если после этого ошибка уйдет, то поиск проблемы необходимо сосредоточить на пакетах и всём, что с ними связано.
А теперь перейдем к обсуждению конкретных проблем и их решению. Для разнообразия буду использовать globstar, описанный ранее.
  • При компиляции исходников сгенерированные классы появляются в корне папки out
    Это связано с тем, что вы забыли указать принадлежность классов к пакету. Напишите в начале классов package и имя пакета.
  • Генерируемые классы появляются не в папке out, а в папке с исходниками
    Причина в том, что при компиляции не была указана папка для генерируемых компилятором файлов. Необходимо использовать параметр -d и путь до папки out.
  • При запуске выдается ошибка, что не был найден класс с main-методом (Could not find or load main class)
    Причиной может быть все что угодно:
  • не указанно (или указано с опечатками) полное имя класса при запуске
  • при запуске указывается имя class-файла (с расширением class), а не имя класса (без расширения). Компилятор будет думать, что class является частью имени класса, что не верно. Удалите class у имени файла в аргументе
  • неверно указан путь до папки out
  • попробуйте вместо относительного пути до классов (если используете options-файлы) использовать абсолютный. Если это сработает, то ищите проблему в относительном пути
Больше подробностей по ссылке.

  • При компиляции появляется ошибка error: <identifier> expected
    Данная ошибка связана с тем, что в имени вашего пакета используются зарезервированные Java слова. Решение этого вопроса разбиралось ранее.
  • Во время компиляции выдается ошибка error: reference to ClassName is ambiguous
    Данная ошибка связана с конфликтом имен, когда в рамках одного класса используются другие классы с одинаковыми именами. Из-за того, что имена одинаковые, компилятор не может понять, какой из них следует создать и использовать. В этом случае нужно явно указать полное имя класса
Рассмотрим в качестве примера три простых класса, находящихся в разных пакетах:

ru.topjava.abc
ru.topjava.def
ru.topjava.ghi
При этом два класса имеют одинаковое имя Message и метод print().

package ru.topjava.abc;

import ru.topjava.def.Message;
import ru.topjava.ghi.Message;

public class Main {

    public static void main(String[] args) {
        Message msg1 = new Message();
        msg1.print();

        Message msg2 = new Message();
        msg2.print();
    }
}

package ru.topjava.def;

public class Message {

    public void print() {
        System.out.println("def.Message");
    }
}

package ru.topjava.ghi;

public class Message {

    public void print() {
        System.out.println("ghi.Message");
    }
}
Скомпилируем классы из корня проекта, используя globstar:

> javac -d out/ src/ru/topjava/**/*.java
Вы можете сделать тоже самое, используя -sourcepath, если не работает globstar:

> javac -d out/ -sourcepath src/ru/topjava/ src/ru/topjava/abc/Main.java
Компилятор выдаст ошибку:

src\ru\topjava\abc\Main.java:4: error: a type with the same simple name is already defined by the single-type-import of Message
import ru.topjava.ghi.Message;
^
src\ru\topjava\abc\Main.java:9: error: reference to Message is ambiguous
        Message msg1 = new Message();
        ^
  both class ru.topjava.def.Message in ru.topjava.def and class ru.topjava.ghi.Message in ru.topjava.ghi match
src\ru\topjava\abc\Main.java:9: error: reference to Message is ambiguous
        Message msg1 = new Message();
                           ^
  both class ru.topjava.def.Message in ru.topjava.def and class ru.topjava.ghi.Message in ru.topjava.ghi match
src\ru\topjava\abc\Main.java:12: error: reference to Message is ambiguous
        Message msg2 = new Message();
        ^
  both class ru.topjava.def.Message in ru.topjava.def and class ru.topjava.ghi.Message in ru.topjava.ghi match
src\ru\topjava\abc\Main.java:12: error: reference to Message is ambiguous
        Message msg2 = new Message();
                           ^
  both class ru.topjava.def.Message in ru.topjava.def and class ru.topjava.ghi.Message in ru.topjava.ghi match
5 errors
Из текста ошибки видно, что в классе импортируются два класса с одинаковыми именами, но из разных пакетов; а также что ссылка на Message неоднозначна.
Первая ошибка связана с тем, что в Java вы не можете импортировать два класса с одним и тем же именем. Но если мы не будем импортировать нужный нам класс, то как тогда использовать его возможности? Внимательный читатель скажет, что в одной из предыдущих глав мы рассматривали способ обращения к классам через указание его полного имени. В итоге нам нужно импортировать один любой класс (обычно, который используется чаще всего), а ко второму обращаться через его полное имя. Либо (что лучше) использовать для обоих классов полные имена, чтобы все были осведомлены, что классы с одинаковыми именами определены в разных пакетах.
В итоге измененный класс будет выглядеть так:

package ru.topjava.abc;

public class Main {

    public static void main(String[] args) {
        ru.topjava.def.Message msg1 = new ru.topjava.def.Message();
        msg1.print();

        ru.topjava.ghi.Message msg2 = new ru.topjava.ghi.Message();
        msg2.print();
    }
}
Компиляция и запуск программы на этот раз пройдет без ошибок:

> javac -d out/ src/ru/topjava/**/*.java
> java -cp out/ ru.topjava.abc.Main
def.Message
ghi.Message
Существуют и другие рекомендации, позволяющие избежать этих ошибок:
  • если повторяющиеся имена классов являются вашими, а не частью внешних библиотек, к которым у вас нет доступа, то просто переименуйте один из своих классов
  • можно создавать промежуточные классы-обертки, которые не будут ничего делать, кроме как содержать поле экземпляра класса, имя которого дублируется
Заключение
В данной публикации мы подробно обсудили пакеты языка Java, процесс компиляции и запуска программ при их использовании. Надеюсь, что из статьи вы получили исчерпывающую информацию по данным темам. Если у вас останутся вопросы, то пишите их в комментариях.
Автор: Чимаев Максим
Оцените статью, если она вам понравилась!