Skip to content

Latest commit

 

History

History
941 lines (798 loc) · 80.6 KB

c8.md

File metadata and controls

941 lines (798 loc) · 80.6 KB

Глава 8

Пакеты и интерфейсы

Основные навыки и понятия

  • Применение пакетов
  • Влияние пакетов на доступ к членам класса
  • Применение модификатора доступа protected
  • Импорт пакетов
  • Представление о стандартных пакетах Java
  • Основные положения об интерфейсах
  • Реализация интерфейсов
  • Применение интерфейсных ссылок
  • Представление о переменных интерфейса
  • Наследование интерфейсов

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

Пакеты

Иногда взаимозависимые части программ оказывается удобно объединить в группу. В Java для этой цели предусмотрены пакеты. Во-первых, пакет предоставляет механизм объединения взаимосвязанных частей программы. При обращении к классам, входящим в пакет, указывается имя пакета. Таким образом, пакет предоставляет средства для именования коллекции классов. И во-вторых, пакет является частью механизма управления доступом в Java. Классы могут быть объявлены как закрытые для всех пакетов, кроме того, в который они входят. Следовательно, пакет предоставляет также средства для инкапсуляции классов. Рассмотрим все эти средства более подробно.

Как правило, при именовании класса для него выделяется имя в пространстве имен. Пространство имен определяет область объявлений. В Java нельзя присваивать двум классам одинаковые имена из одного и того же пространства имен. Иными словами, в пределах пространства имен каждый класс должен иметь уникальное имя. В примерах программ, представленных в предыдущих главах, по умолчанию использовалось глобальное пространство имен. Это удобно для небольших программ, но по мере увеличения объема кода могут возникнуть конфликты имен. В крупных программах бывает нелегко выбрать уникальное имя для класса. Более того, при использовании библиотек и кода, написанного другими программистами, приходится принимать специальные меры для предотвращения конфликтов имен. Для разрешения подобных затруднений служат пакеты, позволяющие разделить пространство имен на отдельные области. Если класс определен в пакете, то имя пакета присоединяется к имени класса, в результате чего исключается конфликт между двумя классами, имеющими одинаковые имена, но принадлежащими к разным пакетам.

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

Определение пакета

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

Для создания пакета достаточно поместить оператор package в начало файла с исходным кодом программы на Java. В результате классы, определенные в этом файле, будут принадлежать указанному пакету. А поскольку пакет определяет пространство имен, имена классов, содержащихся в файле, войдут в это пространство имен как составные его части.

Общая форма оператора package выглядит следующим образом:

package имя_пакета;

Например, приведенная ниже строка кода определяет пакет mypack.

package mypack;

Для управления пакетами в Java используется файловая система, в которой для хранения содержимого каждого пакета выделяется отдельный каталог. Например, файлы с расширением . class, содержащие классы и объявленные в пакете mypack, будут храниться в каталоге mypack.

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

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

В Java допускается создавать иерархию пакетов. Для этого достаточно разделить имена пакетов точками. Ниже приведена общая форма оператора package для определения многоуровневого пакета.

package пакет_1. пакет_2. пакет_3. . . пакет_Ы;

Очевидно, что для поддержки иерархии пакетов следует создать аналогичную иерархию каталогов. Например:

package alpha.beta.gamma;

Классы, содержащиеся в данном пакете, должны храниться в структуре каталогов . . . /alpha/beta/gamma, где многоточие обозначает путь к каталогу alpha.

Поиск пакета и переменная окружения CLASSPATH

Как пояснялось выше, иерархия каталогов пакетов должна отражать иерархию пакетов. В связи с этим возникает важный вопрос: как исполняющая система Java узнает, где искать созданные пакеты? Ответ на этот вопрос следует разделить на три части. Во-первых, по умолчанию исполняющая система Java обращается к текущему рабочему каталогу. Так, если поместить пакет в подкаталоге текущего каталога, он будет там найден. Во-вторых, один или несколько путей к каталогам можно задать в качестве значения переменной окружения CLASSPATH. И в-третьих, при вызове интерпретатора java и компилятора j ava с из командной строки можно указать параметр -classpath, а также путь к каталогам с классами.

