Модуль 2. Урок 4. Пакеты в Java.

Для сохранения прогресса вступите в курс. Войти или зарегистрироваться.

alt text Это базовая тема. Двигаться дальше, к следующему уроку, стоит только после вдумчивого понимания этой темы. Смелее задавайте вопросы в обсуждениях и не ленитесь читать уже заданные вопросы - возможно там уже есть для вас ответ или подсказка :)


Что такое пакеты?

Пакеты, по сути, являются файловой и логической структурой связей классов в мире java. Очень схоже с файловой системой компьютера. На уровне файловой системы пакеты это и есть папки, в которых лежат другие папки (подпакеты) и классы. Но пакеты не всегда описывают напрямую всю структуру проекта. На практике проект включает в себя различные ресурсы, а структура папок, которую мы назначаем как имена пакетов для наших классов — может быть лишь небольшой частью целого проекта. Ведь, кроме основного кода в пакетах, у нас должны быть еще и тесты, библиотеки или даже другие языки программирования в проекте в целом.

Пакеты необходимы не только для красивого размещения файликов по папочкам или решения серьезной проблемы коллизии имен, но и для разделения доступа классов друг к другу. Именно благодаря пакетам мы можем через точку . получать доступ к нужным нам классам различных библиотек.

Как аналогию, можно привести пример с адресами.

Представим себя в роли некого класса, который выполняет некие функции для этого мира. Для того чтобы этот мир к нам обратился - нам нужно иметь конкретный адрес, путь по которому можно будет взаимодействовать с нами. Пускай это будет адрес, например:

package   страна.область.город.район.улица.дом
         кореневой_пакет.подпакет.подпакет.подпакет.имяКласса

Что такое пакет для класса

Для класса его пакет — это его местоположение в проекте, относительно других классов. Напоминает почтовый адрес. Расположения класса в пакете — влияет на доступность полей и методов между классами, не только за счет модификаторов доступа private, default, protected, public, но и благодаря "расстоянию" между классами. Под расстоянием, в данном случае, подразумевается какой путь по папкам нужно пройти от одного класса к другому.

Как создать класс внутри пакета?

По сути нам нужно создать текстовый файл с расширением java в нужной папке.

Сначала пишем имя любимого редактора в терминале, потом существующий путь, а потом имя будущего текстового файла. Пример для редактора nano:

UserName BASH ~/projectName/src
$ nano -f /io/hexlet/xo/view/ConsoleView.java

Или с помощью sublime:

UserName BASH ~/projectName/src
$ subl /io/hexlet/xo/view/ConsoleView.java

Если команда subl не найдена и Вы уверены в том, что sublime установлен — проверьте, в переменных среды окружения, наличие прописанного пути к папке, в которой лежит subl.

Можно и обычным блокнотом создать нужный файл в нужной директории. Создать сам файл можно и любым другим удобным для вас способом.

Файл создан. Но класса в нем нет. Создадим класс:

class ConsoleView {
    //some field & methods
}

Теперь у нас есть и файл в нужной папке и класс в файле. Но сам класс ничего не знает про то, что он принадлежит некому пакету. Исправим это:

package io.hexlet.xo.view;

class ConsoleView {
    //some field & methods
}

Не забываем сохранять файл!

Ключевые моменты верного создания класса внутри пакета:

  • класс должен лежать внутри папки, в которую прописан его пакет
  • внутри класса должно использоваться ключевое слово package
  • ключевое слово package должно быть в первой строке кода
  • после package нужно указать полное имя пакета, с корня пакета до пакета(папки) в которой размещен класс. Но не само имя класса.

Корень пакетов

А как же задать корневой каталог (папку) как основу пакетов, в котором уже и происходит ветвление этих всех под-пакетов (под-папок)? Почему, например, папка io является корнем для классов проекта, а папка src — не является пакетом, да и вообще не входит в пакетную структуру проекта?

Все очень просто: корневым пакетом считается тот который первым прописан в каждом java-файле после слова package, в текущем проекте. А в самом проекте по мимо папки src, на ее уровне и выше, — могут быть много различных папок и файлов, для работы с проектом в целом. Но это не делает их корневым пакетом, или вообще частью пакетов java, в этом проекте, если они не включены в имена пакетов в классах, после ключевого слова package. Пример структуры папок и пакетов:

