В стандартную библиотеку 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.3\MyProgs\ch3 есть пустой подкаталог 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. Компилируем второй файл, указывая путь p2\Inp2.java.
7. Запускаем программу java p2.Inp2.
Вместо шагов 2 и 3 можно просто создать три class-файла в любом месте, а потом перенести их в каталог p1. В class-файлах не хранится никакая информация о путях к файлам.
Смысл действий 5 и 6 в том, что при компиляции файла Inp2java компилятор уже должен знать класс p1.Base, а отыскивает он файл с этим классом по пути p1\Base.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 введены перечисления? Нельзя ли обойтись интерфейсами?