Пакеты, интерфейсы и перечисления

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

Разработчики Java включили в язык дополнительную конструкцию — пакеты (packages). Все классы Java распределяются по пакетам. Кроме классов пакеты могут содержать интерфейсы и вложенные подпакеты (subpackages). Образуется древовидная структура пакетов и подпакетов.

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

Каждый пакет создает одно пространство имен (namespace). Это означает, что все имена классов, интерфейсов и подпакетов в пакете должны быть уникальны. Имена в разных пакетах могут совпадать, но это будут разные программные единицы. Таким образом, ни один класс, интерфейс или подпакет не может оказаться сразу в двух пакетах. Если надо в одном месте программы использовать два класса с одинаковыми именами из разных пакетов, то имя класса уточняется именем пакета: пакет.Класс. Такое уточненное имя называется полным именем класса (fully qualified name).

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

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

Пакетами пользуются еще и для того, чтобы добавить к уже имеющимся правам доступа к членам класса private, protected и public еще один, "пакетный" уровень доступа.

Если член класса не отмечен ни одним из модификаторов private, protected, public, то по умолчанию к нему осуществляется пакетный доступ (default access), т. е. к такому члену может обратиться любой метод любого класса из того же пакета. Пакеты ограничивают и доступ к классу целиком — если класс не помечен модификатором public, то все его члены, даже открытые, public, не будут видны из других пакетов.

Следует обратить внимание на то, что члены с пакетным доступом не видны в подпакетах данного пакета.

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

Пакет и подпакет

Чтобы создать пакет, надо просто в первой строке java-файла с исходным кодом записать строку

package имя;

например:

package mypack;

Тем самым создается пакет с указанным именем mypack и все классы, записанные в этом файле, попадут в пакет mypack. Повторяя эту строку в начале каждого исходного файла, включаем в пакет новые классы.

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

package mypack.subpack;

и все классы этого файла и всех файлов с такой же первой строкой попадут в подпакет subpack пакета mypack.

Можно создать и подпакет подпакета, написав что-нибудь вроде

package mypack.subpack.sub;

и т. д. сколько угодно раз.

Поскольку строка package имя; только одна и это обязательно первая строка файла, каждый класс попадает только в один пакет или подпакет.

Компилятор Java может сам создать каталог с тем же именем mypack, а в нем подкаталог subpack и разместить в них class-файлы с байт-кодами.

Полные имена классов A, B будут выглядеть так: mypack.A mypack.subpack.B.

Соглашение "Code Conventions" рекомендует записывать имена пакетов строчными буквами. Тогда они не будут совпадать с именами классов, которые, по соглашению, начинаются с прописной буквы. Кроме того, соглашение советует использовать в качестве имени пакета или подпакета доменное имя своего сайта, записанное в обратном порядке, например:

com.sun.developer

Это обеспечит уникальность имени пакета во всем Интернете.

До сих пор мы ни разу не создавали пакет. Куда же попадали наши файлы с откомпилированными классами?

Компилятор всегда создает для таких классов безымянный пакет (unnamed package), которому соответствует текущий каталог (current working directory) файловой системы.

Вот поэтому у нас class-файл всегда оказывался в том же каталоге, что и соответствующий исходный java-файл.

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

Например, библиотека классов Java SE 7 API хранится в пакетах java, javax, org. Пакет java содержит только подпакеты applet, awt, beans, dyn, io, lang, math, net, nio, rmi, security, sql, text, util и ни одного класса. Эти пакеты имеют свои подпакеты, например пакет создания ГИП (Графический интерфейс пользователя) и графики java.awt содержит классы, интерфейсы и подпакеты color, datatransfer, dnd, event, font, geom, im, image, print.

Конечно, количество и состав пакетов Java SE API меняется с каждой новой версией.

Права доступа к членам класса

Пришло время подробно рассмотреть различные ограничения доступа к полям и методам класса.

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

package p1; package p2; Inp1 Inp2 Base - —Derived p2 Derivedpl Рис. 3.1. Размещение наших классов по пакетам