Рассмотрим в качестве примера следующее определение пакета:

package mypack

Для того чтобы программа могла найти пакет mypack, должно быть выполнено одно из трех условий: программа должна быть запущена из каталога, содержащего пакет mypack; в переменной окружения CLASSPATH должен храниться полный путь к каталогу с пакетом mypack; либо при запуске программы интерпретатору java должен быть передан параметр -classpath и указан путь к каталогу с пакетом mypack.

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

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

Простой пример применения пакета

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

// Простая программа, демонстрирующая применение пакета.
// Этот файл является частью пакета bookpack.
package bookpack;

// Класс Book принадлежит пакету bookpack.
class Book {
    private String title;
    private String author;
    private int pubDate;

    Book(String t, String a, int d) {
        title = t;
        author = a;
        pubDate = d;
    }

    void show() {
        System.out.println(title);
        System.out.println(author);
        System.out.println(pubDate);
        System.out.println();
    }
}

// Класс BookDemo принадлежит пакету bookpack.
class BookDemo {
    public static void main(String args[])  {
        Book books[] = new Book[5];

        books[0] = new Book("Java: A Beginner's Guide",
                            "Schildt", 2011);
        books[1] = new Book("Java: The Complete Reference",
                            "Schildt", 2011);
        books[2] = new Book("The Art of Java",
                            "Schildt and Holmes", 2003);
        books[3] = new Book("Red Storm Rising",
                            "Clancy", 1986);
        books[4] = new Book("On the Road",
                            "Kerouac", 1955);

        for(int i=0; i < books.length; i++) books[i].show();
    }
}

Присвойте файлу с приведенным выше исходным кодом имя BookDemo. j ava и поместите его в каталог bookpack.

Скомпилируйте этот исходный файл из каталога, находящегося непосредственно над каталогом bookpack, указав в командной строке следующее:

javac bookpack/BookDemo.j ava

После этого попробуйте выполнить скомпилированную программу, указав в командной строке следующее:

java bookpack.BookDemo

Не забывайте, что для нормального выполнения указанных выше команд текущим должен быть каталог, являющийся родительским по отношению к каталогу bookpack. (Для компиляции и запуска программы из какого-нибудь другого каталога вам придется указать путь к каталогу bookpack, используя один из двух других описанных выше способов обращения к каталогам с пакетами.)

Теперь классы BookDemo и Book принадлежат пакету bookpack. Это означает, что при вызове интерпретатора нельзя ограничиваться передачей ему только имени класса BookDemo. Приведенная ниже команда не будет выполнена, java BookDemo

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

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

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

Область действия члена класса определяется указываемым модификатором доступа: private, public или protected, хотя модификатор может и отсутствовать. На формирование области действия оказывает также влияние принадлежность класса тому или иному пакету. Таким образом, область действия члена класса определяется его доступностью как в классе, так и в пакете. Столь сложный, многоуровневый подход к управлению доступом позволяет установить достаточно обширный набор прав доступа. В табл. 8.1 описаны разные уровни доступа к членам классов. Рассмотрим каждый из них в отдельности.

Таблица 8.1. Уровни доступа к членам классов

Закрытый член Член, доступный по умолчанию Защищенный член Открытый член
Доступен в том же классе Да Да Да Да
Доступен из подкласса в том же пакете Нет Да Да Да
Доступен из любого класса в том же пакете Нет Да Да Да
Доступен из подкласса в любом пакете Нет Нет Да Да
Доступен из всякого класса в любом пакете Нет Нет Нет Да

Если модификатор доступа явно не указан для члена класса, он доступен только в своем пакете, но не за его пределами. Следовательно, член класса, для которого не задан модификатор доступа, является открытым в текущем пакете и закрытым за его пределами.