-/projectName // Папка проекта. Может содержать кучу всякой всячины. 
            README.md // файл с описанием. В пакет никак не входит, но входит в проект.
            +/out // Папка для скомпилированных файлов *.class    
            -/src // Папка для исходных файлов *.java
                -/io // корень пакета
                    StartClass.java // package io;
                   -/hexlet
                           OneMoreClass.java // package io.hexlet;
                          -/xo
                             -/controllers
                                 ControllerOne.java // package io.hexlet.xo.controllers;
                                 Controller2.java   // package io.hexlet.xo.controllers;  
                             -/model
                                 ModelClaas1.java  // package io.hexlet.xo.model;
                                 ModelClass2.java  // package io.hexlet.xo.model;
                             -/view
                                 SomeView.java  // package io.hexlet.xo.view;

Пример содержимого класса OneMoreClass.java:

package io.hexlet;

class OneMoreClass {
    // тут некий код, переменные или просто пустота.
}

Пример содержимого класса SomeView.java:

package io.hexlet.xo.view;

class SomeView {
    // просто метод для дальнейших примеров.
    public void message() {
        System.out.println("Some very important message!");
    }
}

Как видно из примеров выше — любой класс в этом проекте всегда содержит, в имени пакета на первом месте, папку io. Это и будет корневым пакетом проекта. На месте папки io могла быть папка com или любое другое имя. Важно что бы все java-классы текущего проекта указывали на эту папку как на первую после ключевого слова package. Если хоть у одного класса в этом проекте папка io не будет прописана — то такой класс как бы и не в проекте, а просто мусор в папке.

package класса всегда:

  1. должен начинаться с одинакового имени каталога, общего для всех классов проекта;
  2. точно соответствует пути по которому лежит класс, начиная с имени корневого пакета;
  3. не содержит в себе имя самого класса и любого другого класса.

Имена пакетов

  • имя состоит из одного слова (желательно)
  • без заглавных букв (желательно)

Совокупность имен под-пакетов делает проект уникальным, не похожим на миллионы других. Даже если, в каждом проекте в мире, 100% будет класс Main.java — то они маловероятно пересекутся и помешают друг-другу. Но даже если пересекутся — это решаемо, рассмотрим это в дальнейших примерах.


Применение пакетов

Как уже было сказано выше — пакет можно сравнить с адресом. Воспользуемся все тем же примером ветвления папок(пакетов). И попробуем применить к задачке.

Допустим в классе StartClass.java нам нужно создать экземпляр класса SomeView.java, для использования его метода. Как это можно реализовать:

package io;

class StartClass {
    // создаем объект класса SomeView
    io.hexlet.xo.view.SomeView someView = new io.hexlet.xo.view.SomeView();

    public static void main(String... args) {
        someView.message() // на экран выведется: "Some very important message!"
    }
}

Только благодаря прописанному пути к классу SomeView, класс StartClass может вообще узнать о его существовании.

Но прописывание таких длинных путей — просто не удобно. Представляете если бы нам приходилось создавать подобный объект раз так 10 в этом коде?! Ужас. И по этому в java есть такое понятие как импортирование.

Импортирование пакетов

Для того чтобы не писать длинные пути к каждому конкретному классу, при каждом его использовании — нужно сделать импорт только один раз этого класса, в нуждающемся классе, в котором мы собираемся применять импортируемый класс. Для этого применяется ключевое слово import. Импорты пишутся между строчкой package ... и строчкой объявления класса. Перепишем предыдущий пример с применением ключевого слова import:

package io;

import io.hexlet.xo.view.SomeView; // Расширение файла в конце не пишем!!! Только имя класса!

class StartClass {
    // создаем объект класса SomeView
    SomeView someView = new SomeView(); // имя класса доступно без имени пакета

    public static void main(String... args) {
        someView.message() // на экран выведется: "Some very important message!"
    }
}

Представим, что в пакете io.hexlet.xo.view очень много разных классов. И они нам все нужны тут в классе StartClass, как их все разом заимпортировать?

Решение приходит со звездочкой *.

Перепишем предыдущий пример с импортированием любых классов из пакета io.hexlet.xo.view:

package io;

import io.hexlet.xo.view.*; // все классы из пакета импортированы

class StartClass {
    // создаем объект класса SomeView
    SomeView someView = new SomeView(); // имя класса доступно без имени пакета

    public static void main(String... args) {
        someView.message() // на экран выведется: "Some very important message!"
    }
}