В файле Basejava описаны три класса: Inp1, Base и класс Derivedp1, расширяющий класс Base. Эти классы размещены в пакете p1. В классе Base определены переменные всех четырех типов доступа, а в методах f () классов Inp1 и Derivedp1 сделана попытка доступа ко всем полям класса Base. Неудачные попытки отмечены комментариями. В комментариях помещены сообщения компилятора. Листинг 3.1 показывает содержимое этого файла.

Листинг 3.1. Файл Base.java с описанием пакета pi

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 Derivedp1 extends Base{ public void f(Base a){

// a.priv = 1; // "priv has private access in p1.Base"

a.pack = 1; a.prot = 1; a.publ = 1;

// priv = 1; // "priv has private access in p1.Base"

pack = 1; prot = 1; publ = 1;

}

}

Как видно из листинга 3.1, в пакете недоступны только закрытые, private, поля другого класса.

В файле Inp2java описаны два класса: Inp2 и класс Derivedp2, расширяющий класс Base. Эти классы находятся в другом пакете p2. В них тоже сделана попытка обращения к полям класса Base. Неудачные попытки прокомментированы сообщениями компилятора. Листинг 3.2 показывает содержимое этого файла.

Напомним, что класс Base должен быть помечен при своем описании в пакете p1 модификатором public, иначе из пакета p2 не будет видно ни одного его члена.

Листинг 3.2. Файл Inp2.java с описанием пакета р2

package p2; import p1.Base; class Inp2{

public static void main(String[] args){

Base b = new Base();

// b.priv = 1; // "priv has private access in p1.Base"

// b.pack = 1; // "pack is not public in p1.Base;

// cannot be accessed from outside package" // b.prot = 1; // "prot has protected access in p1.Base"

b.publ = 1;

}

}

class Derivedp2 extends Base{ public void f(Base a){

// "priv has private access in p1.Base"

// priv = 1;

// pack = 1;

prot = 1; publ = 1; super.prot = 1;

}

}

// "pack is not public in p1.Base; cannot // be accessed from outside package"

// "prot has protected access in p1.Base"

// "priv has private access in p1.Base"

// "pack is not public in p1.Base; cannot // be accessed from outside package"

Здесь, в другом пакете, доступ ограничен в большей степени.

Из независимого класса можно обратиться только к открытым, public, полям класса другого пакета. Из подкласса можно обратиться еще и к защищенным, protected, полям, но только унаследованным непосредственно, а не через экземпляр суперкласса.

Все указанное относится не только к полям, но и к методам.

Подытожим в табл. 3.1 все сказанное.

Таблица 3.1. Права доступа к полям и методам класса Класс Пакет Пакет и подклассы Все классы private + "package" + + protected + + * public + + + + * Особенность доступа к protected-полям и методам из чужого пакета отмечена звездочкой.

Размещение пакетов по файлам

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

Обратимся к уже рассмотренному примеру. Пусть в каталоге D:jdk1.3MyProgsch3 есть пустой подкаталог classes и два файла — Basejava и Inp2java, — содержимое которых показано в листингах 3.1 и 3.2. Рисунок 3.2 демонстрирует структуру каталогов уже после компиляции.

Мы можем проделать всю работу вручную.

1. В каталоге classes создаем подкаталоги р1 и p2.

2. Переносим файл Basejava в каталог р1 и делаем р1 текущим каталогом.

ch3

-classes-i- р1 —г- Base.class

Base.java

-Derivedpi .class

4np2.java

4np1 .class

T

Derivedp2.class

LP2

I—Inp2.class

Рис. 3.2. Структура каталогов

3. Компилируем Base.java, получая в каталоге р1 три файла: Base.class, Inp1.class, Derivedp1.class.

4. Переносим файл Inp2java в каталог p2.

5. Снова делаем текущим каталог classes.

6. Компилируем второй файл, указывая путь p2Inp2.java.

7. Запускаем программу java p2.Inp2.

Вместо шагов 2 и 3 можно просто создать три class-файла в любом месте, а потом перенести их в каталог p1. В class-файлах не хранится никакая информация о путях к файлам.

Смысл действий 5 и 6 в том, что при компиляции файла Inp2java компилятор уже должен знать класс p1.Base, а отыскивает он файл с этим классом по пути p1Base.class, начиная от текущего каталога.

Обратите внимание на то, что в последнем действии (7) надо указывать полное имя класса.

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

1. Вызываем компилятор с ключом -d путь, указывая параметром путь начальный каталог для пакета:

javac -d classes Base.java

Компилятор создаст в каталоге classes подкаталог p1 и поместит туда три class-файла.

2. Вызываем компилятор с еще одним ключом -classpath путь, указывая параметром путь каталог classes, в котором находится подкаталог с уже откомпилированным пакетом p1:

javac -classpath classes -d classes Inp2.java

Компилятор, руководствуясь ключом -d, создаст в каталоге classes подкаталог p2 и поместит туда два class-файла, при создании которых он "заглядывал" в каталог p1, руководствуясь ключом -classpath.

3. Делаем текущим каталог classes.

4. Запускаем программу java p2.Inp2.

Для "юниксоидов" все это звучит, как музыка, ну а прочим придется вспомнить

MS-DOS.

Конечно, если вы используете для работы не компилятор командной строки, а какой-нибудь IDE, вроде Eclipse или NetBeans, то все эти действия будут сделаны без вашего участия.

На рис. 3.3 показан вывод этих действий в окно Command Prompt и содержимое каталогов после компиляции.

Рис. 3.3. Протокол компиляции и запуска программы

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

Внимательный читатель заметил во второй строке листинга 3.2 новый оператор import. Для чего он нужен?

Дело в том, что компилятор будет искать классы только в двух пакетах: в том, что указан в первой строке файла, и в пакете стандартных классов java.lang. Для классов из другого пакета надо указывать полные имена. В нашем примере они короткие, и мы могли бы писать в листинге 3.2 вместо Base полное имя p1. Base.

Но если полные имена длинные, а используются классы часто, то стучать по клавишам, набирая полные имена, становится утомительно. Вот тут-то мы и пишем операторы import, указывая компилятору полные имена классов.

Правила использования оператора import очень просты: пишется слово import и через пробел полное имя класса, завершенное точкой с запятой. Сколько классов надо указать, столько операторов import и пишется.

Это тоже может стать утомительным и тогда используется вторая форма оператора import — указывается имя пакета или подпакета, а вместо короткого имени класса ставится звездочка *. Этой записью компилятору предписывается просмотреть весь пакет. В нашем примере можно было написать

import p1.*;

Напомним, что импортировать разрешается только открытые классы, помеченные модификатором public.

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

Пакет java.lang просматривается всегда, его необязательно импортировать. Остальные пакеты стандартной библиотеки надо указывать в операторах import, либо записывать полные имена классов.

Начиная с версии Java SE 5 в язык введена еще одна форма оператора import, предназначенная для поиска статических полей и методов класса — оператор import static. Например, можно написать оператор

import static java.lang.Math.*;

После этого все статические поля и методы класса Math можно использовать без указания имени класса. Вместо записи

double r = Math.cos(Math.PI * alpha);

как мы делали раньше, можно записать просто

double r = cos(PI * alpha);

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

Знатокам C/C++

Оператор import не эквивалентен директиве препроцессора include — он не подключает никакие файлы.

Java-файлы

Теперь можно описать структуру исходного файла с текстом программы на языке Java.

? В первой строке файла может быть необязательный оператор package.

? В следующих строках могут быть необязательные операторы import.

? Далее идут описания классов и интерфейсов.

Еще два правила.

? Среди классов файла может быть только один открытый public-класс.

? Имя файла должно совпадать с именем открытого класса, если последний существует.

Отсюда следует, что если в проекте есть несколько открытых классов, то они должны находиться в разных файлах.

Соглашение "Code Conventions" рекомендует открытый класс, если он имеется в файле, описывать первым.

Для технологии Java характерно записывать исходный текст каждого класса в отдельном файле. В конце концов, компилятор всегда создает class-файл для каждого класса.

Интерфейсы

Вы уже заметили, что сделать расширение можно только от одного класса, каждый класс в или с происходит из неполной семьи, как показано на рис. 3.4, а. Все классы происходят только от "Адама", от класса Object. Но часто возникает необходимость породить класс D от двух классов в и с, как показано на рис. 3.4, б. Это называется множественным наследованием (multiple inheritance). В множественном наследовании нет ничего плохого. Трудности возникают, если классы в и с сами порождены от одного класса а, как показано на рис. 3.4, в. Это так называемое "ромбовидное" наследование.

А В С А Л V в с D D а) б) в) Рис. 3.4. Разные варианты наследования

В самом деле, пусть в классе а определен метод f(), к которому мы обращаемся из некоторого метода класса D. Можем мы быть уверены, что метод f() выполняет то, что написано в классе а, т. е. это метод A.f()? Может, он переопределен в классах в и с? Если так, то каким вариантом мы пользуемся: B.f() или C.f() ? Конечно, допустимо определить экземпляры классов и обращаться к методам этих экземпляров, но это совсем другая ситуация.

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

Создатели языка Java после долгих споров и размышлений поступили радикально — запретили множественное наследование классов вообще. При расширении класса после слова extends можно написать только одно имя суперкласса. С помощью уточнения super можно обратиться только к членам непосредственного суперкласса.

Но что делать, если все-таки при порождении надо использовать несколько предков? Например, у нас есть общий класс автомобилей Automobile, от которого можно породить класс грузовиков Truck и класс легковых автомобилей Car. Но вот надо описать пикап 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(), описанный и в интерфейсе Car, и в интерфейсе Truck с одинаковой сигнатурой? Ответ простой — никак. Такую ситуацию нельзя реализовать в классе Pickup. Программу надо спроектировать по-другому.

Итак, интерфейсы позволяют реализовать средствами Java чистое объектно-ориентированное проектирование, не отвлекаясь на вопросы реализации проекта.

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

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

Листинг 3.3 показывает, как можно собрать с помощью интерфейса "хор" домашних животных из листинга 2.2.

Листинг 3.3. Использование интерфейса для организации полиморфизма

interface Voice{ void voice();

}

class Dog implements Voice{

@Override

public void voice(){

System.out.println("Gav-gav!");

}

}

class Cat implements Voice{

@Override

public void voice(){

System.out.println("Miaou!");

}

}

class Cow implements Voice{

@Override

public void voice(){

System.out.println("Mu-u-u!");

}

} public class Chorus{

public static void main(String[] args){

Voice[] singer = new Voice[3]; singer[0] = new Dog(); singer[1] = new Cat(); singer[2] = new Cow(); for (Voice v: singer) v.voice();

}

}

Здесь используется интерфейс Voice вместо абстрактного класса Pet, описанного в листинге 2.2.

Что же лучше использовать: абстрактный класс или интерфейс? На этот вопрос нет однозначного ответа.

Создавая абстрактный класс, вы волей-неволей погружаете его в иерархию классов, связанную условиями одиночного наследования и единым предком — классом Object. Пользуясь интерфейсами, вы можете свободно проектировать систему, не задумываясь об этих ограничениях.

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

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

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

Наконец, можно использовать интерфейсы просто для определения констант, как показано в листинге 3.4.

Листинг 3.4. Система управления светофором

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){

System.out.println("Stop!"); break;

System.out.println("Wait!"); break; System.out.println("Walk!"); break; System.err.println("Time Error"); break; System.err.println("Unknown light."); return;

for(int k = 0; k < 10; k++) switch(t.shift()){ case Lights.RED: case Lights.YELLOW: case Lights.GREEN: case Lights.ERROR: default:

}

}

Здесь, в интерфейсе Lights, определены константы, общие для всего проекта.

Класс Timer реализует этот интерфейс и использует константы напрямую как свои собственные. Метод shift() этого класса подает сигналы переключения светофору с разной задержкой в зависимости от цвета. Задержку осуществляет метод sleep () класса Thread из стандартной библиотеки, которому передается время задержки в миллисекундах. Этот метод нуждается в обработке исключений try{}catch(){}, о которой мы будем говорить в главе 21.

Класс TrafficRegulator не реализует интерфейс Lights и пользуется полными именами Lights.RED и т. д. Это возможно потому, что константы RED, YELLOW и GREEN по умолчанию являются статическими.

