Java. Полное руководство. 8-е издание. С. Н. Тригуб Перевод с английского и редакция
Скачать 25.04 Mb.
|
] Иерархия пакетов должна быть отражена в файловой системе среды разработки Java. Например, в среде Windows пакет, объявленный как package j ava. awt. image; , должен храниться в каталоге j ava\awt \image. Необходимо тщательно проверять правильность выбора имен пакетов. Имя пакета нельзя изменить, не изменяя имя каталога, в котором хранятся классы. Поиск пакетов и переменная среды c l a s s p a t Как было сказано в предыдущем разделе, пакеты соответствуют каталогам. Это обстоятельство порождает важный вопрос откуда системе времени выполнения Java известно, где следует искать создаваемые пакеты Ответ на него состоит из следующих частей во-первых, по умолчанию в качестве отправной точки система времени выполнения Java использует текущий рабочий каталог. Следовательно, если пакет находится в подкаталоге текущего каталога, он будет найден. Во- вторых, путь или пути к каталогу можно указать, устанавливая значение переменной среды CLASSPATH. В-третьих, вызови в командной сроке можно использовать с параметром -classpath, указывающим путь к классам. Например, рассмотрим следующую спецификацию пакета MyPack; Глава 9. Пакеты и интерфейсы 2 2 Чтобы программа могла найти пакет МуРаск, должно выполняться одно из следующих двух условий. Либо программа должна выполняться из каталога, расположенного непосредственно над каталогом МуРаск, либо переменная среды LASS PATH должна содержать путь к каталогу МуРаск, либо параметр -class path должен указывать путь к каталогу МуРаск вовремя запуска программы j При использовании двух последних способов путь класса не должен содержать сам пакет МуРаск. Он должен просто указывать путь к этому каталогу. Например, в среде Windows, если путь к каталогу МуРаск имеет вид С : \MyPrograms\ Java\ Y.yPack, путь класса к пакету МуРаск будет выглядеть так. С:\MyPrograms\Java Простейш ий способ проверки примеров, приведенных в этой книге, — создание каталогов пакетов в текущем каталоге разработки, помещение файлов .class в соответствующие каталоги и последующий запуск программ из каталога разработки. В следующем примере использован именно этот подход. Краткий пример пакета С учетом описанного выше можете попытаться использовать следующий простой пакет Простой пакет package МуРаск; class Balance { String name; double bal; Balance(String n, double b) { name = n; bal = b; } void show() { if(bal<0) System.out.print("--> "); System.out.println(name + $" + bal); } } class AccountBalance { public static void main(String a r g s []) { Balance current[] = new Balance[3]; current[0] = new Balance("K. J. Fielding", 123.23); current[1] = new Balance("Will Tell", 157.02); current[2] = new Balance("Tom Jackson", -12.33); for(int i=0; i<3; i++) current[i ].s h o w (Назовите этот файл AccountBalance. j ava и поместите его в каталог МуРаск. Затем выполните компиляцию файла. Убедитесь, что результирующий файл .class также помещен в каталог МуРаск. Затем попробуйте выполнить класс AccountBalance, вводя следующую командную строку, java МуРаск.AccountBalance 2 2 2 Часть I. Язык Помните, что при выполнении этой команды текущим должен быть каталог, расположенный над каталогом МуРаск, либо переменная среды CLASSPATH должна содержать соответствующий путь. Как мы уже поясняли, теперь класс AccountBalance — часть пакета МуРаск. Это означает, что его нельзя выполнять самостоятельно. То есть нельзя использовать следующую командную строку Имя AccountBalance требует уточнения именем его пакета. Защита доступа В предыдущих главах вы рассмотрели различные аспекты механизма управления доступом Java и его модификаторы. Например, вы уже знаете, что доступ к закрытому члену класса предоставляется только другим членам этого класса. Пакеты добавляют к управлению доступом еще одно измерение. Как вы вскоре убедитесь, Java предоставляет множество уровней защиты, обеспечивая очень точное управление видимостью переменных и методов внутри классов, подклассов и пакетов. Классы и пакеты одновременно служат средствами инкапсуляции и хранилищем пространства имени области видимости переменных и методов. Пакеты играют роль контейнеров классов и других подчиненных пакетов. Классы служат контейнерами данных и кода. Класс — наименьшая единица абстракции Java. Вследствие взаимодействия между классами и пакетами Java определяет четыре категории видимости членов класса. • Подклассы водном пакете. • Классы водном пакете, не являющиеся подклассами. • Подклассы в различных пакетах. • Классы, которые не находятся водном пакете и не являются подклассами. Три модификатора доступа — private, public и protected — предоставляют разнообразные способы создания множества уровней доступа, необходимых для этих категорий. Взаимосвязь между ними описана в табл. Таблица 9.1. Доступ к членам класса : / ' ' ........ . и j. P r i v a t e Модификатор отсутствует P r o t e s t e Один и тот же класс Да Да Да Да Подкласс класса этого же пакета Нет Да Да Да Класс этого же пакета, не являющийся подклассом Нет Да Да Да Подкласс класса другого пакета Нет Нет Да Да Класс другого пакета, не являющийся подклассом класса данного пакета Нет Нет Нет Да Хотя на первый взгляд механизм управления доступом Java может показаться сложным, следующие соображения могут облегчить его понимание. Любой компонент, объявленный как public, доступен из любого кода. Любой компонент, объявленный как private, невиден для компонентов, расположенных вне его класса. Если член не содержит явного модификатора доступа, он видим подклассами другим классам в данном пакете. Этот уровень доступа используется по умол Глава 9. Пакеты и интерфейсы 2 3 чанию. Если нужно, чтобы элемент был виден за пределами его текущего пакета, но только классам, которые являются непосредственными подклассами данного класса, элемент должен быть объявлен как Правила доступа, описанные в табл. 9.1, применимы только к членам класса. Для класса, не являющегося вложенным, может быть указан только один из двух возможных уровней доступа заданный по умолчанию и public. Когда класс объявлен как public, он доступен любому другому коду. Если для класса указан уровень доступа, определенный по умолчанию, он доступен только для кода внутри данного пакета. Когда класс является открытым, он должен быть единственным открытым классом, объявленным в файле, и имя файла должно совпадать с именем класса. Пример защиты доступа Следующий пример демонстрирует использование всех комбинаций модификаторов управления доступом. Он содержит два пакета и пять классов. Не забудьте, что классы двух различных пакетов должны храниться в каталогах, имена которых совпадают с именами соответствующих пакетов, — в данном случае pi и р2. Исходный файл первого пакета определяет три класса Protection, Derived и Same Package. Первый класс определяет четыре переменные типа int — по одной в каждом из допустимых режимов защиты доступа. Переменная п объявлена с уровнем защиты, используемым по умолчанию, n_pri — как private, n_pro — protected, an_pub — В этом примере все другие классы будут предпринимать попытку обращения к переменным экземпляра этого класса. Строки, компиляция которых невозможна из-за нарушений правил доступа, оформлены в виде комментариев. Перед каждой из этих строк помещен комментарий с указанием точек программы, из которых был бы возможен доступ к этому уровню защиты. Второй класс, Derived, — подкласс класса Protection этого же пакета pi. Он предоставляет классу Derived доступ ко всем переменным класса Protection, кроме переменной n_p г i , объявленной как private. Третий класс, S аше Расе, не является подклассом класса Protection, но находится в этом же пакете и обладает доступом ко всем переменным, кроме переменной Файл Protection. j ava содержит следующий код pi; public class Protection { int n = 1; private int n_pri = 2; protected int n_pro = 3; public int n_pub = 4; public Protection() конструктор базового класса = " + n ) ; System.out.println("n_pri = " + n_pri); System.out.println("n_pro = " + n_pro); System.out.println("n_pub = " + Файл Derived. j ava содержит такой код pi; class Derived extends Protection { 2 2 4 Часть I. Язык Java Derived() конструктор подкласса = " + n) ; // доступно только для класса System.out.println("n_pri = "4 + n_pri); System, out .println ("n_j?ro = " + n_j?ro) ; System.out.println("n_pub = " + Файл Same Package. j ava содержит следующий код pi; class SamePackage { SamePackage() { Protection p = new конструктор этого же пакета = " + p.n ) ; // class only // System.out.println("n_pri = " + p.n_pri); System.out.println("n_pro = " + p.n_pro); System.out.println("n_pub = " + Ниже приведен исходный код второго пакета, р. Два определенных в нем класса отражают оставшиеся две ситуации управления доступом. Первый класс, Protection2, — это подкласс класса p i . Protection. Он имеет доступ ко всем переменным класса p i . Protection, кроме переменных n_pri поскольку она объявлена как private) и п, которая объявлена с уровнем защиты, используемым по умолчанию. Вспомните, что заданный по умолчанию режим доступа разрешает доступ изданного класса или пакета, ноне из подклассов другого пакета. И наконец, класс OtherPackage имеет доступ только к одной переменной — n_pub, которая была объявлена как publ Файл Protect ion2 . j ava содержит следующий код р 2 ; class Protection2 extends p i .Protection { Protection2() унаследованный конструктор другого пакета доступно только для данного класса или пакета // System.out.println("n = " + n ) ; // доступно только для данного класса System.out.println("n_pri = " + n_pri); System.out.println("n_pro = " + n_pro); System.out.println("n_pub = " + Файл OtherPackage. j ava содержит следующий код Глава 9. Пакеты и интерфейсы 2 2 5 package р 2 ; class OtherPackage { OtherPackage() { p i .Protection р = new p i конструктор другого пакета доступно только для данного класса или пакета System.out.println("n = " + p.n) ; // доступно только для данного класса System.out.println("n_pri = " + p.n_pri); // доступно только для данного класса, подкласса или о = " + р .n_p r o ); System.out.println("n_pub = " + Для проверки работы этих двух пакетов можно использовать следующие два проверочных файла. Проверочный файл для пакета p i имеет такой вид / Демонстрационный пакет p i . package pi; '/ Конкретизация различных классов пакета pi. public class Demo { public static void main(String a r g s []) { Protection obi = new Protection(); Derived ob2 = new Derived(); SamePackage ob3 = new Следующий файл — проверочный файл пакета р Демонстрационный пакет р 2 . package р 2 ; // Конкретизация различных классов пакета р 2 . public class Demo { public static void main(String a r g s []) { Protection2 obi = new Protection2(); OtherPackage ob2 = new Импорт пакетов Если вспомнить, что пакеты предлагают эффективный механизм изоляции различных классов друг от друга, становится понятно, почему все встроенные классы Java хранятся в пакетах. Ни один из основных классов Java не хранится в неиме нованном пакете, используемом по умолчанию. Все стандартные классы хранятся в каком-либо именованном пакете. Поскольку внутри пакетов классы должны быть полностью определены именами их пакетов, длинное, разделенное точками имя пути пакета каждого используемого класса может оказаться слишком громоздким. Поэтому, чтобы определенные классы или весь пакет можно было сделать видимыми, в Java включен оператор im p o r t. После того как класс импортирован 2 2 6 Часть I. Язык на него можно ссылаться непосредственно, используя только его имя. Оператор import служит только для удобства программистов и не является обязательным с технической точки зрения для создания завершенной программы Java. Однако если в приложении придется ссылаться на несколько десятков классов, оператор import значительно уменьшит объем вводимого кода. В исходном файле программы Java операторы import должны следовать непосредственно за оператором package если таковой имеется) перед любыми определениями классов. Оператор имеет следующую общую форму пакет пакет ] .( имя_класса I В этой форме пакет ! — имя пакета верхнего уровня, пакет 2 — имя подчиненного пакета внутри внешнего пакета, отделенное символом точка ( .) . Глубина вложенности пакетов практически не ограничена ничем, кроме файловой системы. И наконец, имя класса может быть задано либо явно, либо с помощью символа звездочка (*), который указывает компилятору Java о необходимости импорта всего пакета. Следующий фрагмент демонстрирует применение обеих форм оператора java.util.Date; import j a v a . i о . * Все стандартные классы, поставляемые с системой Java, хранятся в пакете java. Основные функции языка хранятся в пакете java. lang внутри пакета j ava. Обычно каждый пакет или класс, который нужно использовать, приходится импортировать. Но поскольку система Java бесполезна без многих функций, определенных в пакете j ava. lang, компилятор неявно импортирует его для всех программ. Это эквивалентно наличию следующей строки в каждой из программ При наличии в двух различных пакетах, импортируемых с применением формы со звездочкой, классов с одинаковыми именами компилятор никак на это не отреагирует, если только не будет предпринята попытка использования одного из этих классов. В этом случае возникнет ошибка времени компиляции, и имя класса придется указать явно, задавая его пакет. Полностью определенное имя класса с указанием полной иерархии пакетов можно использовать везде, где допускается имя класса. Например, в следующем фрагменте кода присутствует оператор импорта java.util.*; class MyDate extends Date Этот же пример без оператора import выглядит следующим образом MyDate extends java.util.Date В этой версии объект Date полностью определен. Как видно в табл. 9.1, при импорте пакета в импортирующем коде классам, не являющимся подклассами классов пакета, будут доступны только те элементы пакета, которые объявлены как public. Например, если нужно, чтобы приведенный ранее класс Balance пакета МуРаск был доступен в качестве самостоятельного класса вне пакета МуРаск, его необходимо объявить как public и поместить в отдельный файл, как показано в следующем примере МуРаск; /* Теперь класс Balance, его конструктор и его метод show() Глава 9. Пакеты и интерфейсы 2 2 являются открытыми. Это означает, что вне их пакета они могут использоваться кодом, не являющимся подклассом пакета class Balance { String name; double bal; public Balance(String n, double b) { name = n ; bal = b; } public void show() { if(bal<0) System.out.print("--> "); System.out.println(name + $" + Как видите, теперь класс Balance объявлен как public. Его конструктор и метод show () также объявлены как public. Это означает, что они доступны любому коду вне пакета МуРаск. Например, класс TestBalance импортирует пакет МуРаск и поэтому может использовать класс Balance. import М у Раск.*; class TestBalance { public static void main(String a r g s []) { /* Поскольку класс Balance объявлен как public, его можно использовать и вызывать его конструктор. */ Balance test = new Balance("J. J. Jaspers", 99.88); test.show(); // можно также вызывать метод s h o w В качестве эксперимента удалите модификатор public из класса Balance, а затем попытайтесь выполнить компиляцию класса TestBalance. Как уже было сказано, это приведет к возникновению ошибок. Интерфейсы П рименение ключевого слова interface позволяет полностью абстрагировать интерфейс класса от его реализации. То есть с использованием ключевого слова interface можно задать действия, которые должен выполнять классно не то, как именно он должен это делать. Синтаксически интерфейсы аналогичны классам, ноне содержат переменных экземпляров, а объявления их методов не содержат тела метода. На практике это означает, что можно объявлять интерфейсы, которые не делают никаких допущений относительно их реализации. Как только интерфейс определен, его может реализовать любое количество классов. Кроме того, один класс может реализовать любое количество интерфейсов. Чтобы реализовать интерфейс, класс должен создать полный набор методов, определенных интерфейсом. Однако каждый класс может определять нюансы своей реализации данного интерфейса. Ключевое слово interface позволяет в полной мере использовать концепцию полиморфизма под названием один интерфейс, несколько методов 2 2 Часть I. Язык Интерфейсы предназначены для поддержки динамического разрешения методов вовремя выполнения. Обычно, чтобы вызов метода мог выполняться из одного класса в другом, оба класса должны присутствовать вовремя компиляции, дабы компилятор Java мог проверить совместимость сигнатур методов. Само по себе это требование создает статическую и нерасширяемую среду обработки классов. В такой системе функциональные возможности неизбежно передаются по иерархии классов все выше и выше, в результате чего механизмы будут становиться доступными все большему количеству подклассов. Интерфейсы предназначены для предотвращения этой проблемы. Они изолируют определение метода или набора методов от иерархии наследования. Поскольку иерархия интерфейсов не совпадает с иерархией классов, классы, никак несвязанные между собой в иерархии классов, могут реализовать один и тот же интерфейс. Именно здесь возможности интерфейсов проявляются наиболее полно. На заметку Интерфейсы добавляют большинство функциональных возможностей, требуемых многим приложениям, которым в обычных условиях в языках вроде C ++ пришлось бы прибегать к использованию множественного наследования. Определение интерфейса Во многом определение интерфейса подобно определению класса. Упрощенная общая форма интерфейса имеет следующий вид. д оступ interface имя { возвращаемый_тип имя_метода1 {список_параметров) ; возвращаемый_тип имя_метода2 (список_параметров) ; тип имя_конечной_переменной1 = значение тип имя_конечной_переменной2 = значение / . . возвращаемый тип имя_ме тодаЫ ( с пи сок параметров тип имя_конечной_переменнойЫ = значение Если определение не содержит никакого модификатора доступа, используется доступ по умолчанию и интерфейс доступен только другим членам того пакета, в котором он объявлен. Если интерфейс объявлен как p u b l i c , он может быть использован любым другим кодом. В этом случае интерфейс должен быть единственным открытым интерфейсом, объявленным в файле, и имя файла должно совпадать с именем интерфейса. Имя — имя интерфейса, которым может быть любой допустимый идентификатор. Обратите внимание на то, что объявляемые методы не содержат тел. Их объявления завершаются списком параметров, за которым следует символ точка с запятой. По сути, они представляют собой абстрактные методы. Ни один из указанных внутри интерфейса методов не может обладать никакой заданной по умолчанию реализацией. Каждый класс, который включает в себя интерфейс, должен реализовать все его методы. Переменные могут быть объявлены внутри объявлений интерфейсов. Они неявно объявляются как f i n a l и s t a t i c , те. реализующий класс не может их изменять. Кроме того, они должны быть также инициализированы. Все методы и переменные неявно объявляются как p u b l i c Ниже приведен пример определения интерфейса. В нем объявляется простой интерфейс, который содержит один метод c a l l b a c k O , принимающий единственный целочисленный параметр Callback { void callback(int param); } Глава 9. Пакеты и интерфейсы 2 2 Реализация интерфейсов Как только интерфейс определен, его может реализовать один или несколько классов. Чтобы реализовать интерфейс, в определение класса потребуется включить конструкцию implements, а затем создать методы, определенные интерфейсом. Общая форма класса, который содержит выражение implements, имеет следующий вид. д оступ class имя_класса [extends суперкласс ] [implements интерфейс [ , интерфейс { // тело_класса } Если класс реализует более одного интерфейса, имена интерфейсов разделяются запятыми. Если класс реализует два интерфейса, которые объявляют один и тот же метод, то один и тот же метод будет использоваться клиентами любого интерфейса. Методы, которые реализуют интерфейс, должны быть объявлены как public. Кроме того, сигнатура типа реализующего метода должна в точности совпадать с сигнатурой типа, указанной в определении Рассмотрим небольшой пример класса, который реализует приведенный ранее интерфейс Callback. class Client implements Callback { // Реализует интерфейс Callback public void callback(int p) Метод callback, вызванный со значением " + P) Обратите внимание на то, что метод callback () объявлен с использованием модификатора доступа pub Помните При реализации метода интерфейса он должен быть объявлен как Вполне допустима и достаточно распространена ситуация, когда классы, которые реализуют интерфейсы, определяют собственные дополнительные члены. Например, следующая версия класса Client реализует метод callback() и добавляет метод nonlnf aceMeth (). class Client implements Callback { // Реализует интерфейс Callback public void callback(int p) Метод callback, вызванный со значением " + P) ; } void nonlfaceMeth() Классы, которые реализуют интерфейсы" +могут определять также и другие члены."); } } Доступ к реализациям через ссылки на интерфейсы Переменные можно объявлять как объектные ссылки, которые используют тип интерфейса, а не тип класса. При помощи такой переменной можно ссылаться на 2 3 0 Часть I. Язык любой экземпляр любого класса, реализующего объявленный интерфейс. При вызове метода с помощью одной из таких ссылок выбор нужной версии будет производиться в зависимости от конкретного экземпляра интерфейса, на который выполняется ссылка. Это — одна из главных особенностей интерфейсов. Поиск выполняемого метода осуществляется динамически вовремя выполнения, что позволяет создавать классы позже, чем код, который вызывает методы по отношению к этим классам. Диспетчеризация кода может выполняться с использованием интерфейса без необходимости наличия каких-либо сведений о вызывающем. Этот процесс аналогичен использованию ссылки на суперкласс для доступа к объекту подкласса, описанному в главе Внимание Поскольку в системе Java динамический поиск методов вовремя выполнения сопряжен со значительными накладными расходами по сравнению с обычным вызовом методов, в коде, для которого важна производительность, интерфейсы следует использовать только тогда, когда это действительно необходимо. В следующем примере метод callback () вызывается через ссылочную переменную интерфейса Testlface { public static void main(String a r g s []) { Callback с = new Cl i e n t (); с Эта программа создает следующий вывод. Метод callback, вызванный со значением Обратите внимание на то, что хотя переменная с объявлена с типом интерфейса Callback, ей был присвоен экземпляр класса Client. Хотя переменную с можно использовать для доступа к методу callback () , она не имеет доступа к каким-то другим членам класса Client. Ссылочная переменная интерфейса располагает только сведениями о тех методах, которые объявлены в ее объявлении interface. Таким образом, переменная сне может применяться для доступа к методу nonl- faceMeth() , поскольку она объявлена классом Client, ан е классом Хотя приведенный пример формально показывает, как ссылочная переменная интерфейса может получать доступ к объекту реализации, он не демонстрирует полиморфные возможности такой ссылки. Чтобы продемонстрировать пример такого применения, вначале создадим вторую реализацию интерфейса Еще одна реализация интерфейса Callback, class AnotherClient implements Callback { // Реализация интерфейса Callback public void callback(int p) Еще одна версия callback"); System.out.println("p в квадрате равно " + (Теперь проверим работу следующего класса Testlface2 { public static void main(String a r g s []) { Callback с = new Cli e n t (); AnotherClient ob = new с .callback(42); Глава 9. Пакеты и интерфейсы 2 3 с = ob; // теперь с ссылается на объект с Эта программа создает следующий вывод вызванный со значением 42 Еще одна версия callback р в квадрате равно Как видите, вызываемая версия метода cal lback () определяется типом объекта, на который переменная с ссылается вовремя выполнения. Представленный пример очень прост, поэтому вскоре мы приведем еще один, более реальный пример. Частичны ер еал изац и и Если класс содержит интерфейс, ноне полностью реализует определенные им методы, он должен быть объявлен как abstract абстрактный class Incomplete implements Callback { int a, b; void show() { System.out.println(a + " " + b ) ; } / / В этом примере класс Incomplete не реализует метод callback () идол жен быть объявлен как абстрактный. Любой класс, который наследует класс Incomplete, должен реализовать метод callback () либо быть также объявлен как Вложенные интерфейсы Интерфейс может быть объявлен членом класса или другого интерфейса. Такой интерфейс называется интерфейсомчлепом или вложенным интерфейсом Вложенный интерфейс может быть объявлен как public, private или protected. Это отличает его от интерфейса верхнего уровня, который должен быть либо объявлен как public, либо, как уже было отмечено, должен использовать уровень доступа, заданный по умолчанию. Когда вложенный интерфейс используется вне содержащей его области видимости, он должен определяться именем класса или интерфейса, членом которого является. То есть вне класса или интерфейса, в котором объявлен вложенный интерфейс, его имя должно быть полностью определено. В следующем примере демонстрируется применение вложенного интерфейса Пример вложенного интерфейса Этот класс содержит интерфейс-член. class А { // это вложенный интерфейс public interface NestedlF { boolean isNotNegative(int x ) ; } } // Класс В реализует вложенный интерфейс class В implements A.NestedlF { 2 3 Часть I. Язык Java public boolean isNotNegative(int x) { return x < 0 ? false : true; } } class NestedlFDemo { public static void main(String a r g s []) { // использует ссылку на вложенный интерфейс A.NestedlF nif = new Вне является отрицательным это не будет отображаться"); } } Обратите внимание на то, что объект А определяет вложенный интерфейс NestedlF, который объявлен как public. Затем объект В реализует вложенный интерфейс, указав следующее Обратите также внимание на то, что имя интерфейса полностью определено и содержит имя класса. Внутри метода создается ссылка на интерфейс Ас именем nif, которой присваивается ссылка на объект В. Поскольку объект В реализует интерфейс А . NestedlF, это допустимо. Использование интерфейсов Чтобы возможности интерфейсов были понятны, рассмотрим более реальный пример. В предыдущих главах мы разработали класс Stack, который реализует простой стек фиксированного размера. Однако существует множество способов реализации стека. Например, стек может иметь фиксированный размер либо быть увеличивающимся. Стек может также храниться в массиве, связанном списке, бинарном дереве и т.п. Независимо от реализации стека, его интерфейс остается неизменным. То есть методы push () и pop () определяют интерфейс стека независимо от нюансов реализации. Поскольку интерфейс стека отделен от его реализации, можно без труда определить интерфейс стека, предоставляя реализации определение специфичных особенностей. Рассмотрим два примера. Вначале создадим интерфейс, который определяет целочисленный стек. Поместим его в файл I nt Stack, j ava. Этот интерфейс будет использоваться обеими реализациями стека Определение интерфейса целочисленного стека interface IntStack { void push(int item); // сохранение элемента int p o p (); // извлечение элемента } Следующая программа создает класс FixedStack, который реализует версию целочисленного стека фиксированной длины Реализация IntStack, использующая область хранения // фиксированного размера class FixedStack implements IntStack { private int stck[]; Глава 9. Пакеты и интерфейсы 3 3 private int tos; 1 1 резервирование и инициализация стека FixedStack(int size) { stck = new int[size]; tos = -1; } // заталкивание элемента в стек public void push(int item) { if (tos= = st c k .length-1) // использование члена длины стека Стек полон выталкивание элемента из стека public int p o p () { if (tos < 0) Стек пуст = new FixedStack(8); // заталкивание чисел в стек i=0; i<5; i + +) mystackl.p u s h (i ); for(int i=0; i<8; i++) mystack2.p u s h (i ); // выталкивание этих чисел из стека Стек в mystackl:"); for(int i=0; i<5; i++) System.out.println(mystackl.pop()); System.out.p r i Стек в mystack2:"); for(int i=0; i<8; Теперь создадим еще одну реализацию интерфейса IntStack, которая, используя тоже самое определение interface, создает динамический стек. В этой реализации каждый стек создается с начальной длиной. При превышении этой начальной длины размер стека увеличивается. Каждый раз, когда возникает потребность в дополнительном месте, размер стека удваивается Реализация "увеличивающегося" стека class DynStack implements IntStack { private int stck[]; private int tos; // резервирование и инициализация стека DynStack(int size) { stck = new intEsize]; tos = -1; 2 3 Часть I. Язык Java } // Заталкивание элемента в стек public void push(int item) { // если стек полон, резервирование стека большего размера if(tos==stck.length-1) { int temp[] = new int[stck.length * 2 ] ; // удвоение размера for(int i=0; i stck = temp; stck[++tos] = item; } else stck[++tos] = item; } // Выталкивание элемента из стека public int p o p () { if(tos < 0) Стек пуст = new DynStack(8); // Эти циклы увеличивают размеры каждого из стеков for(int i=0; i<12; i++) mystackl.p u s h (i ); for(int i=0; i<2 0; i++) mystack2.p u s h (i Стек в mystackl:"); for(int i=0; i<12; Стек в mystack2:"); for(int i=0; i<20; Следующий класс использует обе реализации классов FixedStack и DynStack. Для этого применяется ссылка на интерфейс. Это означает, что поиск версий при обращении к методами) осуществляется вовремя выполнения, а не вовремя компиляции Создание переменной интерфейса и обращение к стекам через нее IFTest3 { public static void main(String a r g s []) { IntStack mystack; // создание ссылочной переменной интерфейса DynStack ds = new DynStack(5); FixedStack fs = new FixedStack(8); mystack = ds; // загрузка динамического стека заталкивание чисел в стек Глава 9. Пакеты и интерфейсы 2 3 5 for(int i=0; i<12; i++) mystack.p u s h (i ); mystack = fs; // загрузка фиксированного стека i=0; i<8; i++) mystack.p u s h (i ); mystack = Значения в динамическом стеке for(int i = 0; i<12; i + + ) System.out.println(mystack.pop()); mystack = Значения в фиксированном стеке for(int i=0; i<8; i++) System.out.printIn(mystack.p o p (В этой программе mystack — ссылка на интерфейс IntStack. Таким образом, когда она ссылается на переменную ds, программа использует версии методов push ( ) и pop () , определенные реализацией DynStack. Когда же она ссылается на переменную f s, программа использует версии методов push () и pop ( ), определенные реализацией FixedStack. Как уже было сказано, эти решения принимаются вовремя выполнения. Обращение к нескольким реализациям интерфейса через ссылочную переменную интерфейса — наиболее мощный метод поддержки полиморфизма времени выполнения Переменные в интерфейсах Интерфейсы можно применять для импорта совместно используемых констант в несколько классов за счет простого объявления интерфейса, который содержит переменные, инициализированные нужными значениями. При включении интерфейса в класс (те. при реализации интерфейса) имена всех этих переменных будут помещены в область констант. (Это аналогично использованию в программе C /C ++ заголовочного файла для создания большого количества констант типа define или объявлений const.) Если интерфейс не содержит никаких методов, любой класс, который включает в себя такой интерфейс, в действительности ничего не реализует. Это равносильно тому, что класс импортировал бы постоянные поля в пространство имен класса в качестве финальных переменных. В следующем примере эта технология применяется для реализации автоматизированной системы принятия решений int SOON = 4; int NEVER = 5; } class Question implements SharedConstants { Random rand = new Ran d o m (); int ask() { int prob = (int) (100 * rand.nextDouble()); if (prob < 30) 2 3 Часть I. Язык Java else return N0; // 30% ! if (prob < 60) return YES; // 30% ! if (prob < 75) return LATER; // 15% ! if (prob < 98) return SOON; // 13% return NEVER; // 2% } class AskMe implements SharedConstants { static void answer(int result) { switch(result) { case Нет break; case Да break; case Возможно break; case LATER: System.out.pri n t l n (Позднее break; case Вскоре break; case Никогда break; } } public static void main(String a r g s []) { Question q = new Question(); answer(q.ask()) answer(q.ask()) answer(q.ask()) answer(q.a s k (Обратите внимание на то, что в этой программе использован один из стандартных классов Java — Random. Этот класс создает псевдослучайные числа. Он содержит несколько методов, которые позволяют получать случайные числа в требуемой программой форме. В этом примере применяется метод next Double (), который возвращает случайные числа в диапазоне от 0,0 до В приведенном примере программы два класса Question и AskMe реализуют интерфейс SharedConstants, в котором определены константы N0 Нет, YES Да, MAYBE Возможно, SOON Вскоре, LATER Позднее) и NEVER Никогда. Код внутри каждого класса ссылается на эти константы так, как если бы каждый класс определял или наследовал их непосредственно. Ниже показан вывод, полученный в результате выполнения этой программы. Обратите внимание на то, что при каждом запуске результаты выполнения программы будут различными Глава 9. Пакеты и интерфейсы 2 3 7 Позднее Вскоре Нет Да Возможность расширения интерфейсов Ключевое слово e x te n d s позволяет одному интерфейсу наследовать другой. Синтаксис определения такого наследования аналогичен синтаксису наследования классов. Когда класс реализует интерфейс, который наследует другой интерфейс, он должен предоставлять реализации всех методов, определенных внутри цепочки наследования интерфейса. Ниже показан пример Один интерфейс может расширять другой interface А { void m e t h l (); void m e t h 2 (); } // Теперь В включает в себя m e t h l () и m e t h 2 () и добавляет m e t h 3 (). interface В extends А { void m e t h 3 (); } // Этот класс должен реализовать все методы классов Аи В class MyClass implements В { public void m e t h l () Реализация m e t h l () } public void m e t h 2 () Реализация m e t h 2 ()."); } public void m e t h 3 () Реализация m e t h 3 ()."); } } class IFExtend { public static void main(String a r g []) { MyClass ob = new MyC l a s s (); ob.methl(); ob.meth2(); В порядке эксперимента можете попытаться удалить реализацию метода m e th l () из класса M yC lass. Это приведет к ошибке времени компиляции. Как уже было сказано, любой класс, который реализует интерфейс, должен реализовать все определенные этим интерфейсом методы, в том числе любые методы, унаследованные от других интерфейсов. Хотя в приведенных в этой книге примерах пакеты или интерфейсы используются не очень часто, оба средства являются важными составляющими среды программирования Java. Буквально все реальные программы, написанные на языке Java, будут храниться в пакетах. Вполне вероятно, что многие из них будут также реализовать интерфейсы. Поэтому важно освоить их применение 2> |