Стоит обратить внимание, что когда в классе много импортов не только из текущего проекта, но и из других проектов и библиотек — могут возникнуть коллизии в обращении к импортированным классам. Например, если в разных библиотеках есть свой класс SomeView и они оба заимпортированны "за компанию" массово через *, то Java не будет знать какой класс использовать при вызове эго по имени... Их же больше одного.

В такой ситуации придется прописывать явный, полный путь при каждом обращении к имени класса.

Например, вот так:

package io;

import io.hexlet.xo.view.SomeView; // конкретный класс импортирован из определенного пакета
import com.some_priject.SomeView; // конкретный класс импортирован из определенного пакета стороннего гипотетического проекта

class StartClass {
    // создаем объект класса SomeView
    io.hexlet.xo.view.SomeView someView = new io.hexlet.xo.view.SomeView(); // Указываем полный путь к классу текущего проекта

    com.some_priject.SomeView otherView = new com.some_priject.SomeView(); // Указываем полный путь к классу стороннего гипотетического проекта

    public static void main(String... args) {
        someView.message() // на экран выведется: "Some very important message!"
    }
}

Импортирование по умолчанию

Не зависимо от того, написал-ли программист вообще слово import, перед определением своего класса — java всегда автоматически, не явно, импортирует в текущий класс два пакета:

  1. java.lang.*;. В ручную в коде это можно прописать так: import java.lang.*; Это одна из стандартных библиотек java.
  2. Текущий пакет, в котором находится класс. В ручную это выглядело бы так: import . или import ./, или import полный.путь.к.папке.с.этим.файлом. Но, поскольку это делает java за программиста, не стоит импортировать текущий пакет в класс.

Обратите внимание на строку System.out.println("Some very important message!"); в классе SomeView:

package io.hexlet.xo.view;

class SomeView {
    // просто метод для дальнейших примеров.
    public void message() {
        System.out.println("Some very important message!");
    }
}

Класс SomeView видит класс System и его открытые поля, в частности поле out, благодаря стандартному импорту java.lang.*;, т.к. System входит в пакет java.lang.

Статическое импортирование

Этот аспект будет рассмотрен в "Модуль 3. Урок 3. Уровни методов в Java." Поскольку там мы лучше познакомимся с ключевым словом static.

Как компилировать и запускать классы в пакетах

Для компиляции, как вам уже известно, используется команда javac.

Для компиляции отдельного файла в конкретном пакете, нужно прописать команду так: javac src/com/package_name/package_name/ClassName.java. Обратите внимание, что для команды javac мы прописываем все пути через слеш, а не через точки. И обязательно нужно указать расширение файлов.

Если этот файл (класс) ClassName.java использует в своем коде другой класс, из соседнего пакета — это уже зависимость. Нужно подсказать компилятору, где еще лежат сорцы, от которого может зависеть компилируемый класс, с помощью ключа -sourcepath. Выглядит это так: javac -sourcepath src src/com/package_name/package_name/ClassName.java или так: javac src/com/package_name/package_name/ClassName.java -sourcepath src.

Для того что бы класс-файлы не лежали рядом с исходниками, а в отдельной папочке, применим ключ -d: javac src/com/package_name/package_name/ClassName.java -sourcepath src -d out.

Для того, что бы компилятор прошелся по всем директориям в папке, например, src и скомпилировал там все исходники — нужно ввести команду так: javac src/**/*.java (пройтись по всем папкам в папке src и скомпилировать там все файлы *.java). И лучше сразу разложить класс-файлы в отдельную папку: javac src/**/*.java -d out.

Для запуска java-приложений используется командаjava; будь-то отдельный класс или целый пакет с расширением `.jar`.*

Поскольку наш исполняемый файл ClassName.class лежит в отдельной папочке и в своей некой ветке директорий (пакете), то теперь нужно не просто написать java ClassName, а указать classpath (путь к классам). Это делается с помощью ключа -classpath, сокращенно: -cp. Команда запуска ClassName.class будет выглядеть так: java -cp out com.package_name.package_name.ClassName.

Обратите внимание:

  • В данной команде ключ пути к классам должен идти первым, а потом уже имя самого класса.
  • И имя класса должно быть без расширения (в отличии от javac)!
  • Если даже есть зависимые классы — все равно нужно указывать только тот класс, с которого начинается программа. Это класс со специальным main-методом. Остальные классы, которые затрагивает запускаемый класс, будут использованы автоматически.

Полезные ссылки:

Документация:

Группы для общения:

Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Javascript, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →