|
|||||
RUS / ENG |
|||||
01 / Учебник по Java Thinking in Java, 2nd edition, Revision 11[ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]5: Скрытие реализацииОсновным обсуждением в объектно-ориентированном
программировании является “отделение вещей, которые меняются, от тех,
которые не меняются.” Это очень важно для библиотек. Пользователь (клиентский
программист) этой библиотеки должен полагаться на
ту часть библиотеки, которую он использует, и знать, что ему не нужно будет
снова переписывать код, как только выйдет новая версия этой библиотеки. С другой
стороны, создатель библиотеки должен иметь свободу модификаций
и расширений, и быть уверенным, что эти изменения не повлияют на работу кода
клиентского программиста. Все это может быть достигнуто с помощью соглашений. Например,
программист библиотеки должен оставлять существующие методы при модификации
класса в библиотеке, т.к. это может нарушить работу кода клиентского программиста.
Обратная ситуация гораздо сложнее. В случае с членами данных, как создатель
библиотеки узнает, какие члены данных используются клиентским программистом?
Это также верно для методов, которые являются частью класса и не используются
напрямую клиентским программистом. А что, если создатель библиотеки хочет удалить
старую реализацию и поместить новую? Изменение любого из этих методов может
нарушить работу кода клиентского программиста. Получается, что программист находится
в весьма затруднительном положении и не может ничего изменить. Для решения этой проблемы, Java предоставляет спецификаторы
доступа для того, чтобы создатель библиотеки мог сказать что доступно клиентскому
программисту, а что нет. Уровни контроля доступа от “полного”
до “минимального” определяются с помощью ключевых слов: публичный
- public, защищенный - protected, дружественный - “friendly”
(не имеет ключевого слова) и приватный
- private. Из предыдущего параграфа Вы можете посчитать, что как разработчик
библиотеки, Вы будете хранить все, что возможно как “private”, и
раскрывать только те методы, которые Вы хотите предоставить клиентскому программисту.
Это абсолютно верно, хотя это бывает трудно понимать людям, программирующим
на других языках (особенно на C), которые имеют доступ ко всему, без ограничений.
К концу этой главы Вы поймете, насколько большое значение имеет контроль доступа
в Java. Однако, концепция библиотеки компонент и контроля доступа
к ним это еще не все. Существует вопрос - как хранить вместе связанные компоненты
в модуле библиотеки. В Java это реализуется с помощью ключевого слова package
(пакет), и спецификаторы доступа действуют в зависимости от того, находится
ли класс в том же пакете или нет. Итак, в начале этой главы Вы узнаете, как
размещать компоненты библиотеки в пакетах. А затем, Вы сможете понять значение
спецификаторов доступа. package: модуль библиотеки Пакет это что Вы используете,
когда пишете ключевое слово import для подключения
целой библиотеки, такой как import java.util.*; Это включает в программу библиотеку утилит,
которая является частью стандартной поставки Java. Например, класс ArrayList
находится в java.util, и Вы можете также указать полное имя java.util.ArrayList
(которое Вы можете использовать без выражения import), либо просто написать
ArrayList (при использовании import). Если Вы хотите включить единичный класс, Вы можете указать
этот класс в выражении import import java.util.ArrayList; После этого, Вы можете использовать ArrayList
без ограничений. Однако, никакие другие классы из пакета java.util
не будут доступны. Использование импорта обусловлено необходимостью управления
“пространством имен.” Имена всех членов
класса изолированы друг от друга. Метод f( ) внутри класса A
не будет конфликтовать с методом f( ) которой
имеет такую же сигнатуру (список аргументов) в классе B. А что же насчет
имен классов? Представьте, что Вы создаете класс stack и устанавливаете
на машине, на которой уже есть класс stack, написанный кем-то другим?
С Java в интернете такое вполне может произойти, и Вы об этом можете не узнать,
т.к. классы часто загружаются автоматически в процессе запуска Java-приложения. Из-за появления возможных конфликтов важно иметь полный
контроль над пространством имен в Java, а также, иметь возможность создавать
абсолютно уникальные имена. До сих пор, большинство примеров в этой книге существовали
в единичном файле и проектировались для локального использования, без упоминания
о пакетах. (В этом случае класс располагался в “пакете по умолчанию.”)
Ради упрощения такой подход будет использоваться, где это возможно, в оставшейся
части книги. Однако, если Вы планируете создавать библиотеки и программы которые
будут дружественными для других программ на Java на той же машине, Вам нужно
будет подумать о предотвращении конфликтов с именами классов. Когда Вы создаете файл исходного текста в Java, он обычно
называется модулем компиляции (иногда модулем
трансляции). Каждый модуль компиляции должен иметь расширение .java,
и внутри него может быть расположен публичный класс, который должен иметь имя
такое же, как имя файла (учитывая регистры, но без расширения .java).
В каждом модуле компиляции может быть только один публичный
класс, в противном случае, компилятор будет недоволен. Остальные классы в этом
модуле компиляции, если они есть, скрыты от мира за пределами этого пакета,
т.к. они не публичные, и представляют классы “поддержки” для главного
публичного класса. Когда Вы компилируете файл .java Вы получаете выходной
файл с точно таким же именем и расширением .class для каждого класса
в файле .java. Таким образом, из нескольких .java файлов Вы
получаете несколько .class файлов. Если Вы работали с компилирующими
языками, то Вы, возможно, получали от компилятора выходные файлы (обычно это
“obj” файлы), которые, затем, объединялись вместе с другими файлами
такого же типа с помощью линкера (для создания исполняемого файла) либо генератора
библиотеки (для создания библиотеки). Но Java работает не так. Работающая программа
это набор .class файлов, которые могут быть собраны в пакет и запакованы
в JAR файл (с помощью Java архиватора
jar). А интерпретатор Java способен находить, загружать и интерпретировать
эти файлы[32]. Библиотека это также набор .class файлов. Каждый
файл содержит один публичный класс (Вас не заставляют иметь публичный класс,
но это типичная ситуация), так что для каждого файла есть один компонент. Если
Вы хотите чтобы все эти компоненты хранились вместе (из различных .java и
.class файлов), Вы используете ключевое слово package. Когда Вы пишите: package mypackage; в начале файла (если Вы используете выражение package,
перед ним могут быть только комментарии), этим Вы указываете, что этот модуль
компиляции является частью библиотеки с названием mypackage. Или, другими
словами, Вы говорите, что публичный класс внутри этого модуля компиляции скрыт
под именем mypackage, и если кто-то захочет использовать этот класс он
должен либо указать имя пакета, либо использовать ключевое слово import
вместе с mypackage (используя варианты, показанные ранее). Заметьте,
что в Java есть соглашение для имен пакетов, это - использование символов только
нижнего регистра, даже для внутренних слов. Например, предположим, что имя файла - MyClass.java.
Это значит, что может быть только один публичный класс в этом файле, и имя
этого класса должно быть - MyClass (включая регистры): package mypackage; public class MyClass { // . . . Теперь, если кто-то хочет использовать класс MyClass
или любой другой публичный класс из пакета mypackage, ему нужно будет
использовать ключевое слово import чтобы сделать доступными имена из
пакета mypackage. Существует также альтернатива - использование имен
с префиксами: mypackage.MyClass m = new mypackage.MyClass(); А ключевое слово import может это упростить: import mypackage.*; // . . . MyClass m = new MyClass(); Это стоит запомнить, т.к. с помощью ключевых слов package
и import, Вы можете, как разработчик библиотеки, разделять глобальное
пространство имен, и, в результате, исключить конфликт имен, не зависимо от
того, сколько людей подключаются к интернет и начинают писать классы на Java. Создание уникальных имен пакетовОбратите внимание, что пакет, на самом деле, не всегда
располагается в единичном файле, и может быть собран из большого количества
.class файлов - и, в результате, получится небольшой беспорядок. Чтобы
не допустить этого, по логике вещей, нужно поместить все .class файлы
в один пакет и в отдельный каталог, с помощью иерархии файловой структуры Вашей
операционной системы. Это один из способов - как Java решает проблему нагромождения
файлов; С другим способом Вы познакомитесь позже, когда будет описана утилита
jar. Сборка файлов пакета в отдельном каталоге решает две другие
проблемы: создания уникальных имен пакета, и поиска тех классов, которые могут
быть скрыты где угодно в структуре каталогов. Это достигается, как было описано
в Главе 2, с помощью указания пути к .class файлу в имени пакета, после
ключевого слова package. Компилятор навязывает именно такую форму, но
по соглашению, первая часть имени пакета зарезервирована - это доменное имя
создателя класса в интернет. Поскольку доменные имена в интернете гарантированно
являются уникальными, то, следуя этому соглашению, Вы гарантированно получаете
уникальные имена пакетов и никогда получите конфликта имен. (Во всяком случае,
пока Вы не отдадите это доменное имя кому-нибудь другому, кто начнет писать
классы на Java с теми же именами путей, что и у Вас.) Конечно, если у Вас нет
доменного имени, Вам придется придумать невероятную комбинацию (например, как
Ваши имя и фамилия), для создания уникального имени пакета. Но если Вы решите
опубликовывать код на Java, Вам стоит немного напрячься и получить собственное
доменное имя. Также необходимо расположение пакета в отдельном каталоге
на Вашей машине. Когда Java программа запустится и потребует загрузки .class
файла (что делается динамически, в том месте программы где создается объект
соответствующего класса, либо впервые Вы получаете доступ к статическому члену
класса), она сможет определить расположение каталога, где находятся .class
файлы. Интерпретатор Java действует следующим образом. Сначала,
он ищет переменную среды с именем CLASSPATH (она устанавливается
в операционной системе программой установки Java, либо инструментами, основанными
на Java, на Вашей машине). CLASSPATH содержит один или более каталогов, которые
используются как корневые для поиска .class файлов. Начиная с этого корневого
каталога, интерпретатор берет имя пакета и заменяет каждую точку на косую черту
для создания имени пути от корня в CLASSPATH (так, например, package foo.bar.baz
превратится в foo\bar\baz или foo/bar/baz а, может быть, что-то
другое, в зависимости от Вашей операционной системы). Затем это добавляется
к различным элементам переменной CLASSPATH. Вот как интерпретатор ищет .class
файлы, с именем класса, который Вы пытаетесь создать. (Он также производит поиск
в стандартных каталогах, относительно того, где располагается сам интерпретатор). Чтобы понять это, давайте рассмотрим мое доменное имя
- bruceeckel.com. Резервируя его - com.bruceeckel - создаем уникальное
глобальное имя для моих классов. (Имена com, edu, org, и т.д., раньше писались
с заглавными буквами в пакетах Java, однако это изменилось в Java 2, так что
сейчас имя пакета должно быть написано полностью в нижнем регистре.) Теперь
если я хочу создать библиотеку с именем simple, у меня получится следующее
имя пакета: package com.bruceeckel.simple;Теперь это имя пакета может быть использовано, как прикрытие для пространства имен у следующих двух файлов: //: com:bruceeckel:simple:Vector.java // Создание пакета. package com.bruceeckel.simple; public class Vector { public Vector() { System.out.println( "com.bruceeckel.util.Vector"); } } ///:~ Когда Вы создадите пакет, Вы обнаружите, что выражение
package должно быть первой строкой кода после комментариев в файле.
Второй файл выглядит следующим образом: //: com:bruceeckel:simple:List.java // Создание пакета. package com.bruceeckel.simple; public class List { public List() { System.out.println( "com.bruceeckel.util.List"); } } ///:~ Оба этих файла располагаются в подкаталоге на моей машине: C:\DOC\JavaT\com\bruceeckel\simple Если Вы вернетесь назад, то увидите имя пакета com.bruceeckel.simple.
А что же насчет первой части пути? Об этом заботится переменная CLASSPATH,
которая, на моей машине, содержит следующее значение: CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT Вы видите, что CLASSPATH содержит несколько альтернативных
путей поиска. Однако, при использовании JAR файлов, есть небольшая
разница. Вы должны указывать имя JAR файла в CLASSPATH, а не только путь к
нему. Так, для JAR файла grape.jar, Ваша переменная
CLASSPATH может содержать: CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar Как только переменная CLASSPATH корректно установлена,
следующий файл может располагаться в любом каталоге: //: c05:LibTest.java // Использует библиотеку. import com.bruceeckel.simple.*; public class LibTest { public static void main(String[] args) { Vector v = new Vector(); List l = new List(); } } ///:~ Когда компилятор встречает выражение import,
он начинает поиск с каталогов, указанных в CLASSPATH, там ищет подкаталог
com\bruceeckel\simple, а затем, откомпилированный файл с соответствующим именем
(Vector.class для Vector и List.class для List).
Обратите внимание, что оба класса и необходимые методы в Vector и List
должны быть публичными. Установка переменной CLASSPATH стало как бы испытанием
для новичков в Java (так было и для меня, когда я начинал), хотя JDK в Java
2 от Sun стал более умным. Вы увидите, что, после установки, даже если Вы не
установили переменную CLASSPATH, Вы сможете компилировать и запускать основные
программы на Java. Однако, для компиляции и запуска исходных кодов из этой книги
(доступных на CD ROM поставляющемся вместе с книгой, либо на www.BruceEckel.com),
Вам нужно будет сделать некоторые модификации переменной CLASSPATH (которые
описываются в пакете исходных кодов). КоллизииЧто произойдет, если две библиотеки импортируются с
помощью * и содержат одинаковые имена?
Например, предположим, что в программе есть следующие строки: import com.bruceeckel.simple.*; import java.util.*; Т.к. java.util.* также содержит класс Vector,
это приведет к потенциальной коллизии. Однако, пока Вы не пишете код, который
может вызвать коллизию, все будет в порядке, и это хорошо, т.к. в противном
случае, Вам придется очень много печатать на клавиатуре, чтобы предотвратить
возможную коллизию. Коллизия произойдет, если Вы попробуете создать
класс Vector: Vector v = new Vector(); Какой из классов Vector должен здесь участвовать?
Этого не знает ни компилятор, ни читатель. Так что, компилятор выразит недовольство
и заставит Вас быть более точным. Если Вам нужен стандартный класс Java, например,
Vector, Вы можете написать: java.util.Vector v = new java.util.Vector(); Поскольку такая форма (совместно с CLASSPATH) полностью
определяет положение этого класса Vector, нет потребности в выражении
import java.util.*, пока Вы не захотите использовать что-нибудь еще
из java.util. Библиотека инструментов пользователяС помощью этих знаний, Вы сейчас сможете создать свои
собственные библиотеки инструментов, чтобы уменьшить, либо полностью исключить
дублирование кода. Вот пример - создание псевдонима для System.out.println( ),
чтобы уменьшить объем печати. Это может стать частью пакета с названием tools: //: com:bruceeckel:tools:P.java // P.rint & P.rintln сокращения. package com.bruceeckel.tools; public class P { public static void rint(String s) { System.out.print(s); } public static void rintln(String s) { System.out.println(s); } } ///:~ Вы можете использовать эти сокращения для печати String
либо с новой строки (P.rintln( )), либо на текущей строке (P.rint( )). Как Вы можете догадаться, этот файл должен располагаться
в одном из каталогов, указанных в CLASSPATH плюс com/bruceeckel/tools.
После компиляции файл P.class может использоваться где угодно в Вашей
системе после выражения import: //: c05:ToolTest.java // Использует библиотеку инструментов. import com.bruceeckel.tools.*; public class ToolTest { public static void main(String[] args) { P.rintln("Available from now on!"); P.rintln("" + 100); // Приводит к типу String P.rintln("" + 100L); P.rintln("" + 3.14159); } } ///:~ Обратите внимание, что все объекты могут быть преобразованы
в представление String простой установкой их в выражение вместе с объектом
String; в примере выше, это делается с помощью помещения пустой строки
в начале выражения String. Однако, есть небольшое замечание. Если Вы
вызываете System.out.println(100), это работает без приведения к типу
String. Конечно, Вы можете, используя перегрузку, заставить класс P
делать то же самое (это упражнение представлено в конце этой главы). Итак, начиная с этого момента, как только у Вас появляется
новая полезная утилита, Вы вполне можете добавить ее в каталог tools.
(Либо в Ваш собственный каталог util или tools.) Использование импорта для изменения поведенияВ языке C существует условная компиляция, которая
позволяет устанавливать различное поведение Вашего кода без изменения самого
кода. В Java такой возможности нет. Причина, по которой эта функция отсутствует
в Java, возможно в том, что в языке C эта функция в основном использовалась
для создания кросс-платформенных приложений: компилировались различные куски
кода, в зависимости от платформы, на которой они работали. Поскольку Java
автоматически поддерживает кросс-платформенность, необходимости в такой функции
нет. Однако, есть и другая необходимость в условной компиляции.
Самое распространенное использование - отладочный код. Отладка включается в
процессе разработки, и отключается в конечном продукте. Аллен Холуб (Allen Holub)
(www.holub.com) предложил идею - использования пакетов, для имитации
условной компиляции. Он использовал это для создания Java-версии очень полезного
механизма контроля (assertion) из языка C, с помощью которого Вы можете
сказать “это должно быть истинно” либо “это должно быть ложно”
и, если выражение не удовлетворяет этому контролю, Вы узнаете об этом. Такой
инструмент является очень полезным во время отладки. Вот класс, который Вы можете использовать для отладки: //: com:bruceeckel:tools:debug:Assert.java // Инструмент контроля для отладки. package com.bruceeckel.tools.debug; public class Assert { private static void perr(String msg) { System.err.println(msg); } public final static void is_true(boolean exp) { if(!exp) perr("Assertion failed"); } public final static void is_false(boolean exp){ if(exp) perr("Assertion failed"); } public final static void is_true(boolean exp, String msg) { if(!exp) perr("Assertion failed: " + msg); } public final static void is_false(boolean exp, String msg) { if(exp) perr("Assertion failed: " + msg); } } ///:~ Этот класс просто инкапсулирует булевские тесты, и печатает
сообщение об ошибке, если эти тесты завершаются неудачно. В Главе 10, Вы познакомитесь
с более изощренным инструментом для борьбы с ошибками, называемым обработка
исключений, а пока метод perr( ) будет отлично работать. Результат отправляется на консоль в поток стандартных
ошибок - System.err. Когда Вам необходимо использовать этот класс, Вы добавляете
одну строку в свою программу: import com.bruceeckel.tools.debug.*; Для отключения этого контроля, Вы можете использовать
код, где реализован второй класс Assert, находящийся в другом пакете: //: com:bruceeckel:tools:Assert.java // Отключение контроля package com.bruceeckel.tools; public class Assert { public final static void is_true(boolean exp){} public final static void is_false(boolean exp){} public final static void is_true(boolean exp, String msg) {} public final static void is_false(boolean exp, String msg) {} } ///:~ Так, если Вы измените предыдущее выражение import
на: import com.bruceeckel.tools.*; программа больше не будет печатать контрольные данные.
Вот пример: //: c05:TestAssert.java // Демонстрация инструмента контроля. // Комментируете первую или вторую строчку и // получаете различные результаты: import com.bruceeckel.tools.debug.*; // import com.bruceeckel.tools.*; public class TestAssert { public static void main(String[] args) { Assert.is_true((2 + 2) == 5); Assert.is_false((1 + 1) == 2); Assert.is_true((2 + 2) == 5, "2 + 2 == 5"); Assert.is_false((1 + 1) == 2, "1 +1 != 2"); } } ///:~ Изменением импортируемого пакета, Вы производите переход
от отладочной к конечной версии. Эта техника может быть использована для создания
отладочного кода любого типа. Пакетное предостережениеНеобходимо запомнить, что когда Вы создаете пакет, Вы
косвенно задаете структуру каталогов
при задании имени пакета. Пакет должен находиться в каталоге, определенном
в имени пакета, причем этот каталог должен быть доступен по переменной CLASSPATH.
Экспериментирование с ключевым словом package может быть бесполезным
вначале, поскольку пока Вы не будете придерживаться правила: имя пакета определяет
путь к нему, Вы будете получать множество непонятных run-time сообщений, сообщающих
о невозможности найти какой-нибудь класс, даже если он находится в том же самом
каталоге. Если Вы получите подобное сообщение, попробуйте закомментировать выражение
package, и, если все заработает, то Вы знаете, в чем проблема. Спецификаторы доступа в Java Спецификаторы
доступа Java public, protected
и private располагаются перед каждым определением
каждого члена в Вашем классе, независимо от того, метод это или просто поле.
Каждый спецификатор доступа определяет доступ только для одного конкретного
определения. В этом - явное различие с языком C++, в котором спецификатор
доступа определяет доступ для всех последующих определений, пока не встретится
другой спецификатор доступа. Так или иначе, у всего имеется какой-то тип доступа. Далее
Вы узнаете все о различных типах доступа, начиная с типа доступа по умолчанию. Дружественный доступ “Friendly”А что, если Вы вообще не определяете спецификатор доступа,
как это было сделано во всех примерах до настоящей главы? Доступ по умолчанию
не имеет ключевого слова, но обычно называется дружественным - “friendly.”
Это значит, что все другие классы в том же пакете имеют доступ к дружественным
членам, но для классов за пределами этого пакета, члены являются приватными
(private). Т.к. файл модуля компиляции может принадлежать только одному
пакету, все классы внутри этого единичного модуля компиляции автоматически
являются дружественными друг другу. Таким образом, говорят, что дружественные
элементы имеют доступ на уровне
пакета. Дружественный доступ позволяет Вам объединять связанные
классы в пакете, так, что они могут легко общаться друг с другом. Когда Вы располагаете
классы вместе в одном пакете, (определив таким образом совместный доступ для
дружественных членов), Вы “владеете” кодом в этом пакете. Во многих
языках, Вам волей-неволей приходится организовывать определения в файлах, но
Java Вас заставляет создавать
их в разумной форме. К тому же, Вы, возможно, захотите исключить классы, которые
не должны иметь доступ к классам в том же пакете. Класс управляет тем, какой код имеет доступ к его членам.
И нет никакого магического способа “прорваться внутрь.” Код из другого
пакета не может появиться и сказать, “Привет, Я друг Боба!” и затем
посмотреть все защищенные, дружественные и приватные члены Боба. Единственный
путь получить доступ, это:
public: интерфейсный доступЕсли Вы используете ключевое слово public, это
значит, что объявление, следующее сразу за этим словом,
доступно всем, и, конечно, клиентскому программисту, который использует эту
библиотеку. Предположим, что Вы создаете пакет dessert, содержащий
следующий модуль компиляции: //: c05:dessert:Cookie.java // Создаем библиотеку. package c05.dessert; public class Cookie { public Cookie() { System.out.println("Cookie constructor"); } void bite() { System.out.println("bite"); } } ///:~ Запомните, Cookie.java должен располагаться в
каталоге c05\dessert (с05 означает пятую главу этой книги),
который должен быть доступен по одному из путей в CLASSPATH. Не надейтесь,
что Java всегда просматривает текущий каталог, как один из начальных каталогов
для поиска классов. Если Вы не добавите путь "." в переменную среды
CLASSPATH, Java не будет этого делать. Теперь, если Вы создадите программу, использующую Cookie: //: c05:Dinner.java // Использует библиотеку. import c05.dessert.*; public class Dinner { public Dinner() { System.out.println("Dinner constructor"); } public static void main(String[] args) { Cookie x = new Cookie(); //! x.bite(); // Недоступно } } ///:~ Вы сможете создать объект Cookie, т.к. его конструктор
и сам класс являются публичными. (Далее Вы больше узнаете о концепции публичных
классов.) Однако, метод bite( ) недоступен внутри Dinner.java
т.к. bite( ) остается дружественным только внутри пакета dessert. Пакет по умолчанию
|
|||||
НОВОСТИ | ОБО МНЕ | УСЛУГИ | РАБОТЫ | ПРОГРАММЫ | СТАТЬИ | КОНТАКТЫ |
|||||
|