Зона кода

А давайте немного попрограммируем...

Структура программ на языке Java

Java

Предлагаю вниманию читателей статью, написанную давно — в 2012 году. Зачем ей пылиться на винчестере? Пусть лучше пылится на сайте, решил я.

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

Когда писалась статья, во дворе свирепствовала 7-я версия языка Java. Сейчас, в феврале 2018-го года, за окном уже вовсю резвится 9-я версия. Но, полагаю, что вся информация, изложенная в статье, справедлива, всё же, и для последней версии языка, хоть я этого и не проверял (а надо бы проверить). Ведь обычно, насколько я помню, совместимость программ, написанных на старых версиях, с новыми компиляторами сохранялась.

Статья адресована новичкам. Но и мне самому она, полагаю, будет полезна, если я решусь вернуться к языку Java после многолетнего перерыва в работе с ним. Пожалуй, именно информация о структуре программ на Java будет для меня наиболее полезна в случае такого возвращения, поскольку она наиболее сильно, по сравнению с остальными элементами языка, "затёрлась" в моей голове.

Всю информацию я попытался изложить достаточно подробно, и в то же время, систематично.

Итак, передаю слово себе самому 6-летней давности.

Вступление

Здравствуйте, уважаемый читатель! В этой статье рассматривается структура программы на языке Java, а также освещаются другие вопросы, связанные с этой темой: компиляция и выполнение программ, указание загрузчику пути для поиска классов, импорт классов и т. д.

Статья адресована тем, кто только-только начинает своё знакомство с языком Java. Но она может быть полезной и для тех, кто привык компилировать и запускать свои программы с помощью какой-либо интегрированной среды разработки, например, Eclipse, и не умеет делать это с использованием командной строки.

В связи с этим вспоминаю случай из моего педагогического прошлого. Студентка, изучавшая язык Java под моим чутким руководством, благополучно сдала зачёт за семестр. А во втором семестре выяснилось, что девушка не умеет компилировать и запускать программы из командной строки. Оказалось, что ранее она пользовалась NetBeans’ом, который выполнял всю работу по сборке и выполнению приложений за неё. И при этом она успешно написала полтора десятка не самых простых программ!

Ну что ж, переходим к делу. Начну с того, что в понятие “программа” можно вкладывать два разных смысла. С одной стороны, программа — это набор текстовых файлов с расширением java, содержащих исходный код на языке Java. С другой стороны, программа — это набор двоичных файлов с расширением class, содержащих так называемый байтовый код и предназначенных для выполнения виртуальной машиной Java. Обратите внимание на то, что эти файлы должны быть размещены в каталогах, имеющих вполне определенную структуру. Об этом мы ещё поговорим в дальнейшем.

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

Заметим, что скомпилированные файлы вместе со структурой каталогов, в которых они содержатся, могут быть помещены в так называемый jar-архив. В дальнейшем программа может быть запущена прямо из jar-архива. Но это тема для отдельной статьи, а в этой jar-архивы рассматриваться не будут.

Начнём рассматривать структуры программ на языке Java, двигаясь от простого к сложному.

Один файл — один класс

В простейшем случае программа состоит из одного файла, содержащего исходный код единственного класса. Подробное рассмотрение понятия класса выходит за рамки данной статьи. Для нас сейчас важно лишь понимать, что класс представляет собой контейнер, содержащий программный код. Вообще, весь исполняемый код, написанный на языке Java, хранится внутри классов или интерфейсов (об интерфейсах речь пойдёт позже). Вне классов и интерфейсов могут располагаться лишь инструкции import и package, а также комментарии.

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

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

Для компиляции файла необходимо вызвать из текущей директории (т. е. директории, в которой содержится файл) команду javac (компилятор), передав ей, в качестве параметра, полное имя файла, например:

javaс Alpha.java

