Текст книги "Java: руководство для начинающих (ЛП)"
Автор книги: Герберт Шилдт
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 18 (всего у книги 36 страниц)
Как видите, теперь нет нужды предварять имя класса Book именем пакета. Библиотечные классы Java, содержащиеся в пакетах
Как пояснялось ранее, в Java определено большое количество стандартных классов, доступных всем программам. Библиотека классов Java обычно называется Java API (Application Programming Interface – прикладной программный интерфейс). Классы, входящие в состав библиотеки Java API, хранятся в пакетах. На верхней ступени иерархии находится пакет java. В его состав входят подчиненные пакеты, включая и перечисленные ниже. Пакет Описание java.lang Содержит большое количество классов общего назначения java.io Содержит классы, предназначенные для поддержки ввода-вывода java.net Содержит классы, предназначенные для поддержки сетевого взаимодействия java.applet Содержит классы, предназначенные для создания апплетов java.awt Содержит классы, обеспечивающие поддержку набора инструментальных средств Abstract Window Toolkit
В примерах программ, представленных в этой книге, с самого начала использовался пакет j ava. lang. Помимо прочего, он содержит класс System (к нему не раз приходилось обращаться при вызове метода println ()). Пакет j ava. lang примечателен тем, что он автоматически включается в каждую программу на Java. А содержимое других пакетов приходится импортировать явным образом. Некоторые из этих пакетов будут рассмотрены в последующих главах книги. Интерфейсы
Иногда в объектно-ориентированном программировании полезно определить, что именно должен делать класс, но не как он должен это делать. Примером тому может служить упоминавшийся ранее абстрактный метод. В абстрактном методе определяются возвращаемый тип и сигнатура метода, но не предоставляется его реализация. А в подклассе должна быть обеспечена своя собственная реализация каждого абстрактного метода, определенного в его суперклассе. Таким образом, абстрактный метод определяет интерфейс, но не реализацию метода. Конечно, абстрактные классы и методы приносят известную пользу, но положенный в их основу принцип может быть развит далее. В Java предусмотрено разделение интерфейса класса и его реализации с помощью ключевого слова interface.
С точки зрения синтаксиса интерфейсы подобны абстрактным классам. Но в интерфейсе ни у одного из методов не должно быть тела. Это означает, что в интерфейсе вообще не предоставляется никакой реализации. В нем указывается только, что именно следует делать, но не как это делать. Как только интерфейс будет определен, он может быть реализован в любом количестве классов. Кроме того, в одном классе может быть реализовано любое количество интерфейсов.
Для реализации интерфейса в классе должны быть предоставлены тела (т.е. конкретные реализации) методов, описанных в этом интерфейсе. Каждому классу предоставляется полная свобода для определения деталей своей собственной реализации интерфейса. Следовательно, один и тот же интерфейс может быть реализован в двух классах по-разному. Тем не менее в каждом из них должен поддерживаться один и тот же ряд методов данного интерфейса. А в том коде, где известен такой интерфейс, могут использоваться объекты любого из этих двух классов, поскольку интерфейс для всех этих объектов остается одинаковым. Благодаря поддержке интерфейсов в Java может быть в полной мере реализован главный принцип полиморфизма: “один интерфейс – множество методов”.
Интерфейсы объявляются с помощью ключевого слова interface. Ниже приведена упрощенная форма объявления интерфейса. доступ interface имя { возвращаемый_тип имя_метода_1 (список_параметров) ; возвращаемый__тип имя_метода_2 (список_параметров) ; тип переменная__1 = значение; тип переменная_2 = значение; // ... возвращаемый_тип имя_метода_Ы(список_параметров) ; тип переменная_Ы = значение; }
Здесь доступ обозначает тип доступа, который определяется модификатором доступа public или вообще не указывается. Если модификатор доступа отсутствует, применяется правило, предусмотренное по умолчанию, т.е. интерфейс оказывается доступным только членам своего пакета. Ключевое слово public указывает на то, что интерфейс может использоваться в любом другом пакете. (Код интерфейса, объявленного как public, должен храниться в файле, имя которого совпадает с именем интерфейса.) А имя интерфейса может быть любым допустимым идентификатором.
При объявлении методов указываются их сигнатуры и возвращаемые типы. Эти методы являются, по существу, абстрактными. Как упоминалось выше, реализация метода не может содержаться в составе интерфейса. Каждый класс, в определении которого указан интерфейс, должен реализовать все методы, объявленные в интерфейсе. Методы, объявленные в интерфейсе, неявно считаются открытыми (public).
Переменные, объявленные в интерфейсе, не являются переменными экземпляра. Они неявно обозначаются ключевыми словами public, finalnstaticn обязательно подлежат инициализации. По существу, они являются константами. Ниже приведен пример определения интерфейса. Предполагается, что этот интерфейс должен быть реализован в классе, где формируется последовательный ряд числовых значений. public interface Series { int getNext(); // возвратить следующее по порядку число void reset(); // начать все с самого сначала void setStart(int х); // задать начальное значение }
Этот интерфейс объявляется открытым (public), а следовательно, он может быть реализован в классе, принадлежащем любому пакету. Реализация интерфейсов
Определенный один раз интерфейс может быть реализован одним или несколькими классами. Для реализации интерфейса в определение класса следует ввести ключевое слово implements, а затем определить методы, объявленные в интерфейсе. Ниже приведена общая форма реализации интерфейса в классе. class имя_класса extends суперкласс implements интерфейс { // тело класса }
Если в классе должно быть реализовано несколько интерфейсов, то имена интерфейсов указываются через запятую. Разумеется, ключевое слово extends и имя суперкласса указывать не обязательно.
Реализуемые методы интерфейса должны быть объявлены открытыми (public). А сигнатура реализованного метода должна полностью соответствовать сигнатуре, объявленной в составе интерфейса. Ниже приведен пример класса ByTwos, реализующего рассмотренный ранее интерфейс Series. В этом классе формируется последовательный ряд числовых значений, каждое из которых на два больше предыдущего. // Реализация интерфейса Series, class ByTwos implements Series { int start; int val; ByTwos() { start = 0; val = 0; } public int getNext() { val += 2; return val; } public void reset() { start = 0; val = 0; } public void setStart(int x) { start = x; val = x; } }
Обратите внимание на то, что методы getNext (), reset () и setStart () объявлены открытыми. Это нужно сделать непременно, поскольку любой метод интерфейса неявно считается открытым для доступа. Ниже приведен пример программы, демонстрирующий применение класса ByTwos. class SeriesDemo { public static void main(String args[]) { ByTwos ob = new ByTwos(); for(int i=0; i < 5; i++) System.out.println("Next value is " + ob.getNext ()); System.out.println("nResetting"); ob.reset(); for(int i=0; i < 5; i++) System.out.println("Next value is " + ob.getNext()); System.out.println("nStarting at 100"); ob.setStart(100) ; for(int i=0; i < 5; i++) System.out.println("Next value is " + ob.getNext()); } }
Выполнение этой программы дает следующий результат: Next value is 2 Next value is 4 Next value is 6 Next value is 8 Next value is 10 Resetting Next value is 2 Next value is 4 Next value is 6 Next value is 8 Next value is 10 Starting at 100 Next value is 102 Next value is 104 Next value is 106 Next value is 108 Next value is 110
Класс, реализующий интерфейс, может содержать дополнительные переменные и методы, что вполне допустимо. Более того, именно так в большинстве случаев и поступают программирующие на Java. Например, в приведенную ниже версию класса By Twos добавлен метод getPrevious (), возвращающий предыдущее числовое значение. // Реализация интерфейса Series и добавление метода getPrevious(). class ByTwos implements Series { int start; int val; int prev; ByTwos() { start = 0; val = 0; prev = -2; } public int getNextO { prev = val; val += 2; return val; } public void reset() { start = 0; val = 0; prev = -2; } public void setStart(int x) { start = x; val = x; prev = x – 2; } // Добавление метода, не объявленного в интерфейсе Series. int getPrevious() { return prev; } }
Обратите внимание на то, что для добавления метода getPrevious () пришлось изменить реализацию методов, объявленных в интерфейсе Series. Но сам интерфейс не претерпел никаких изменений. Эти изменения не видны за пределами класса и не влияют на его использование. В этом и состоит одно из преимуществ интерфейсов.
Как пояснялось ранее, интерфейс может быть реализован каким угодно количеством классов. В качестве примера ниже приведен код класса ByThrees. Этот класс формирует последовательный ряд числовых значений, каждое из которых на три больше предыдущего. // Еще одна реализация интерфейса Series, class ByThrees implements Series { int start; int val; ByThrees() { start = 0; val = 0; } public int getNext () { val += 3; return val; } public void reset () { start = 0; val – 0; } public void setStart(int x) { start = x; val = x; } }
Следует также иметь в виду, что если в определении класса имеется ключевое слово implements, но он не реализует все методы указанного интерфейса, то этот класс должен быть объявлен абстрактным (abstract). Объект такого класса создать нельзя, но его можно использовать в качестве суперкласса, а завершить реализацию методов интерфейса в его подклассах. Применение интерфейсных ссылок
Как это ни покажется странным, но в Java допускается объявлять переменные ссылочного интерфейсного типа, т.е. переменные ссылки на интерфейс. Такая переменная может ссылаться на любой объект, реализующий ее интерфейс. При вызове метода для объекта по интерфейсной ссылке выполняется вариант этого метода, реализованный в классе данного объекта. Этот процесс аналогичен применению ссылки на суперкласс для доступа к объекту подкласса, как пояснялось в главе 7.
Ниже приведен пример программы, демонстрирующий применение интерфейсной ссылки. По такой ссылке в данной программе будут вызываться методы, принадлежащие классам ByTwos и ByThrees. // Применение интерфейсных ссылок, class ByTwos implements Series { int start; int val; ByTwos () { start = 0; val = 0; } public int getNext() { val += 2; return val; } public void reset() { start = 0; val = 0; } public void setStart(int x) { start = x; val = x; } } class ByThrees implements Series { int start; int val; ByThrees() { start = 0; val = 0; } public int getNext() { val += 3; return val; } public void reset() { start = 0; val = 0; } public void setStart(int x) { start = x; val = x; } } class SeriesDemo2 { public static void main(String args[]) { ByTwos twoOb = new ByTwos(); ByThrees threeOb = new ByThrees(); Series ob; for(int i=0; i < 5; i++) { ob = twoOb; // Обращение к объекту по интерфейсной ссылке. System.out.println("Next ByTwos value is " + ob.getNext()); ob = threeOb; // Обращение к объекту по интерфейсной ссылке. System.out.println("Next ByThrees value is " + ob.getNext()); } } }
В методе main () переменная ob объявляется как ссылка на интерфейс Series. Это означает, что в данной переменной может храниться ссылка на любой объект, реализующий интерфейс Series. В данном случае в переменной ob сохраняется ссылка на объекты twoOb и threeOb, т.е. в разные моменты времени переменная представляет собой ссылку на объект класса ByTwos или же на объект класса ByThrees. Оба эти класса реализуют интерфейс Series. Переменная ссылки на интерфейс содержит сведения только о методах, объявленных в этом интерфейсе. Следовательно, переменная ob не может быть использована для доступа к любым другим переменным и методам, которые поддерживаются в объекте, но не объявлены в интерфейсе.
Пример для опробования 8.1. Создание интерфейса для очереди
Для того чтобы продемонстрировать истинные возможности интерфейсов, обратимся к конкретному практическому примеру. В предыдущих главах был создан класс Queue, реализующий простую очередь фиксированного размера для хранения символов. Но обеспечить функционирование очереди можно разными способами. В частности, очередь может быть фиксированного размера или “растущей”, линейной (т.е. переполняться при достижении верхней границы выделенной памяти) или кольцевой (в этом случае при удалении символов из очереди освобождается место для новых элементов). Кроме того, очередь может быть реализована на базе массива, связного списка, двоичного дерева и т.д. Как бы ни была воплощена очередь, интерфейс для нее остается без изменения, т.е. методы put () и get (), определяющие этот интерфейс, выполняют одинаковые действия независимо от внутренней организации очереди. А поскольку интерфейс для очереди не зависит от конкретной ее реализации, его нетрудно определить, а конкретные детали разработать в каждой реализации очереди по отдельности.
В этом проекте предстоит сначала создать интерфейс для очереди, хранящей символы, а затем реализовать его тремя способами. Во всех трех реализациях для хранения символов будет использоваться массив. Одна из очередей будет линейной и фиксированного размера, т.е. такая же, как и реализованная ранее. Вторая очередь будет кольцевой. В кольцевой очереди при достижении границ массива значения индексов будут автоматически изменяться таким образом, чтобы указывать на начало очереди. Таким образом, в кольцевую очередь можно будет поместить любое количество элементов, но при условии своевременного удаления элементов, включенных в нее ранее. И наконец, третья очередь будет динамической. Это означает, что ее размеры будут увеличиваться по мере необходимости.
Последовательность действий
Создайте файл iCharQ.java и введите в него следующее определение интерфейса: // Интерфейс для очереди символов. public interface ICharQ { // поместить символ в очередь void put(char ch); // извлечь символ из очереди char get (); }
Как видите, этот интерфейс чрезвычайно прост: в нем объявлены только два метода. Эти методы должны быть определены в любом классе, реализующем интерфейс ICharQ.
Создайте файл IQDemo.java.
Начните написание примера программы в файле IQDemo.java с приведенного ниже кода класса FixedQueue. // Класс, реализующий очередь фиксированного размера // для хранения символов. class FixedQueue implements ICharQ { private char q[]; // Массив для хранения элементов очереди, private int putloc, getloc; // Индексы размещения и извлечения // элементов очереди. // создать пустую очередь заданного размера public FixedQueue(int size) { q = new char[size+1]; // выделить память для очереди putloc = getloc = 0; } // поместить символ в очередь public void put(char ch) { if(putloc==q.length-1) { System.out.println(" – Queue is full."); return; } putloc++; q[putloc] = ch; } // извлечь символ из очереди public char get() { if(getloc == putloc) { System.out.println(" – Queue is empty."); return (char) 0; } getloc++; return q[getloc]; } }
Эта реализация интерфейса ICharQ выполнена на основе уже знакомого вам класса Queue, разработанного в главе 5.
Добавьте в файл IQDemo. j ava приведенный ниже класс CircularQueue. Он реализует кольцевую очередь для хранения символов. // Кольцевая очередь. class CircularQueue implements ICharQ { private char q[]; // Массив для хранения элементов очереди, private int putloc, getloc; // Индексы размещения и извлечения // элементов очереди. // создать пустую очередь заданного размера public CircularQueue (int size) { q = new char[size+1]; // выделить память для очереди putloc = getloc = 0; } // поместить символ в очередь public void put(char ch) { /* Очередь считается полной, если индекс putloc на единицу меньше индекса getloc или если индекс putloc указывает на конец массива, а индекс getloc – на его начало. */ if(putloc+l==getloc | ((putloc==q.length-1) & (getloc==0))) { System.out.println(" – Queue is full."); return; } putloc++; if(putloc==q.length) putloc = 0; // перейти в начало массива q[putloc] = ch; } // извлечь символ из очереди public char get() { if(getloc == putloc) { System.out.println(" – Queue is empty."); return (char) 0; } getloc++; if (getloc==q. length) getloc = 0f– // вернуться в начало очереди return q[getloc]; } }
В кольцевой очереди повторно используются элементы массива, освобожденные при извлечении символов. Поэтому в нее можно поместить неограниченное число элементов (при условии, что элементы, помещенные в очередь ранее, будут вовремя удалены). Отслеживание границ массива производится очень просто (достаточно обнулить индекс по достижении верхней границы), хотя условие достижения этих границ может, на первый взгляд, показаться не совсем понятным. Кольцевая очередь переполняется не тогда, когда достигается верхняя граница массива, а тогда, когда число элементов, ожидающих извлечения из очереди, становится слишком большим. Поэтому в методе put () проверяется ряд условий с целью определить момент переполнения очереди. Как следует из комментариев к коду, очередь считается заполненной, если индекс putloc оказывается на единицу меньше индекса getloc или если индекс putloc указывает на конец массива, а индекс getloc – на его начало. Как и прежде, очередь считается пустой, если индексы getloc и putloc равны.
Введите в файл IQDemo.java приведенный ниже код класса DynQueue. Этот код реализует динамическую, или “растущую”, очередь, т.е. такую очередь, размеры которой увеличиваются, когда в ней не хватает места для символов. // Динамическая очередь. class DynQueue implements ICharQ { private char q[]; // Массив для хранения элементов очереди, private int putloc, getloc; // Индексы размещения и извлечения // элементов очереди. // создать пустую очередь заданного размера public DynQueue(int size) { q = new char[size+1]; // выделить память для очереди putloc = getloc = 0; } // поместить символ в очередь public void put(char ch) { if(putloc==q.length-1)-{ // увеличить размер очереди char t[] = new ch^r[q.length * 2]; // скопировать элементы в новую очередь for(int i=0; i < q.length; i++) t[i] = q[i]; q = t; } putloc++; q[putloc] = ch; } // извлечь символ из очереди ' public char get() { if(getloc == putloc) { System.out.println(" – Queue is empty."); return (char) 0; } getloc++; return q[getloc]; } }
В данной реализации при попытке поместить в заполненную очередь еще один элемент создается новый массив, размеры которого в два раза превышают размеры исходного, текущее содержимое очереди копируется в новый массив, а ссылка на него помещается в переменную q.
Для того чтобы продемонстрировать все три реализации интерфейса ICharQ, добавьте в файл IQDemo.java приведенный ниже класс, в котором для доступа ко всем трем очередям используется переменная ссылки на интерфейс ICharQ. // Демонстрация трех реализаций интерфейса ICharQ. class IQDemo { public static void main(String args[]) { FixedQueue ql = new FixedQueue(10); DynQueue q2 = new DynQueue(5); CircularQueue q3 = new CircularQueue(10); ICharQ iQ; char ch; int i; iQ = q1; // поместить ряд символов в очередь фиксированного размера for(i=0; i < 10; i++) iQ.put((char) ('A1 + i) ) ; // отобразить содержимое очереди System.out.print("Contents of fixed queue: "); for(i=0; i < 10; i++) { ch = iQ. get () ; System.out.print(ch); } System.out.println (); iQ = q2; // поместить ряд символов в динамическую очередь for (i=0; i < 10; i++) iQ.put((char) ('Z1 – i)); // отобразить содержимое очереди System.out.print("Contents of dynamic queue: "); for(i=0; i < 10; i++) { ch = iQ.get (); System.out.print(ch); } System.out.println (); iQ = q3; // поместить ряд символов в кольцевую очередь for (i=0; i < 10; i++) iQ.put((char) ('A1 + i)); // отобразить содержимое очереди System.out.print("Contents of circular queue: "); for(i=0; i < 10; i++) { ch = iQ.get(); System.out.print(ch); } System.out.println(); // поместить больше символов в кольцевую очередь for(i=10; i < 20; i++) – iQ.put((char) (’A' + i)); // отобразить содержимое очереди System.out.print("Contents of circular queue: "); for(i=0; i < 10; i++) { ch = iQ.get(); System.out.print(ch); } System.out.println("nStore and consume from" + " circular queue."); // поместить символы в кольцевую очередь и извлечь их оттуда for(i=0; i < 20; i++) { iQ.put((char) ('A1 + i)); ch = iQ.get(); System.out.print(ch); } } }
Выполнение этой программы дает следующий результат: Contents of fixed queue: ABCDEFGHIJ Contents of dynamic queue: ZYXWVUTSRQ Contents of circular queue: ABCDEFGHIJ Contents of circular queue: KLMNOPQRST Store and consume from circular queue. ABCDEFGHIJKLMNOPQRST
А теперь попробуйте самостоятельно поупражняться в организации очередей. Создайте кольцевой вариант очереди DynQueue. Добавьте в интерфейс ICharQ метод reset (), устанавливающий очередь в исходное состояние. Создайте статический метод для копирования содержимого одной очереди в другую. Переменные в интерфейсах
Как упоминалось выше, в интерфейсах могут объявляться переменные, но они неявно считаются как public, static и final. На первый взгляд, такие переменные находят лишь ограниченное применение, но это не совсем так. В крупных программах часто используются константы, описывающие размеры массивов, граничные и специальные значения и т.п. Для крупных программ обычно создается несколько исходных файлов, а следовательно, требуется удобный способ доступа к константам из любого файла. В Java решить эту задачу помогают интерфейсы.
Для того чтобы определить набор общедоступных констант, достаточно создать интерфейс, в котором объявлялись бы не методы, а только нужные константы. Каждый класс, которому требуются эти константы, должен просто “реализовать” интерфейс, чтобы сделать константы доступными. Ниже приведен несложный пример, демонстрирующий такой подход. // Интерфейс, содержащий только константы, interface IConst { // Константы, int MIN = 0; int MAX = 10; String ERRORMSG = "Boundary Error"; } class IGonstD implements IConst { public static void main(String args[]) { int nums[] = new int[MAX]; for(int i=MIN; i < 11; i++) { if(i >= MAX) System.out.println(ERRORMSG); else { nums[i] = i; System.out.print(nums[i] + " "); } } } } Наследование интерфейсов
Один интерфейс может наследовать другой интерфейс, для чего служит ключевое слово extends. Синтаксис наследования интерфейсов ничем не отличается от того, что употребляется для наследования классов. Если класс реализует один интерфейс, наследующий другой интерфейс, в нем следует определить все методы, объявленные в интерфейсах по всей цепочке наследования. Ниже приведен пример, демонстрирующий наследование интерфейсов. // Наследование интерфейсов, interface А { void methl() ; void meth2(); } // Интерфейс В содержит методы methl() и meth2(), а // кроме того, в него добавляется метод meth3(). interface В extends А { // Интерфейс В наследует интерфейс А. void meth3(); } // Этот класс должен реализовать все методы, // объявленные в интерфейсах А и В. class MyClass implements В { public void methl() { System.out.println("Implement methl()."); } public void meth2() { System.out.println("Implement meth2()."); } public void meth3() { System.out.println("Implement meth3() .") ; } } class IFExtend { public static void main(String arg[]) { MyClass ob = new MyClass(); ob.methl(); ob.meth2() ; ob.meth3() ; } }
В качестве эксперимента можно попробовать удалить из класса MyClass реализацию метода methl (). Это приведет к ошибке при компиляции. Как упоминалось выше, в каждом классе, реализующем интерфейс, должны быть определены все методы, объявленные в интерфейсе, в том числе те, которые были унаследованы от других интерфейсов.
И хотя пакеты и интерфейсы нечасто используются в примерах программ, представленных в этой книге, следует все же иметь в виду, что эти языковые средства являются важной частью Java. Практически во всех реальных программах и апплетах, написанных на Java, применяются пакеты, а многие классы реализуют интерфейсы. Поэтому эти языковые средства необходимо знать, чтобы уметь пользоваться ими в практике программирования на Java. Упражнение для самопроверки по материалу главы 8
Используя код, созданный в примере для опробования 8.1, поместите в пакет qpack интерфейс iCharQ и все три реализующие его класса. Класс iQDemo должен остаться в пакете, используемом по умолчанию. Покажите, как импортировать и использовать классы из пакета qpack.
Что такое пространство имен? Почему так важна возможность его разделения на отдельные области в Java?
Содержимое пакетов хранится в _ .
В чем отличие доступа, определяемого ключевым словом protected, от доступа по умолчанию?
Допустим, классы, содержащиеся в одном пакете, требуется использовать в другом пакете. Какими двумя способами можно этого добиться?
“Один интерфейс – множество методов” – главный принцип Java. Какое языковое средство лучше всего демонстрирует этот принцип?
Сколько классов могут реализовать один и тот же интерфейс? Сколько интерфейсов может реализовать класс?
Может ли один интерфейс наследовать другой интерфейс?
Создайте интерфейс для класса Vehicle, рассмотренного в главе 7, назвав его IVehicle.
Переменные, объявленные в интерфейсе, неявно считаются как static и final. Какие преимущества это дает?
Пакет, по существу, является контейнером для классов. Верно или не верно?
Какой стандартный пакет импортируется по умолчанию в любую программу на Java?