Перечисления

Просматривая листинг 3.4, вы, наверное, заметили, что создавать интерфейс только для записи констант не совсем удобно. Начиная с версии Java SE 5 для этой цели в язык введены перечисления (enumerations). Создавая перечисление, мы сразу же указываем константы, входящие в него. Вместо интерфейса Lights, описанного в листинге 3.4, можно воспользоваться перечислением, сделав такую запись:

enum Lights{ RED, YELLOW, GREEN, ERROR }

Как видите, запись сильно упростилась. Мы записываем только константы, не указывая их характеристики. Каков же, в таком случае, их тип? У них тип перечисления Lights.

Перечисления в языке Java образуют самостоятельные типы, что указывается словом enum в описании перечисления, но все они неявно наследуют абстрактный класс java.lang.Enum. Это наследование не надо указывать словом extends, как мы обычно делаем, определяя классы. Оно введено только для того, чтобы включить перечисления в иерархию классов Java API. Тем не менее мы можем воспользоваться методами класса Enum для получения некоторых характеристик перечисления, как показано в листинге 3.5.

Листинг 3.5. Общие свойства перечислений

enum Lights { RED, YELLOW, GREEN, ERROR }

public class EnumMethods{

public static void main(String[] args){ for (Lights light: Lights.values()){

System.out.println("Тип: " + light.getDeclaringClass());

System.out.println("4HcnoBoe значение: " + light.ordinal());

}

}

}

Обратите внимание, во-первых, на то, как задается цикл для перебора всех значений перечисления Lights. В заголовке цикла определяется переменная light типа перечисления Lights. Метод values (), имеющийся в каждом перечислении, дает ссылку на его значения. Эти значения получает последовательно, одно за другим, переменная light.

Во-вторых, посмотрите, как можно узнать тип значений перечисления. Его возвращает метод getDeclaringClass ( ) класса Enum. В случае листинга 3.5 мы получим тип Lights.

В-третьих, у каждой константы, входящей в перечисление, есть свой порядковый номер 0, 1, 2 и т. д. Его можно узнать методом ordinal ( ) класса Enum.

Перечисление — это не только собрание констант. Это полноценный класс, в котором можно определить поля, методы и конструкторы. Мы уже видели, что в каждом перечислении есть методы, унаследованные от класса Enum, например метод values (), возвращающий массив значений перечисления.

Расширим определение перечисления Lights. Для использования его в классе TrafficRegulator нам надо сделать так, чтобы числовое значение константы error было равно -1 и чтобы методом shift() можно было бы получить следующую константу. Этого можно добиться следующим определением:

enum Lights{

RED(0), YELLOW (1), GREEN(2), ERROR(-1); private int value;private int currentValue = 0;

Lights(int value){ this.value = value;} public int getValue(){ return value; }

public Lights nextLight(){

currentValue = (currentValue + 1) % 3; return Lights.values()[currentValue];

}

}

Как видите, теперь константы создаются конструктором, определяющим для каждой константы поле value. А сейчас можно применить полученное перечисление Lights для регулирования дорожного движения. Это сделано в листинге 3.6.

Листинг 3.6. Система управления светофором с перечислением

enum Lights{

RED(0), YELLOW (1), GREEN(2), ERROR(-1);

private int value;

private int currentValue = 0;

Lights(int value){ this.value = value;

}

public int getValue(){ return value; }

public Lights nextLight(){

currentValue = (currentValue + 1) % 3; return Lights.values()[currentValue];

}

}

class Timer {

private int delay;

private static Lights light = Lights.RED;

Timer(int sec){

delay = 1000 * sec;

}

public Lights shift(){

Lights count = light.nextLight(); 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 Lights.ERROR;

}

return count;

}

public class TrafficRegulator{

public static void main(String[] args){

Timer t = new Timer(1);

for (int k = 0; k < 10; k++) switch (t.shift()){

case RED: System.out.println("Stop!"); break;

case YELLOW: System.out.println("Wait!"); break; case GREEN: System.out.printlnCWalk!"); break; case ERROR: System.err.println("Time Error"); break; default: System.err.println("Unknown light."); return;

}

}

}

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