Результатом выполнение этой команды будет создание двоичного файла с расширением class, содержащего байтовый код нашего класса. Имя этого файла (без расширения) будет совпадать с именем класса, представляющим собой идентификатор языка Java. В нашем случае скомпилированный файл будет называться Alpha.class. Созданный файл будет помещён в ту же директорию, в которой находится файл с исходным кодом.

Для запуска программы на выполнение необходимо вызвать команду java (интерпретатор) с именем запускаемого класса в качестве параметра, например:

java Alpha

В результате начнётся выполнение метода main() запускаемого класса (Alpha в нашем примере).

Компиляция одного класса в одном файле

Компиляция одного класса в одном файле (щёлкните для увеличения)

Один файл — несколько классов

В одном файле может содержаться несколько классов. В этом случае имя файла (без расширения) должно совпадать с именем любого из этих классов.

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

Запускать программу можно посредством запуска любого класса, содержащего метод main().

Следующий рисунок иллюстрирует ситуацию, при которой в файле Alpha.java содержатся описания трёх классов: Alpha.class, Beta.class, Gamma.class, причём в классе Beta содержится метод main().

Компиляция нескольких классов в одном файле

Компиляция нескольких классов в одном файле (щёлкните для увеличения)

Несколько классов в нескольких файлах

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

Скомпилировать такую программу можно, запустив команду javac с параметрами, совпадающими с именами файлов, разделённых пробелами, например:

javac Alpha.java Beta.java Gamma.java

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

Можно также скомпилировать сразу все файлы с расширением java, находящиеся в текущей директории:

javac *.java

Предположим, что код нашей программы находится в файлах Alpha.java, Beta.java, Gamma.java, содержащих описание одноимённых классов, причём в одном из этих файлов имеется также описание класса Delta. Пусть также выполнение программы должно начинаться с метода main() класса Delta. Тогда компиляцию и выполнение программы можно продемонстрировать с помощью следующей иллюстрации:

Компиляция нескольких классов в нескольких файлах

Компиляция нескольких классов в нескольких файлах (щёлкните для увеличения)

А что будет, если запустить компиляцию только одного файла, являющегося частью программы, состоящей из нескольких файлов? Ответ на этот вопрос зависит от того, используются ли в этом файле классы, код которых нём не содержится.

Если такие классы не используются, то будут скомпилированы только классы, код которых имеется в этом файле.

Если такие классы используются, то будет предпринята попытка найти байтовый код этих классов. Если байтовый код найден, а исходный отсутствует, то никаких дополнительных действий, помимо компиляции нашего единственного файла, произведено не будет. Если, наоборот, имеется исходный код, а байтовый отсутствует, то этот исходный код будет скомпилирован.

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

Ну а если компилятору не удалось найти ни исходный код классов, ни их байтовый код, то наш единственный файл он компилировать не будет, а вместо этого выведет сообщение об ошибке.

Заметим, что область поиска файлов передаётся компилятору через параметры (но это не единственный вариант). Данному вопросу будет посвящён отдельный раздел статьи. Если же компилятор "не знает", где искать файлы, то поиск он осуществляет в той директории, в которой находится наш компилируемый файл (затем поиск продолжается в том месте, где хранится стандартная библиотека Java; об этом мы ещё поговорим подробнее).

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

А вот если код класса Beta содержится в файле Gamma.java, то он не будет обнаружен, т. к. компилятор не станет просматривать содержимое этого файла в поисках исходного кода Beta. Как следствие, код класса Beta будет скомпилирован только в том случае, если будет компилироваться код класса Gamma. Тогда код Beta скомпилируется "заодно" с Gamma.

Итак, как мы видим, компиляция только одного файла может привести к компиляции ещё нескольких файлов. Разумеется, при компиляции каждого из них могут потребоваться и другие классы; в этом случае могут возникнуть несколько "цепочек компиляций".

Классы, помещённые в пакеты

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

Пакеты имеют имена (из этого правила имеется исключение, речь о котором пойдёт ниже). Имя подпакета — это обычный идентификатор языка Java. Часто также говорят о полном имени пакета. Если пакет не является подпакетом, то его полное имя совпадает с его именем. Полное имя подпакета представляет собой полное имя пакета, содержащего данный подпакет, за которым следует точка и имя этого подпакета.

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