Члены класса, объявленные как открытые (public), доступны из классов, принадлежащих любым пакетам. На доступ к ним никаких ограничений не накладывается. А члены класса, объявленные как закрытые (private), доступны только для членов того же самого класса. Другие классы, даже принадлежащие тому же самому пакету, не могут воздействовать на них. И наконец, члены класса, объявленные как защищенные (protected), доступны для классов, находящихся в том же самом пакете, а также для подклассов данного класса, независимо от того, каким пакетам эти подклассы принадлежат.

Правила доступа, приведенные в табл. 8.1, распространяются только на члены классов. Сами же классы могут быть объявлены как открытые или же доступные по умолчанию. Если в определении класса присутствует ключевое слово public, он доступен для других классов. Отсутствие модификатора доступа означает, что класс доступен только классам, находящимся в том же самом пакете. На классы, объявленные как открытые, накладывается следующее единственное ограничение: имя файла, в котором находится исходный код класса, должно совпадать с именем класса.

Пример доступа к пакету

В рассмотренном выше примере классы Book и BookDemo находились в одном и том же пакете, поэтому при организации доступа из класса BookDemo к классу Book не возникало никаких затруднений. По умолчанию все члены класса имеют право обращаться к членам других классов из того же самого пакета. Если бы класс Book находился в одном пакете, а класс BookDemo — в другом, ситуация оказалась бы немного сложнее. В этом случае доступ к классу Book по умолчанию был бы запрещен. Для того чтобы сделать класс Book доступным для других пакетов, в код программы нужно внести три изменения. Во-первых, сам класс Book должен быть объявлен открытым (public). Это позволит обращаться к нему из-за пределов пакета bookpack. Во-вторых, конструктор класса должен быть также объявлен открытым. И наконец, модификатор доступа public следует указать перед методом show(). Благодаря этому конструктор и метод show() станут доступными за пределами пакета bookpack. Следовательно, для использования класса Book в классах, принадлежащих другим пакетам, его следует объявить так, как показано ниже.

// Класс Book, видоизмененный для открытого доступа,
package bookpack;
// Класс Book и некоторые его члены должны быть объявлены открытыми,
// чтобы ими можно было пользоваться в других пакетах.
public class Book {
    private String title;
    private String author;
    private int pubDate;

    // Теперь конструктор объявлен открытым,
    public Book(String t, String a, int d) {
        title = t;
        author = a;
        pubDate = d;
    }

    // Теперь метод объявлен открытым,
    public void show()  {
        System.out.println(title) ;
        System.out.println(author);
        System.out.println(pubDate);
        System.out.println() ;
    }
}

Для того чтобы воспользоваться классом Book в другом пакете, нужно применить оператор import, который будет рассматриваться в следующем разделе, либо указать полностью определенное имя класса, т.е. предварять имя класса именем пакета. Ниже приведен пример класса UseBook, содержащегося в пакете bookpackext. Для обращения к классу Book в нем используется полностью определенное имя этого класса.

// Этот класс принадлежит пакету bookpackext.
package bookpackext;
// использовать класс Book из пакета bookpack.

class UseBook {
    public static void main(String args[])  {
        // Перед именем класса Book указывается имя пакета bookpack.
        bookpack.Book books[] = new bookpack.Book[5];
        books[0] = new bookpack.Book("Java: A Beginner's Guide",
                                     "Schildt", 2011);
        books[1] = new bookpack.Book("Java: The Complete Reference",
                                     "Schildt", 20011);
        books[2] = new bookpack.Book("The Art of Java",
                                     "Schildt and Holmes", 2003);
        books[3] = new bookpack.Book("Red Storm Rising",
                                     "Clancy", 1986);
        books[4] = new bookpack.Book("On the Road",
                                     "Kerouac", 1955);

        for(int i=0; i < books.length; i++) books[i].show();
    }
}

Обратите внимание на то, что при каждом обращении к классу Book перед ним указывается имя пакета bookpack. Если бы здесь не использовалось полностью определенное имя, то при компиляции класса UseBook класс Book не был бы найден.

Представление о защищенных членах классов

Начинающие программисты иногда неправильно пользуются модификатором доступа protected. Как пояснялось ранее, переменные и методы, объявленные защищенными (protected), доступны для классов, находящихся в том же самом пакете, а также для подклассов данного класса, независимо от того, каким пакетам эти подклассы принадлежат. Иными словами, член класса, объявленный как protected, доступен для подклассов, но защищен от доступа за пределами пакета.