Программист может добавить в каждую константу свои поля и методы. В листинге 3.7 приведен известный из документации пример простейшего калькулятора, в котором абстрактный метод выполнения арифметической операции eval () переопределяется в зависимости от ее конкретного вида в каждой константе перечисления Operation.

Листинг 3.7. Простейший калькулятор

public enum Operation{ PLUS { double eval(double x, double y){ return x + y; }}, MINUS { double eval(double x, double y){ return x - y; }}, TIMES { double eval(double x, double y){ return x * y; }}, DIVIDE { double eval(double x, double y){ return x / y; }}; abstract double eval(double x, double y);

public static void main(String[] args){ double x = -23.567, y = 0.235; for (Operation op: Operation.values())

System.out.println(op.eval(x, y));

}

}

Объявление аннотаций

Аннотации, о которых уже шла речь в главе 1, объявляются интерфейсами специального вида, помеченными символом "at-sign", на жаргоне называемом "собачкой". Например, аннотация @Override, использованная нами в листинге 2.2, может быть объявлена так: public @interface Override{ }

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

public @interface MethodDescription{ int id();

String description() default "[Method]";

String date();

}

объявляет аннотацию с тремя элементами id, name и date. У элемента name есть значение по умолчанию, равное Method.

Объявление интерфейса-аннотации определяет новый тип — тип аннотации (annotation type).

Аннотация записывается в программе в тех местах, где можно записывать модификаторы. По соглашению аннотация записывается перед всеми модификаторами. Элементы аннотации записываются как пары "имя — значение" через запятую. В каждой паре имя отделяется от значения знаком равенства:

@MethodDescription( id = 123456,

description = "Calculation method", date = "04.01.2008"

)

public int someMethod(){

}

Если у аннотации только один элемент, то его лучше назвать value (), например:

public @interface Copyright{

String value();

}

потому что в этом случае можно записать значение этого элемента просто как строку в кавычках, а не как пару "имя — значение":

@ Copyright("2008 My Company") public class MyClass{

}

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

Теперь нам известны все средства языка Java, позволяющие проектировать решение поставленной задачи. Заканчивая разговор о проектировании, нельзя не упомянуть о постоянно пополняемой коллекции образцов проектирования (design patterns).

Design patterns

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

Нет ли подобных общих методов в программировании? Есть.

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

Такая информационная система очень часто проектируется по схеме MVC.

Схема проектирования MVC

Естественно спроектировать в нашей автоматизированной системе три части.

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

? Вторая часть, назовем ее Моделью (Model), принимает эту унифицированную информацию от Контроллера, ничего не зная о датчике и не интересуясь тем, от какого именно датчика она поступила, и преобразует ее по своим алгоритмам опять-таки к какому-то однообразному виду, например к последовательности чисел.

? Третья часть системы, Вид (View), непосредственно связана с устройствами вывода и преобразует поступившую от Модели последовательность чисел в таблицу чисел, график, диаграмму или пакет для отправки по сети. Для каждого устройства вывода придется написать свой модуль, учитывающий особенности именно этого устройства.

В чем удобство такой трехзвенной схемы? Она очень гибка. Замена одного датчика приведет к замене только одного модуля в Контроллере, ни Модель, ни Вид этого даже не заметят. Надо представить прогноз в каком-то новом виде, например для телевидения? Пожалуйста, достаточно написать один модуль и вставить его в Вид. Изменился алгоритм обработки данных? Меняем Модель.

Эта схема разработана еще в 80-х годах прошлого столетия в языке Smalltalk и получила название MVC (Model-View-Controller). Оказалось, что она применима во многих областях, далеких от метеорологии, всюду, где удобно отделить обработку от ввода и вывода информации.

Сбор информации часто организуется так. На экране дисплея открывается поле ввода, в которое вы набиваете сведения, допустим, фамилии в произвольном порядке, а в соседнем поле вывода отображается обработанная информация, например список фамилий по алфавиту. Будьте уверены, что эта программа организована по схеме MVC. Контроллером служит поле ввода, Видом — поле вывода, а Моделью — метод сортировки фамилий.