Полное имя класса состоит из полного имени пакета, в котором этот класс содержится, за которым следует точка и имя класса. Например, если файл Alpha.java имеет следующую структуру:

package pack1.pack2.pack3;
class Alpha
{
... ... ...
}

то в этом случае полное имя класса Alpha будет pack1.pack2.pack3.Alpha.

Если файл с исходным кодом не содержит инструкции package, то классы, в нём описанные, считаются вложенными в особый пакет, не имеющий имени. Имена таких классов совпадают с их полными именами.

Классы, описанные в одном пакете, доступны из классов других пакетов только в случае, если они отмечены как общедоступные (публичные) посредством ключевого слова public, которое записывается перед именем класса. Остальные классы считаются доступными на уровне пакета, т. е. к ним можно обращаться только из классов, входящих в тот же пакет.

В каждом файле с исходным кодом может содержаться не более одного общедоступного класса, причём, если такой класс имеется, то имя файла (без расширения) должно совпадать с именем этого класса. Если же все классы в файле доступны на уровне пакета, то имя файла должно совпадать с именем любого из этих классов. Заметим, что классы из одного пакета могут описываться в нескольких различных файлах с исходным кодом.

Запуск на выполнение класса, вложенного в именованный пакет, отличается от рассмотренного ранее запуска класса из безымянного пакета. В первом случае требуется выполнение ряда условий, которые рассмотрены ниже.

Во-первых, если класс вложен в пакет, то скомпилированный код класса должен находиться в каталоге, имеющим то же самое имя, что и пакет. Если этот пакет вложен в некоторый другой пакет, то каталог, одноимённый с внутренним пакетом, нужно поместить в каталог, одноимённый с внешним пакетом и т. д., т. е. иерархии пакетов, вложенных друг в друга, должна соответствовать иерархия одноимённых каталогов, также вложенных друг в друга.

Например, чтобы можно было запустить на выполнение класс Alpha (см. приведённую выше структуру файла Alpha.java), файл с байтовым кодом этого класса нужно поместить в каталог pack3, вложенный в каталог pack2, который, в свою очередь, должен располагаться в каталоге pack1. Другими словами, полное имя файла Alpha.class должно быть таким:

...pack1\pack2\pack3\Alpha.class

Во-вторых, запускать класс на выполнение с помощью команды java нужно из корня директории, в которой находится каталог, соответствующий самому внешнему пакету (применение ключа -classpath, который будет рассмотрен в дальнейшем, позволяет обходить это требование). В нашем примере речь идёт о директории, в которой находится каталог pack1.

В-третьих, при запуске файла на выполнение нужно указывать полное имя класса. В нашем примере для выполнения класса Alpha (мы предполагаем, что он содержит метод main()) нужно воспользоваться следующей командой:

java pack1.pack2.pack3.Alpha

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

Пусть, например, в каталоге MyProject находится файл Alpha.java, содержащий код единственного класса Alpha. Если в этом коде используется класс pack.Beta, то при компиляции командной

javac Alpha.java

файл Beta.class (или, в случае его отсутствия, Beta.java), будет искаться не в каталоге MyProject, а в каталоге MyProject\pack.

Можно при компиляции файлов с кодом сделать так, чтобы необходимая структура каталогов была создана автоматически. Для этого нужно использовать ключ -d. Ключ передаётся компилятору, после чего следует имя директории, в которую нужно поместить каталог, соответствующий самому внешнему пакету. Затем указывается сам компилируемый файл. Рассмотрим пример.

Пусть в директории MyProject имеется каталог source, в котором размещён файл Alpha.java, имеющий такую структуру:

package pack1.pack2.pack3;
class Alpha
{
... ... ...
}