Для того чтобы стало понятнее назначение модификатора доступа protected, рассмотрим следующий пример. Сначала изменим класс Book, объявив его переменные экземпляра защищенными, как показано ниже.

// Объявление защищенными переменных экземпляра в классе Book,
package BookPack;
public class Book {
    // При объявлении этих переменных использован
    // модификатор доступа protected.
    protected String title;
    protected String author;
    protected int pubDate;

    public Book(String t, String a, int d) {
        title = t;
        author = a;
        pubDate = d;
    }

    public void show() {
        System.out.println(title);
        System.out.println(author);
        System.out.println(pubDate);
        System.out.println() ;
    }
}

Теперь создадим подкласс ExtBook класса Book, а также класс ProtectDemo, в котором будет использоваться класс ExtBook. В классе ExtBook содержится поле, предназначенное для хранения названия издательства, а также несколько методов доступа. Оба эти класса принадлежат пакету bookpackext. Их исходный код приведен ниже.

// Пример применения модификатора protected,
package bookpackext;

class ExtBook extends bookpack.Book {
    private String publisher;

    public ExtBook(String t, String a, int d, String p) {
        super(t, a, d);
        publisher = p;
    }

    public void show() {
        super.show();
        System.out.println(publisher);
        System.out.println() ;
    }

    public String getPublisher()    { return publisher; }
    public void setPublisher(String p) { publisher = p; }

    /* Следующие операторы допустимы, поскольку подклассы имеют
       право доступа к членам класса, объявленным защищенными. */
    public String getTitle() { return title; }
    public void setTitle(String t) { title = t; }
    public String getAuthor() { return author; }
    public void setAuthor(String a) { author = a; }
    public int getPubDate() { return pubDate; }
    public void setPubDate(int d) { pubDate = d; }
}

class ProtectDemo {
    public static void main(String args[] ) {
    ExtBook books[] = new ExtBook[5];
    books[0] = new ExtBook("Java: A Beginner's Guide",
                           "Schildt", 2007, "Osborne/McGraw-Hill");
    books[1] = new ExtBook("Java: The Complete Reference",
                           "Schildt", 2007, "Osborne/McGraw-Hill");
    books[2] = new ExtBook("The Art of Java", "Schildt and Holmes", 2003,
                           "Osborne/McGraw-Hill");
    books[3] = new ExtBook("Red Storm Rising", "Clancy", 1986, "Putnam");
    books[4] = new ExtBook("On the Road",
                           "Kerouac", 1955, "Viking");

    for(int i=0; i < books.length; i++) books[i].show();

    // искать книги по автору
    System.out.println("Showing all books by Schildt.");
    for(int i=0; i < books.length; i++)
        if(books[i].getAuthor() == "Schildt")
            System.out.println (books[i].getTitle());

    // Доступ к защищенному полю эа пределами подклассов не разрешается.
    //  books[0].title = "test title"; // Ошибка: доступ запрещен!
    }
}

Обратите внимание на код класса ExtBook. В связи с тем что класс ExtBook является подклассом, производным от класса Book, он имеет доступ к защищенным членам класса Book. Это правило действует, несмотря на то, что класс ExtBook находится в другом пакете. Следовательно, он может обращаться непосредственно к переменным экземпляра title, author и pubDate, что и было использовано при написании методов доступа. В то же время доступ к этим переменным экземпляра из класса ProtectDemo запрещен, поскольку класс ProtectDemo не является подклассом, производным от класса Book. Так, если удалить комментарии в приведенной ниже строке кода, рассматриваемая здесь программа не будет скомпилирована.

//  books[0].title = "test title"; // Ошибка: доступ запрещен.

Импорт пакетов

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

Ниже приведена общая форма оператора import,

import имя_пакета.имя_класса;

Если требуется импортировать все содержимое пакета, вместо имени класса следует указать звездочку (*). Ниже приведены примеры обеих форм записи оператора import.