В объектно-ориентированном программировании каждая из трех частей схемы MVC реализуется одним или несколькими классами. Модель обладает методами setXxx(), которые использует Контроллер для передачи информации в Модель. Одна Модель может получать информацию от нескольких Контроллеров. Модель предоставляет Виду методы getXxx () и isXxx () для получения информации.

В некоторых реализациях схемы MVC Вид и Контроллер не взаимодействуют. Контроллер, реагируя на события, обращается к методам setXxx() Модели, которые меняют хранящуюся в ней информацию. Модель, изменив информацию, сообщает об этом тем Видам, которые зарегистрировались у нее. Этот способ взаимодействия Модели и Вида получил название "подписка-рассылка" (subscribe-publish). Виды подписываются у Модели, и та рассылает им сообщения о всяком изменении состояния объекта методами fireXxx (), после чего Виды забирают измененную информацию, обращаясь к методам getXxx () и isXxx () Модели.

В других реализациях Контроллер руководит взаимодействием Модели и Вида.

По схеме MVC построены компоненты графической библиотеки Swing, которые мы рассмотрим в главе 11.

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

Шаблон Singleton

Вот, пожалуй, самая простая из этих схем. Надо написать класс, у которого можно создать только один экземпляр, но этим экземпляром должны пользоваться объекты других классов. Для решения поставленной задачи предложена схема Singleton, представленная в листинге 3.8.

Листинг 3.8. Схема 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 методом setValue ( ) или просмотреть его текущее состояние методом getValue ( ).

Это только схема — класс Singleton надо еще наполнить полезным содержимым, но идея выражена ясно и полностью.

Схемы проектирования были систематизированы и изложены в [7]. Четыре автора этой книги были прозваны "бандой четырех" (Gang of Four), а книга, коротко, "GoF". Схемы обработки информации получили название "design patterns". Русский термин еще не устоялся. Говорят о "шаблонах", "схемах разработки", "шаблонах проектирования".

В книге GoF описаны 23 шаблона, разбитые на три группы:

? шаблоны создания объектов: Factory, Abstract Factory, Singleton, Builder, Prototype;

? шаблоны структуры объектов: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy;

? шаблоны поведения объектов: Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template, Visitor.

Описания даны в основном на языке C++. В книге [8] те же шаблоны представлены на языке Java. В ней описаны и дополнительные шаблоны. Той же теме посвящено электронное издание [9]. В книге [10] подробно обсуждаются вопросы разработки систем на основе design patterns.

К сожалению, нет возможности разобрать подробно design patterns в этой книге. Но каждый разработчик, программирующий на объектно-ориентированном языке, должен их знать. Описание многих разработок начинается словами: "Проект решен на основе шаблона...", и структура проекта сразу становится ясна для всякого, знакомого с design patterns.

По ходу книги мы будем указывать, на основе какого шаблона сделана та или иная разработка.

Заключение

Вот мы и закончили первую часть книги. Теперь вы знаете все основные конструкции языка Java, позволяющие спроектировать и реализовать проект любой сложности на основе ООП. Оставшиеся конструкции языка, не менее важные, но реже используемые, отложим до части IV. Части II и III книги посвятим изучению классов и методов, входящих в Core API. Это будет для вас хорошей тренировкой.

Язык Java, как и все современные языки программирования, — это не только синтаксические конструкции, но и богатая библиотека классов. Знание этих классов и умение пользоваться ими как раз и определяет программиста-практика.

Вопросы для самопроверки

1. Что такое пакет в Java?

2. Могут ли классы и интерфейсы, входящие в один пакет, располагаться в нескольких каталогах файловой системы?

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

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

5. Могут ли два экземпляра одного класса пользоваться закрытыми полями друг друга?

6. Почему метод main() должен быть открытым (public)?

7. Обеспечивает ли импорт пакета поиск классов, расположенных в его подпакетах?

8. Зачем в Java есть и абстрактные классы, и интерфейсы? Нельзя ли было обойтись одной из этих конструкций?

9. Зачем в Java введены перечисления? Нельзя ли обойтись интерфейсами?