Предположим также, что в MyProject имеется пустой каталог classes, в который мы хотим поместить нашу программу после компиляции (вместе со структурой каталогов, соответствующей иерархии пакетов). Для решения нашей задачи нам достаточно выполнить следующую команду из директории MyProject:

javac -d ./classes source/Alpha.java

Здесь мы указываем компилятору относительный путь к каталогу, в котором хотим разместить скомпилированную программу (символом “.” обозначается текущая директория) и путь к файлу с кодом относительно текущей директории вместе с именем самого файла. После выполнения команды в директории classes будет создана иерархия каталогов

pack1\pack2\pack3

а в каталог pack3 будет помещён файл Alpha.class.

Команду для запуска класса Alpha мы уже приводили:

java pack1.pack2.pack3.Alpha

Обратите внимание на то, что эта команда должна быть запущена из директории, в которой содержится иерархия каталогов, соответствующих пакетам, т. е. classes.

Компиляция класса, вложенного в иерархическую структуру пакетов

Компиляция класса, вложенного в иерархическую структуру пакетов (щёлкните для увеличения)

Заметим, что мы могли бы поместить программу и в текущий каталог, если бы при компиляции передали ключу в качестве параметра точку:

javac -d . source/Alpha.java

Использование ключа -d может пригодиться при компиляции классов, принадлежащих одному именованному пакету, но распределённых по разным файлам. Пусть, например, в каталоге MyProject расположены файлы Alpha.java и Beta.java, имеющие следующую структуру:

Файл Alpha.java:

package pack0;
class Alpha
{
... ... ...
    Beta b;
... ... ...
}

Файл Beta.java:

package pack0;
class Beta
{
... ... ...
//Класс pack0.Alpha не используется
... ... ...
}

Мы можем без проблем скомпилировать файл Beta.java командой

javac Beta.java

запущенной из каталога MyProject. А вот аналогичной командой

javac Alpha.java

скомпилировать файл Alpha.java не удастся. Это объясняется тем, что, поскольку в классе Alpha используется класс Beta, компилятор будет искать файл Beta.class (или, в случае его отсутствия, Beta.java), причём поиск будет происходить в несуществующем каталоге MyProject\pack0.

Проблема решается с помощью ключа -d. Сначала компилируем Beta.java:

javac -d Beta.java

а затем — Alpha.java:

javac -d Alpha.java

В результате будет создан каталог MyProject\pack0, в который будут помещены файлы Alpha.class и Beta.class. Точно такого же эффекта можно достичь, воспользовавшись единственной командой

javac -d *.java

Для чего нужны пакеты

Поддержка пакетов в Java имеет несколько положительных сторон.

Во-первых, пакеты помогают избегать конфликтов имён. Очень часто, особенно в крупных проектах, программист вынужден использовать классы, созданные другими разработчиками. Понятно, что может случиться такая ситуация, когда необходимо использовать классы, имеющие одинаковые названия. В отсутствии пакетов, компилятор не смог бы в этом случае определить, к какому именно классу происходит обращения, поэтому код не скомпилировался бы.

Если же классы с одинаковыми названиями помещены в разные пакеты, то, при указании полных имён, конфликтов не возникает. Даже в рамках одной библиотеки может понадобиться создать классы с одинаковыми именами. Если поместить эти классы в разные пакеты, то проблем не возникнет.

Во-вторых, применение пакетов позволяет хорошо структурировать библиотеку классов. Классы, инкапсулирующие различные составные части некоторой единой области, могут быть помещены в один пакет, пакеты, объединённые по аналогичному принципу, также могут быть помещены во внешний пакет и т. д. Хорошая структурированность библиотеки улучшает удобство её использования, документирования и сопровождения.

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

Импорт классов

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

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

import полное_имя_пакета.*;

Например, если мы хотим обращаться по имени ко всем общедоступным классам , находящимся в пакете pack1.pack2.pack3 (т. е. “импортировать“ эти классы), то должны использовать инструкцию

import pack1.pack2.pack3.*;