import mypack.MyClass
import mypack.*;

В первом случае из пакета mypack импортируется класс MyClass, а во втором — все классы из данного пакета. В исходном файле программы на Java операторы import должны следовать сразу же после оператора package (если таковой имеется) и перед определением классов.

С помощью оператора import можно организовать доступ к пакету bookpack и воспользоваться классом Book, не прибегая к полностью определенному имени. Оператор import, разрешающий данное затруднение, помещается в начало того файла, где требуется доступ к классу Book, в следующем виде:

import bookpack.*;

Например, так будет выглядеть исходный код класса UseBook, в котором используется механизм импорта пакетов:

// Использование ключевого слова import,
package bookpackext;
// Импорт пакета bookpack.
import bookpack.*;

// использовать класс Book из пакета bookpack
class UseBook {
    public static void main(String args[]) {
        // Теперь к членам класса Book можно обращаться непосредственно,
        // не указывая полностью определенное имя.
        Book books[] = new Book[5];

        books[0] = new Book("Java: A Beginner's Guide",
                            "Schildt", 2007);
        books[1] = new Book("Java: The Complete Reference",
                            "Schildt", 2007);
        books[2] = new Book("The Art of Java",
                            "Schildt and Holmes", 2003);
        books[3] = new Book("Red Storm Rising",
                            "Clancy", 1986);
        books[4] = new Book("On the Road",
                            "Kerouac", 1955);

        for(int i=0; i < books.length; i++) books[i].show();
    }
}

Как видите, теперь нет нужды предварять имя класса 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(), определяющие этот интерфейс, выполняют одинаковые действия независимо от внутренней организации очереди. А поскольку интерфейс для очереди не зависит от конкретной ее реализации, его нетрудно определить, а конкретные детали разработать в каждой реализации очереди по отдельности.

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

Последовательность действий

  1. Создайте файл iCharQ.java и введите в него следующее определение интерфейса:
    // Интерфейс для очереди символов.
    public interface ICharQ {
        // поместить символ в очередь
        void put(char ch);
    
        // извлечь символ из очереди
        char get();
    }
    
    Как видите, этот интерфейс чрезвычайно прост: в нем объявлены только два метода. Эти методы должны быть определены в любом классе, реализующем интерфейс ICharQ.
  2. Создайте файл IQDemo.java.
  3. Начните написание примера программы в файле 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.
  4. Добавьте в файл 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 равны.
  5. Введите в файл 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.
  6. Для того чтобы продемонстрировать все три реализации интерфейса 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);
            }
        }
    }
    
  7. Выполнение этой программы дает следующий результат:
    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
    
  8. А теперь попробуйте самостоятельно поупражняться в организации очередей. Создайте кольцевой вариант очереди 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

  1. Используя код, созданный в примере для опробования 8.1, поместите в пакет qpack интерфейс iCharQ и все три реализующие его класса. Класс iQDemo должен остаться в пакете, используемом по умолчанию. Покажите, как импортировать и использовать классы из пакета qpack.
  2. Что такое пространство имен? Почему так важна возможность его разделения на отдельные области в Java?
  3. Содержимое пакетов хранится в ___________________ .
  4. В чем отличие доступа, определяемого ключевым словом protected, от доступа по умолчанию?
  5. Допустим, классы, содержащиеся в одном пакете, требуется использовать в другом пакете. Какими двумя способами можно этого добиться?
  6. “Один интерфейс — множество методов” — главный принцип Java. Какое языковое средство лучше всего демонстрирует этот принцип?
  7. Сколько классов могут реализовать один и тот же интерфейс? Сколько интерфейсов может реализовать класс?
  8. Может ли один интерфейс наследовать другой интерфейс?
  9. Создайте интерфейс для класса Vehicle, рассмотренного в главе 7, назвав его IVehicle.
  10. Переменные, объявленные в интерфейсе, неявно считаются как static и final. Какие преимущества это дает?
  11. Пакет, по существу, является контейнером для классов. Верно или не верно?
  12. Какой стандартный пакет импортируется по умолчанию в любую программу на Java?