Design patterns
В математике давно выработаны общие методы решения типовых задач. Доказательство теоремы начинается со слов: "Проведем доказательство от противного" или: "Докажем это методом математической индукции", и вы сразу представляете себе схему доказательства, его путь становится вам понятен.
Нет ли подобных общих методов в программировании? Есть.
Допустим, вам поручили автоматизировать метеорологическую станцию. Информация от различных датчиков или, другими словами,
контроллеров
температуры, давления, влажности, скорости ветра поступает в цифровом виде в компьютер. Там она обрабатывается: вычисляются усредненные значения по регионам, на основе многодневных наблюдений делается прогноз на завтра, т. е. создается
модель
метеорологической картины местности. Затем прогноз выводится по разным каналам: на экран монитора, самописец, передается по сети. Он представляется в разных
видах,
колонках чисел, графиках, диаграммах.
Естественно спроектировать такую автоматизированную систему из трех частей.
Первая часть, назовем ее
Контроллером
(controller), принимает сведения от датчиков и преобразует их в какую-то единообразную форму, пригодную для дальнейшей обработки, например, приводит к одному масштабу. При этом для каждого датчика надо написать свой модуль, на вход которого поступают сигналы конкретного устройства, а на выходе образуется унифицированная информация.
Вторая часть, назовем ее
Моделью
(model), принимает эту унифицированную информацию от Контроллера, ничего не зная о датчике и не интересуясь тем, от какого именно датчика она поступила, и преобразует ее по своим алгоритмам опять-таки к какому-то однообразному виду, например, к последовательности чисел.
Третья часть системы,
Вид
(view), непосредственно связана с устройствами вывода и преобразует поступившую от Модели последовательность чисел в график, диаграмму или пакет для отправки по сети. Для каждого устройства придется написать свой модуль, учитывающий особенности именно этого устройства.
В чем удобство такой трехзвенной схемы? Она очень гибка. Замена одного датчика приведет к замене только одного модуля в Контроллере, ни Модель, ни Вид этого даже не заметят. Надо представить прогноз в каком-то новом виде, например, для телевидения? Пожалуйста, достаточно написать один модуль и вставить его в Вид. Изменился алгоритм обработки данных? Меняем Модель.
Эта схема разработана еще в 80-х годах прошлого столетия [То есть
XX
века. —
Ред.
] в языке Smalltalk и получила название MVG (Model-View-Controller). Оказалось, что она применима во многих областях, далеких от метеорологии, всюду, где удобно отделить обработку от ввода и вывода информации.
Сбор информации часто организуется так. На экране дисплея открывается поле ввода, в которое вы набиваете сведения, допустим, фамилии в произвольном порядке, а в соседнем поле вывода отображается обработанная информация, например, список фамилий по алфавиту. Будьте уверены, что эта программа организована по схеме МУС. Контроллером служит поле ввода, Видом — поле вывода, а Моделью — метод сортировки фамилий. В третьей части книги мы рассмотрим примеры реализации этой схемы.
К середине 90-х годов накопилось много подобных схем. В них сконцентрирован многолетний опыт тысяч программистов, выражены наилучшие решения типовых задач.
Вот, пожалуй, самая простая из этих схем. Надо написать класс, у которого можно создать только один экземпляр, но этим экземпляром должны пользоваться объекты других классов. Для решения этой задачи предложена схема Singleton, представленная в листинге 3.5.
Листинг 3.5.
Схема Singleton
final class Singleton{
private static Singleton s = new Singleton(0);
private int k;
private Singleton(int i){k = i;}
public static Singleton getReference()(return s;}
public int getValue(){return k;}
public void setValue(int i){k = i;}
}
public class SingletonTest {
public static void main(String[] args){
Singleton ref = Singleton.getReference();
System.out.println(ref.getValue());
ref.setValue(ref.getValue() + 5);
System.out.println(ref.getValue());
}
}
Класс
singleton
окончательный — его нельзя расширить. Его конструктор закрытый — никакой метод не может создать экземпляр этого класса. Единственный экземпляр
s
класса
singleton
— статический, он создается внутри класса. Зато любой объект может получить ссылку на экземпляр методом
getReference ()
, Изменить состояние экземпляра s методом s
etValue()
или просмотреть его текущее состояние методом
getValue()
.
Это только схема — класс
singleton
надо еще наполнить полезным содержимым, но идея выражена ясно и полностью.
Схемы проектирования были систематизированы и изложены в книге [7]. Четыре автора этой книги были прозваны "бандой четырех" (Gang of Four), а книга, коротко, "GoF". Схемы обработки информации получили название "Design Patterns". Русский термин еще не устоялся. Говорят о "шаблонах", "схемах разработки", "шаблонах проектирования".
В книге GoF описаны 23 шаблона, разбитые на три группы:
1. Шаблоны создания объектов: Factory, Abstract Factory, Singleton, Builder, Prototype.
2. Шаблоны структуры объектов: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy.
3. Шаблоны поведения объектов: Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template, Visitor.
Описания даны, в основном, на языке C++. В книге [8] те же шаблоны представлены на языке Java. Той же теме посвящено электронное издание [9]. В книге [10] подробно обсуждаются вопросы разработки систем на основе design patterns.
Мы, к сожалению, не можем разобрать подробно design patterns в этой кни-те. Но каждый программист начала XXI века должен их знать. Описание многих разработок начинается словами: "Проект решен на основе шаблона", и структура проекта сразу становится ясна для всякого, знакомого с design patterns.
По ходу книги мы будем указывать, на основе какого шаблона сделана та или иная разработка.
Импорт классов и пакетов
Внимательный читатель заметил во второй строке листинга 3.2 новый оператор
import
. Для чего он нужен?
Дело в том, что компилятор будет искать классы только в -одном пакете, именно, в том, что указан в первой строке файла. Для классов из другого пакета надо указывать полные имена. В нашем примере они короткие, и мы могли бы писать в листинге 3.2 вместо
Base
полное имя
p1.Base.
Но если полные имена длинные, а используются классы часто, то стучать по клавишам, набирая полные имена, становится утомительно. Вот тут-то мы и пишем операторы
import
, указывая компилятору полные имена классов.
Правила использования оператора
import
очень просты: пишется слово
import
и, через пробел, полное имя класса, завершенное точкой с запятой. Сколько классов надо указать, столько операторов
import
и пишется.
Это тоже может стать утомительным и тогда используется вторая форма оператора
import
— указывается имя пакета или подпакета, а вместо короткого имени класса ставится звездочка *. Этой записью компилятору предписывается просмотреть весь пакет. В нашем примере можно было написать
import p1.*;
Напомним, что импортировать можно только открытые классы, помеченные модификатором
public
.
Внимательный читатель и тут настороже. Мы ведь пользовались методами классов стандартной библиотеки, не указывая ее пакетов? Да, правильно.
Пакет
java.iang
просматривается всегда, его необязательно импортировать. Остальные пакеты стандартной библиотеки надо указывать в операторах
import
, либо записывать полные имена классов.
Подчеркнем, что оператор
import
вводится только для удобства программистов и слово "импортировать" не означает никаких перемещений классов.
Знатокам C/C++
Оператор
import
не эквивалентен директиве препроцессора
include
— он не подключает никакие файлы.
Интерфейсы
Вы уже заметили, что получить расширение можно только от одного класса, каждый класс в или с происходит из неполной семьи, как показано на рис. 3.4, а. Все классы происходят только от "Адама", от класса
object
. Но часто возникает необходимость породить класс о от двух классов вис, как показано на рис. 3.4, б. Это называется
множественным наследованием
(multiple inheritance). В множественном наследовании нет ничего плохого. Трудности возникают, если классы вис сами порождены от одного класса А, как показано на рис. 3.4* в. Это так называемое "ромбовидное" наследование.
Рис. 3.4.
Разные варианты наследования
В самом деле, пусть в классе А определен метод f (), к которому мы обращаемся из некоего метода класса о. Можем мы быть уверены, что метод f о выполняет то, что написано в классе А, т. е. это метод A.f о? Может, он переопределен в классах в и с? Если так, то каким вариантом мы пользуемся: B.f() или c.f()? Конечно, можно определить экземпляры классов и обращаться к методам этих экземпляров, но это совсем другой разговор.
В разных языках программирования этот вопрос решается по-разному, главным образом, уточнением имени метода ft). Но при этом всегда нарушается принцип KISS. Вокруг множественного наследования всегда много споров, есть его ярые приверженцы и столь же ярые противники. Не будем встревать в эти споры, наше дело — наилучшим образом использовать средства языка для решения своих задач.
Создатели языка Java после долгих споров и размышлений поступили радикально — запретили множественное наследование вообще. При расширении класса после слова
extends
можно написать только одно имя суперкласса. С помощью уточнения
super
можно обратиться только к членам непосредственного суперкласса.
Но что делать, если все-таки при порождении надо использовать несколько предков? Например, у нас есть общий класс автомобилей
Automobile
, от которого можно породить класс грузовиков
Truck
и класс легковых автомобилей Саг. Но вот надо описать пикап
Pickup
. Этот класс должен наследовать свойства и грузовых, и легковых автомобилей.
В таких случаях используется еще одна конструкция языка Java— интерфейс. Внимательно проанализировав ромбовидное наследование, теоретики ООП выяснили, что проблему создает только реализация методов, а не их описание.
Интерфейс
(interface), в отличие от класса, содержит только константы и заголовки методов, без их реализации.
Интерфейсы размещаются в тех же пакетах и подпакетах, что и классы, и компилируются тоже в class-файлы.
Описание интерфейса начинается со слова
interface
, перед которым может стоять модификатор
public
, означающий, как и для класса, что интерфейс доступен всюду. Если же модификатора
public
нет, интерфейс будет виден только в своем пакете.
После слова
interface
записывается имя интерфейса, .потом может ;стоять слово
extends
и список интерфейсов-предков через запятую. Таким образом, интерфейсы могут порождаться от интерфейсов, образуя свою, независимую от классов, иерархию, причем в ней допускается множественное наследование интерфейсов. В этой иерархии нет корня, общего предка.
Затем, в фигурных скобках, записываются в любом порядке константы и заголовки методов. Можно сказать, что в интерфейсе все методы абстрактные, но слово
abstract
писать не надо. Константы всегда статические, но слова
static
и
final
указывать не нужно.
Все константы и методы в интерфейсах всегда открыты, не надо даже .указывать модификатор
public
.
Вот какую схему можно предложить для иерархии автомобилей:
interface Automobile{ . . . }
interface Car extends Automobile{ . . . }
interface Truck extends Automobile{ . . . }
interface Pickup extends Car, Truck{ . . . }
Таким образом, интерфейс — это только набросок, эскиз. В нем указано, что делать, но не указано, как это делать.
Как же использовать интерфейс, если он полностью абстрактен, в нем нет ни одного полного метода?
Использовать нужно не интерфейс, а его
реализацию
(implementation). Реализация интерфейса — это класс, в котором расписываются методы одного или нескольких интерфейсов. В заголовке класса после его имени или после имени его суперкласса, если он есть, записывается слово
implements
и, через запятую, перечисляются имена интерфейсов.
Вот как можно реализовать иерархию автомобилей:
interface Automobile{ . . . }
interface Car extends Automobile! . . . }
class Truck implements Automobile! . . . }
class Pickup extends Truck implements Car{ . . . }
или так:
interface Automobile{ . . . }
interface Car extends Automobile{ . . . }
interface Truck extends Automobile{ . . . }
class Pickup implements Car, Truck{ . . . }
Реализация интерфейса может быть неполной, некоторые методы интерфейса расписаны, а другие — нет. Такая реализация — абстрактный класс, его обязательно надо пометить модификатором
abstract
.
Как реализовать в классе
pickup
метод
f()
, описанный и в интерфейсе саг, и в интерфейсе
Truck
с одинаковой сигнатурой? Ответ простой — никак. Такую ситуацию нельзя реализовать в классе
Pickup
. Программу надо спроектировать по-другому.
Итак, интерфейсы позволяют реализовать средствами Java чистое объектно-ориентированное проектирование, не отвлекаясь на вопросы реализации проекта.
Мы можем, приступая к разработке проекта, записать его в виде иерархии интерфейсов, не думая о реализации, а затем построить по этому проекту иерархию классов, учитывая ограничения одиночного наследования и видимости членов классов.
Интересно то, что мы можем создавать ссылки на интерфейсы. Конечно, указывать такая ссылка может только на какую-нибудь реализацию интерфейса. Тем самым мы получаем еще один способ организации полиморфизма.
Листинг 3.3 показывает, как можно собрать с помощью интерфейса хор домашних животных из листинга 2.2.
Листинг 3.3.
Использование интерфейса для организации полиморфизма
interface Voice{
void voice();
}
class Dog implements Voice{
public void voice (){
System.out.println("Gav-gav!");
}
}
class Cat implements Voice{
public void voice (){
System.out.println("Miaou!");
}
}
class Cow implements Voice{
public void voice(){
System.out.println("Mu-u-u!");
}
}
public class Chorus{
public static void main(String[] args){
Voiced singer = new Voice[3];
singer[0] = new Dog();
singer[1] = new Cat();
singer[2] = new Cow();
for(int i = 0; i < singer.length; i++)
singer[i].voice();
}
}
Здесь используется интерфейс
voice
вместо абстрактного класса
Pet
, описанного в листинге 2.2.
Что же лучше использовать: абстрактный класс или интерфейс? На этот вопрос нет однозначного ответа.
Создавая абстрактный класс, вы волей-неволей погружаете его в иерархию классов, связанную условиями одиночного наследования и единым предком — классом
object
. Пользуясь интерфейсами, вы можете свободно проектировать систему, не задумываясь об этих ограничениях.
С другой стороны, в абстрактных классах можно сразу реализовать часть методов. Реализуя же интерфейсы, вы обречены на скучное переопределение всех методов.
Вы, наверное, заметили и еще одно ограничение: все реализации методов интерфейсов должны быть открытыми,
public
, поскольку при переопределении можно лишь расширять доступ, а методы интерфейсов всегда открыты.
Вообще же наличие и классов, и интерфейсов дает разработчику богатые возможности проектирования. В нашем примере, вы можете включить в хор любой класс, просто реализовав в нем интерфейс
voice
.
Наконец, можно использовать интерфейсы просто для определения констант, как показано в листинге 3.4.
Листинг 3.4.
Система управления светофором
interface Lights{
int RED = 0;
int YELLOW = 1;
int GREEN = 2;
int ERROR = -1;
}
class Timer implements Lights{
private int delay;
private static int light = RED;
Timer(int sec)(delay = 1000 * sec;}
public int shift(){
int count = (light++) % 3;
try{
switch(count){
case RED: Thread.sleep(delay); break;
case YELLOW: Thread.sleep(delay/3); break;
case GREEN: Thread.sleep(delay/2); break;
}
}catch(Exception e){return ERROR;}
return count;
}
}
class TrafficRegulator{
private static Timer t = new Timer(1);
public static void main(String[] args){
for (int k = -0; k < 10; k++)
switch(t.shift()){
case Lights.RED: System.out.println("Stop!"); break;
case Lights.YELLOW: System.out.println("Wait!"); break;
case Lights.GREEN: System.out.println("Go!"); break;
case Lights.ERROR: System.err.println("Time Error"); break;
default: System.err.println("Unknown light."); return;
}
}
}
Здесь, в интерфейсе
Lights
, определены константы, общие для всего проекта.
Класс
Timer
реализует этот интерфейс и использует константы напрямую как свои собственные. Метод
shift
о этого класса подает сигналы переключения светофору с разной задержкой в зависимости от цвета. Задержку осуществляет метод
sleep()
класса
Thread
из стандартной библиотеки, которому передается время задержки в миллисекундах. Этот метод нуждается в обработке исключений
try{} catch() {}
, о которой мы будем говорить в
главе 16.
Класс
TrafficReguiator
не реализует интерфейс
Lights
и пользуется полными именами
Lights.RED
и т.д. Это возможно потому, что константы
RED, YELLOW
и
GREEN
по умолчанию являются статическими.
Теперь нам известны все средства языка Java, позволяющие проектировать решение поставленной задачи. Заканчивая разговор о проектировании, нельзя не упомянуть о постоянно пополняемой коллекции образцов проектирования (design patterns).
Java-файлы
Теперь можно описать структуру исходного файла с текстом программы на языке Java.
В первой строке файла может быть необязательный оператор
package
.
В следующих строках могут быть необязательные операторы
import
.
Далее идут описания классов и интерфейсов.
Еще два правила.
Среди классов файла может быть только один открытый
public
-класс.
Имя файла должно совпадать с именем открытого класса, если последний существует.
Отсюда следует, что, если в проекте есть несколько открытых классов, то они должны находиться в разных файлах.
Соглашение "Code Conventions" рекомендует открытый класс, который, если он имеется в файле, нужно описывать первым.
Пакет и подпакет
Чтобы создать пакет надо просто в первой строке Java-файла с исходным кодом записать строку
package имя;
, например:
package mypack;
Тем самым создается пакет с указанным именем
mypack
и все классы, записанные в этом файле, попадут в пакет
mypack
. Повторяя эту строку в начале каждого исходного файла, включаем в пакет новые классы.
Имя подпакета уточняется именем пакета. Чтобы создать подпакет с именем, например,
subpack
, следует в первой строке исходного файла написать;
package mypack.subpack;
и все классы этого файла и всех файлов с такой же первой строкой попадут в подпакет
subpack
пакета
mypack
.
Можно создать и подпакет подпакета, написав что-нибудь вроде
package mypack.subpack.sub;
и т. д. сколько угодно раз.
Поскольку строка
package
имя;
только одна и это обязательно первая строка файла, каждый класс попадает только в один пакет или подпакет.
Компилятор Java может сам создать каталог с тем же именем mypack, a в нем подкаталог subpack, и разместить в них class-файлы с байт-кодами.
Полные имена классов А, в будут выглядеть так: mypack.A, mypack.subpack.в.
Фирма SUN рекомендует записывать имена пакетов строчными буквами, тогда они не будут совпадать с именами классов, которые, по соглашению, начинаются с прописной. Кроме того, фирма SUN советует использовать в качестве имени пакета или подпакета доменное имя своего сайта, записанное в обратном порядке, например:
com.sun.developer
До сих пор мы ни разу не создавали пакет. Куда же попадали наши файлы с откомпилированными классами?
Компилятор всегда создает для таких классов
безымянный пакет
(unnamed package), которому соответствует текущий каталог (current working directory)
файловой системы. Вот поэтому у нас class-файл всегда оказывался в том же каталоге, что и соответствующий Java-файл.
Безымянный пакет служит обычно хранилищем небольших пробных или промежуточных классов. Большие проекты лучше хранить в пакетах. Например, библиотека классов Java 2 API хранится в пакетах
java, javax, org.omg.
Пакет
Java
содержит только подпакеты
applet, awt, beans, io, lang, math, net, rmi, security, sql, text, util
и ни одного класса. Эти пакеты имеют свои подпакеты, например, пакет создания ГИП и графики
java.awt
содержит подпакеты
color, datatransfer, dnd, event, font, geometry, im,image, print.
Конечно, состав пакетов меняется от версии к версии.
Права доступа к членам класса
Пришло время подробно разобрать различные ограничения доступа к полям и методам класса.
Рассмотрим большой пример. Пусть имеется пять классов, размещенных в двух пакетах, как показано на рис. 3.1.
Рис. 3.1.
Размещение наших классов по пакетам
В файле Base.java описаны три класса:
inpi, Base
и класс
Derivedpi
, расширяющий класс вазе. Эти классы размещены в пакете pi. В классе Base определены переменные всех четырех типов доступа, а в методах
f()
классов
inp1
и
Derivedp1
сделана попытка доступа ко всем полям класса вазе. Неудачные попытки отмечены комментариями. В комментариях помещены сообщения компилятора. Листинг 3.1 показывает содержимое этого файла.
Листинг 3.1.
Файл Base.java с описанием пакета
p1
package p1;
class Inp1{
public void f () {
Base b = new Base();
// b.priv = 1; // "priv has private access in p1.Base"
b.pack = 1;
b.prot = 1;
b.publ = 1;
}
}
public class Base{
private int priv = 0;
int pack = 0;
protected int prot = 0;
public int publ = 0;
}
class Derivedpi extends Base{
public void f(Base a) {
// a.priv = 1; // "priv hds private access in pi.Base"
a.pack = 1;
a.prot = 1;
a.publ = 1;
// priv = 1; // "priv has private access in pi.Base"
pack = 1;
prot = 1;
publ = 1;
}
}
Как видно из листинга 3.1, в пакете недоступны только закрытые,
private
, поля другого класса.
В файле Inp2.java описаны два класса:
inp2
и класс
Derivedp2
, расширяющий класс
base
. Эти классы находятся в другом пакете
р2
. В этих классах тоже сделана попытка обращения к полям класса вазе. Неудачные попытки прокомментированы сообщениями компилятора. Листинг 3.2 показывает содержимое этого файла.
Напомним, что класс вазе должен быть помечен при своем описании в пакете
p1
модификатором
public
, иначе из пакета
р2
не будет видно ни одного его члена.
Листинг 3.2.
Файл Inp2.java с описанием пакета
р2
package p2;
import pl.Base;
class Inp2{
public static void main(String[] args){
Base b = new Base();
// b.priv = 1; // "priv has private access in pl.Base"
// b.pack = 1; // "pack is not public in pl.Base;
// cannot be accessed from outside package"
// b.prot = 1; //„"prot has protected access in pi.Base"
b.publ = 1;
}
}
class Derivedp2 extends Base{
public void, f (Base a){
// a.priv = 1; // "priv has private access in. p1.Base"
// a.pack = 1; // "pack, is not public in pi.Base; cannot
//be accessed from outside package"
// a.prot = 1; // "prot has protected access in p1.Base"
a.publ = 1;
// priv = 1; // "priv has private access in pi.Base"
// pack = 1; // "pack is not public in pi.Base; cannot
// be accessed from outside package"
prot = 1;
publ = 1;
super.prot = 1;
}
}
Здесь, в другом пакете, доступ ограничен в большей степени.
Из независимого класса можно обратиться только к открытым,
public
, полям класса другого пакета. Из подкласса можно обратиться еще и к защищенным,
protected
, полям, но только унаследованным непосредственно, а не через экземпляр суперкласса.
Все указанное относится не только к полям, но и к методам. Подытожим все сказанное в табл. 3.1.
Таблица 3.1.
Права доступа к полям и методам класса
Класс |
Пакет |
Пакет и подклассы |
Все классы |
|
private |
+ |
|||
"package" |
+ |
+ |
||
protected |
+ |
+ |
* |
|
public |
+ |
+ |
+ |
+ |
protected
-полям и методам из чужого пакета отмечена звездочкой.
Размещение пакетов по файлам
То обстоятельство, что class-файлы, содержащие байт-коды классов, должны быть размещены по соответствующим каталогам, накладывает свои особенности на процесс компиляции и выполнения программы.
Обратимся к тому же примеру. Пусть в каталоге D:\jdkl.3\MyProgs\ch3 есть пустой подкаталог classes и два файла — Base.java и Inp2.java, — содержимое которых показано в листингах 3.1 и 3.2. Рис. 3.2 демонстрирует структуру каталогов уже после компиляции.
Мы можем проделать всю работу вручную.
1. В каталоге classes создаем подкаталоги р! и р2.
2. Переносим файл Base.java в каталог р! и делаем р] текущим каталогом.
3. Компилируем Base.java, получая в каталоге р! три файла: Base.class, Inpl.class, Derivedpl.class.
4. Переносим файл Inp2java в каталог р2.
5. Снова делаем текущим каталог classes.
6. Компилируем второй файл, указывая путь p2\Inp2.java.
7. Запускаем программу
java p2.inp2.
Вместо шагов 2 и 3 можно просто создать три class-файла в любом месте, а потом перенести их в каталог pi. В class-файлах не хранится никакая информация о путях к файлам.
Смысл действий 5 и 6 в том, что при компиляции файла Inp2.java компилятор уже должен знать класс
p1.Base
, а отыскивает он файл с этим классом по пути p1.Base.class, начиная от текущего каталога.
Обратите внимание на то, что в последнем действии 7 надо указывать полное имя класса.
Если использовать ключи (options) командной строки компилятора, то можно выполнить всю работу быстрее.
1. Вызываем компилятор с ключом -d путь, указывая параметром путь начальный каталог для пакета:
javac -d classes Base.java
Компилятор создаст в каталоге classes подкаталог р1 и поместит туда три class-файла.
2. Вызываем компилятор с еще одним ключом -classpath
путь,
указывая параметром путь каталог classes, в котором находится подкаталог с уже откомпилированным пакетом pi:
javac -classpath classes -d classes Inp2.java
Компилятор, руководствуясь ключом -d, создаст в каталоге classes подкаталог р2 и поместит туда два class-файла, при создании которых он "заглядывал" в каталог pi, руководствуясь ключом -classpath.
3. Делаем текущим каталог classes.
4. Запускаем профамму java p2.inp2.
Рис. 3.2.
Структура каталогов
Рис. 3.3.
Протокол компиляции и запуска программы
Для "юниксоидов" все это звучит, как музыка, ну а прочим придется вспомнить MS DOS.
Конечно, если вы используете для работы не компилятор командной строки, а какое-нибудь IDE, то все эти действия будут сделаны без вашего участия.
На рис. 3.2 отображена структура каталогов после компиляции.
На рис. 3.3 показан вывод этих действий в окно
Command Prompt
и содержимое каталогов после компиляции.
и закончили первую часть книги.
Вот мы и закончили первую часть книги. Теперь вы знаете все основные конструкции языка Java, позволяющие спроектировать и реализовать проект любой сложности на основе ООП. Оставшиеся конструкции языка, не менее важные, но реже используемые, отложим до четвертой части. Вторую и третью часть книги посвятим изучению классов и методов, входящих в Core API. Это будет для вас хорошей тренировкой.
Язык Java, как и все современные языки программирования, — это не только синтаксические конструкции, но и богатая библиотека классов. Знание этих классов и умение пользоваться ими как раз и определяет программиста-практика.