Заметим, что импорт классов, вложенных в некоторый пакет, не означает импорта классов из подпакета, который также вложен в данный пакет. Например, инструкция

import pack1.*;

не приведёт к импорту классов из пакетов pack1.pack2 и pack1.pack2.pack3. Для импорта классов из этих двух пакетов необходимо использовать две отдельные инструкции import. Кроме того, если в пакете pack1 отсутствуют классы и интерфейсы, а имеются лишь подпакеты, то такой пакет будет считаться несуществующим и код, содержащий приведённую выше инструкцию, не скомпилируется.

Можно импортировать не все общедоступные классы пакета, а лишь отдельные классы. Для этого нужно использовать инструкцию в следующем формате:

import Полное_имя_класса;

Например, импорт общедоступного класса Alpha из пакета pack1.pack2.pack3 выглядит так:

import pack1.pack2.pack3.Alpha;

В одном файле может содержаться несколько инструкций import. Все они должны располагаться до описаний классов и интерфейсов, но после инструкции package (в случае, если таковая имеется).

Зададимся теперь вопросом: а что будет, если импортированы классы двух пакетов, в которых имеются общедоступные классы с одинаковыми именами? Ответ на этот вопрос достаточно прост. Если к этим классам мы не обращаемся в коде, то никаких проблем не возникнет. Но стоит нам обратиться к одному из них, так сразу же проблемы появятся. Компилятор не сможет определить, к какому именно классу мы обращаемся, поэтому он попросту откажется компилировать код и выдаст соответствующее сообщение о неоднозначности.

Стандартная библиотека

В распоряжении разработчика на языке Java имеется огромная библиотека классов и интерфейсов, являющейся частью платформы Java. Эта библиотека называется стандартной библиотекой или библиотекой Java API (application programming interface — интерфейс прикладного программирования). Классы и интерфейсы, входящие в библиотеку, распределены по пакетам.

Очень часто инструкция import используется для импорта классов, входящих в стандартную библиотеку. Среди всех классов стандартной библиотеки наиболее широко применяются классы, входящие в пакет java.lang. По этой причине все классы этого пакета считаются неявно импортированными в любую программу на языке Java. Это означает, что из программы к ним можно всегда обращаться непосредственно по имени, даже если пакет java.lang не импортируется явно посредством инструкции import. Рассмотрим пример.

Файл Alpha.java:

class Alpha
{
... ... ...
    Integer i;    //Здесь происходит обращение к классу java.lang.Integer
... ... ...
}

Представим теперь ситуацию, что в некотором пакете используется класс, имя которого совпадает с классом из пакета java.lang. Как известно, из других классов этого пакета к данному классу можно обращаться по имени. Поэтому, казалось бы, при таком обращении должна возникнуть неоднозначность. Однако она не возникнет, поскольку это обращение будет считаться обращением к классу из первого пакета, а не из пакета java.lang. Вот пример:

Файл Integer.java:

package pack;
class Integer
{
... ... ...
... ... ...
}

Файл Alpha.java:

package pack;
class Alpha
{
... ... ...
    Integer i;    //А вот теперь здесь происходит обращение
... ... ...       //к классу pack.Integer
}

В то же самое время это правило не распространяется на классы, импортируемые явно (в том числе, и классы из пакета java.lang). Предположим, что в некоторый файл посредством инструкции import импортирован некоторый класс, например, SomeClass. Если в этом же файле описать одноимённый с ним класс, то такой файл компилироваться не будет.

Но если этот класс описать в другом файле, то первый файл успешно скомпилируется (в случае отсутствия в нём ошибок), даже если оба файла содержат код классов одного и того же пакета. Если, при этом, в первом файле будет происходить обращение к SomeClass, то оно будет считаться обращением к классу импортированному в первый файл, а не к одноимённому классу из второго файла.

Вот примеры, иллюстрирующие сказанное в предыдущих двух абзацах. Первый пример:

//Этот файл скомпилирован не будет!
import java.lang.Integer;
class Integer
{
... ... ...
... ... ...
}

А вот второй пример:

Файл Integer.java:

package pack0;
class Integer
{
... ... ...
... ... ...
}

Файл Alpha.java:

package pack0;
import java.lang.Integer;
class Alpha
{
... ... ...
    Integer i;    //Здесь происходит обращение к классу java.lang.Integer
... ... ...
}

Установка путей к классам

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

По умолчанию компилятор ищет файлы, переданные ему для компиляции, в том каталоге, откуда он вызван. Если в этих файлах имеются ссылки на классы, в данных файлах не описанные, то компилятор пытается обнаружить их байтовый код или, хотя бы, исходный. Этот поиск также будет происходить в том каталоге, откуда запущен компилятор, однако файлы будут искаться с учётом иерархии каталогов, которая должна соответствовать иерархии пакетов, в которые вложены файлы.

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

Предположим теперь, что необходимый код классов в текущем каталоге не обнаружен. Тогда компилятор предполагает, что упомянутые в компилируемых файлах классы входят в стандартную библиотеку Java. Для поиска этих классов компилятор обращается к каталогу, в который установлена JRE (Java runtime environment — среда исполнения программ на языке Java), например, к каталогу C:\Program Files\Java\jdk1.7.0_06\jre.

Байтовый код классов будет искаться в jar-архивах, содержащихся в директориях lib и lib\ext (здесь указаны относительные пути относительно каталога, в котором размещена JRE). Именно в этих директориях размещены скомпилированные классы стандартной библиотеки Java.

Кстати, исходный код стандартной библиотеки также доступен. Он хранится в архивном файле src.zip, расположенном в том каталоге, в котором установлен JDK (Java development kit – комплект для разработки на Java). Например, этот каталог может иметь путь C:\Program Files\Java\jdk1.7.0_06.

Часто при компиляции файлов бывает неудобным хранить каталоги со всеми откомпилированными классами, на которые имеются ссылки из этих файлов, в той же директории, в которой расположены компилируемые файлы. В этом случае можно передать компилятору ключ –classpath, после которого перечислить разделённые символами ";" пути к каталогам, в которых будут искаться файлы.

Пусть, например, в файле Example.java имеется следующий код:

package example;
import message.Message;
public class Example
{
    public static void main(String[] args)
    {
        Message.show("Привет!");
    }
}

Пусть также существует файл Message.java следующего содержания:

package message;
public class Message
{
    public static void show(String s)
    {
        System.out.println(s);
    }
}

Поясним: из метода main() класса example.Example вызывается метод show() класса message.Message, которому передаётся в качестве параметра строка "Привет!". Метод show() просто выводит эту строку на консоль.

Создадим каталог Project и подкаталоги Project\lib, Project\lib\message, Project\source и Project\classes. Поместим файлы Example.java и Message.java в каталоги source и message соответственно.

Как мы видим, из в классе example.Example используется класс message.Message, поэтому скомпилируем вначале файл Message.java (предполагается, что эта и все остальные команды, рассматриваемые в этом разделе вызываются из директории Project):

javac lib/message/Message.java

В результате выполнения команды в каталоге message появится файл Message.class. Теперь скомпилируем Example.java, передав компилятору с помощью ключа ‑classpath директорию lib, в которой нужно искать классы, используемые в файле Example.java (в нашем случае — класс Message). С помощью ключа –d укажем компилятору место, в которое нужно поместить скомпилированный класс — директорию classes. Команда будет выглядеть так:

javac -classpath ./lib -d ./classes source/Example.java

В результате выполнения команды в директории classes появится каталог example, а в нём — файл Example.class.

Информацию о том, где искать необходимые для выполнения программы классы, нужно передавать также и интерпретатору. Это осуществляется также с помощью ключа ‑classpath.

Например, чтобы выполнить класс Example из директории Project, нужно указать интерпретатору местонахождение файлов Example.class и Message.class (вместе с директориями, соответствующими пакетам). А именно, нужно указать каталги lib и classes. Команда запуска класса Example будет выглядеть так:

java -classpath ./lib;./classes example.Example

В результате выполнения команды на консоль будет выведено:

Привет!

Компиляция двух файлов по-отдельности

Компиляция двух файлов по-отдельности (щёлкните для увеличения)

Мы компилировали каждый из файлов Message.java и Example.java отдельно. Однако можно было скомпилировать сразу оба файла. Для этого достаточно выполнить только вторую из двух приведённых выше команд:

javac -classpath ./lib -d ./classes source/Example.java

В этом случае, при компиляции Example.java, компилятор пытается найти файл Message.class, поскольку в Example.java используется класс Message. Но файл с байтовым кодом отсутствует, зато имеется исходный код, который и будет скомпилирован. Поскольку имеется ключ –d с параметром ./classes, все скомпилированные файлы будут помещены в каталог classes. Таким образом, будут созданы каталоги classes\example и classes\message, с находящимися в них файлами Example.class и Message.class соответственно.

Теперь для запуска программы из директории Project достаточно указать лишь один каталог для поиска файлов:

java -classpath ./classes example.Example

Компиляция двух файлов одной командой

Компиляция двух файлов одной командой (щёлкните для увеличения)

Обратите внимание на то, что при использовании ключа –classpath при вызове как компилятора, так и интерпретатора, в текущей директории поиск файлов с байтовым кодом, включая байтовый код запускаемого файла (при вызове компилятора), производиться уже не будет (если только текущая директория не прописана в параметре, передаваемом ключу ‑classpath).

Имеется ещё один вариант указания путей к классам — установка переменной окружения CLASSPATH.

Для установки необходимо вызвать окно “Переменные среды” (в Window 7: “Пуск” → “Панель управления” → “Система” → “Дополнительные параметры системы” → “Переменные среды”). В нижней части окна расположена панель “Системные переменные”. Если в списке переменных CLASSPATH уже имеется, достаточно щёлкнуть по её имени в списке и отредактировать текстовое поле “Значение переменной” появившегося окна “Изменение системной переменной”.

Если переменная CLASSPATH в списке отсутствует, то её необходимо создать, щёлкнув по кнопке “Создать” нижней панели и введя имя и значение переменной в соответствующие текстовые поля появившегося окна “Новая системная переменная”.

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

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

В качестве компромиссного варианта можно устанавливать значение CLASSPATH из командной строки с помощью команды set, например так

set CLASSPATH=C:\Project;C:\Project\lib

Можно использовать и относительные пути, заменив полное имя текущей директории точкой. Например, если текущей является директория Project, то предыдущая команда эквивалентна следующей:

set CLASSPATH=.;.\lib

Установка значения переменной CLASSPATH будет действительной только в данном сеансе работы командной оболочки.

Коротко об интерфейсах

В этой статье несколько раз использовался термин “Интерфейс”. Не будем раскрывать его смысл полностью, скажем лишь, что интерфейс, подобно классу, можно рассматривать как контейнер, содержащий некоторые программные элементы, а именно, заголовки методов и константы. Для нас важно, что интерфейсы, в отличие от классов, не могут содержать тел никаких методов, в том числе и метода main(). Это означает, что интерфейс невозможно запустить на выполнение.

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

Выводы

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

Если говорить короче, то программа (в обоих смыслах) — это набор пакетов, содержащих классы и интерфейсы.

Заодно мы узнали много полезных вещей, тесно связанных с главной темой, вынесенной в заголовок статьи. Мы научились компилировать программу, запускать её на выполнение, помещать классы в пакеты, импортировать классы, автоматически создавать нужную структуру каталогов, содержащих байтовый код, указывать компилятору и интерпретатору пути поиска классов и интерфейсов, поговорили о стандартной библиотеке Java и о том, где хранятся её файлы с байтовым кодом. Надеюсь, что из этой статьи вы узнали для себя что-то новое.

Спасибо за внимание!