Материал предоставлен https://it.rfei.ru

Интерфейсы

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

Описание интерфейсов

Высокоуровневый интерфейс имеет следующий общий синтаксис:

<модификатор доступа> interface <имя интерфейса>
                              <extends-выражение> // Заголовок интерфейса
{ // Тело интерфейса
    <Объявление констант>
    <Объявления прототипов метода>
    <Объявления вложенных классов>
    <Объявления вложенных интерфейсов>
}

В заголовке интерфейса имени интерфейса предшествует ключевое слово interface. Также в заголовке интерфейса может содержаться следующая информация:

  • видимость или модификатор доступа;
  • любой интерфейс, который расширяется.

Тело интерфейса может содержать объявления членов, а именно:

  • объявления констант;
  • объявления прототипов методов;
  • объявления вложенных классов и интерфейсов.

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

Объявления членов возможны в произвольном порядке в теле интерфейса. Поскольку интерфейсы предназначены для реализации их классами, их члены неявно общедоступны, а модификатор доступа public опускается. Интерфейсы с пустыми телами используются как маркеры для обозначения классов, имеющих определенное свойство или поведение. Такие интерфейсы также называются интерфейсами способности (ability interfaces). Java API предоставляет несколько примеров таких интерфейсов-маркеров:

java.lang.Cloneable, java.io.Serializable, java.util.EventListener

Объявления прототипов методов

Интерфейс определяет контракт с помощью задания ряда прототипов методов, но не их реализаций. Методы в интерфейсе все неявно абстрактны и общедоступны. Прототип метода имеет такой же синтаксис, как и абстрактный метод. Однако разрешены только модификаторы abstract и public, которые, в свою очередь, обычно опускают.

В примере 6.9 объявлены два интерфейса: IStack в строке (1) и ISafeStrack. в строке (5). Эти интерфейсы рассматриваются в последующих подразделах.

Пример 6.9. Интерфейсы

interface IStack {                                                // (1)
    void   push(Object item);
    Object pop();
}

class StackImpl implements IStack {                               // (2)
    protected Object[] stackArray;
    protected int      tos;  // top of stack

    public StackImpl(int capacity) {
        stackArray = new Object[capacity];
        tos        = -1;
    }

    public void push(Object item)                                 // (3)
        { stackArray[++tos] = item; }

    public Object pop() {                                         // (4)
        Object objRef = stackArray[tos];
        stackArray[tos] = null;
        tos--;
        return objRef;
    }

    public Object peek() { return stackArray[tos]; }
}

interface ISafeStack extends IStack {                             // (5)
    boolean isEmpty();
    boolean isFull();
}

class SafeStackImpl extends StackImpl implements ISafeStack {     // (6)

    public SafeStackImpl(int capacity) { super(capacity); }
    public boolean isEmpty() { return tos < 0; }                  // (7)
    public boolean isFull() { return tos >= stackArray.length-1; }// (8)
}
public class StackUser {

    public static void main(String[] args) {                      // (9)
        SafeStackImpl safeStackRef  = new SafeStackImpl(10);
        StackImpl     stackRef      = safeStackRef;
        ISafeStack    isafeStackRef = safeStackRef;
        IStack        istackRef     = safeStackRef;
        Object        objRef        = safeStackRef;

        safeStackRef.push("Dollars");                             // (10)
        stackRef.push("Kroner");
        System.out.println(isafeStackRef.pop());
        System.out.println(istackRef.pop());
        System.out.println(objRef.getClass());
    }
}

Вывод программы:

Kroner
Dollars
class SafeStackImpl

Реализация интерфейсов

Каждый класс может выбрать для реализации, полностью или частично, ноль или более интерфейсов. Класс определяет интерфейсы, которые реализует, как разделяемый запятой список не повторяющихся имен интерфейсов после оператора implements в заголовке класса. Методы интерфейса все должны иметь доступ public, когда они реализованы в классе (или его подклассах). Класс не может ни уменьшить уровень доступа метода интерфейса, ни определить для него новые исключения в выражении под throws, если попытаться сделать это, то это будет эквивалентно изменению контракта, что недопустимо. Критерий для переопределения методов также применим и при реализации методов интерфейса.

Класс может обеспечить реализации методов, объявленных в интерфейсе, но он не получит всех преимуществ интерфейса, пока не определит явно имя интерфейса в операторе implements.

В примере 6.9 класс StackImpl реализует интерфейс IStack. Одновременно определяя имя интерфейса в операторе implements в заголовке класса в строке (2) и предоставляя реализацию для методов в интерфейсе в (3) и (4). Изменение общедоступной (public) видимости этих методов приведет к ошибке компиляции, так как это действие сужает уровень доступа членов.

Класс может выбрать для реализации только некоторые методы из его интерфейсов (т.е. предоставить частичную реализацию своих интерфейсов). Класс затем должен быть объявлен как абстрактный (abstract). Обратите внимание, что методы интерфейса не могут быть объявлены как static, потому что они содержат контракт, выполняемый объектами класса, реализующего интерфейс. Методы интерфейса всегда реализуются как методы экземпляра (instance methods).

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

Расширение интерфейсов

Интерфейс может расширять другие интерфейсы с помощью оператора extends В отличие от расширения классов интерфейс может расширять несколько интерфейсов. Интерфейсы, расширяемые интерфейсом (напрямую или косвенно), называются суперинтерфейсами. Напротив, интерфейс является подынтерфейсом своего суперинтерфейса. Поскольку интерфейсы определяют новые ссылочные типы, суперинтерфейсы и подынтерфейсы также называются супертипами и подтипами соответственно.

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

В примере 6.9 показан вариант множественного наследования в Java. В нем интерфейс ISafeStack расширяет интерфейс IStack в строке (5). Класс SafeStacklmpl одновременно расширяет класс Stacklmpl и реализует интерфейс ISafeStack в строке (6). И реализация, и иерархия наследования интерфейса для классов и интерфейсов, рассмотренная в примере 6.9, показана на рис. 6.3.

В UML интерфейс имеет сходство с классом. Единственный способ, чтобы их различать, заключается в использовании фразы interface, как показано на рис. 6.3. Наследование интерфейсов показывается подобно наследованию реализации, но пунктирной стрелкой наследования. Размышляя в терминах типов, каждый ссылочный тип в Java является подтипом типа Object. Это означает, что каждый тип интерфейс также является подтипом типа Object. Рис. 6.3 дополнен стрелками наследования чтобы показать отношение между подтипом и супертипом.

Стоит обратить внимание на то, как класс SafeStackImpl реализует интерфейс ISafeStack: он наследует реализации методов push() и рор() из своего суперкласса StackImpl и предоставляет свою собственную реализацию методов isFull() и isEmpty() из интерфейса ISafeStack. Интерфейс ISafeStack наследует два прототипа методов из суперинтерфейса IStack. Все его методы реализуются классом SafeStackImpl. Класс SafeStackImpl неявно реализует интерфейс iStack: он реализует интерфейс ISafeStack, который наследует от интерфейса iStack. Это легко видно из ромбовидной формы иерархии наследования на рис. 6.3. Существует только одна единичная реализация наследования в классе SafeStackImpl.

Рис. 6.З. Отношение наследования

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

  1. Линейная иерархия наследования реализации между классами: класс расширяет другой класс (подклассы — суперклассы).
  2. Иерархия множественного наследования между интерфейсами: интерфейсы расширяют другие интерфейсы (подынтерфейсы - суперинтерфейсы).
  3. Иерархия множественного наследования между интерфейсами и классами: класс реализует интерфейсы.

Хотя интерфейсы не могут порождать объекты, ссылки интерфейсных типов можно объявить. Ссылки на объекты класса могут быть присвоены ссылкам супертипов класса. В примере 6.9 создается объект класса SafeStackImpl в методе main() класса StackUser в строке (9). Значение объектной ссылки присваивается всем ссылка типа супертипов объекта, которые используются для управления им.

Константы в интерфейсах

В интерфейсе также можно объявить именованные константы. Такие константы описываются посредством объявлений полей и считаются общедоступными (public) и статическими (static) и неизменяемыми (final). Модификаторы доступа обычно опускаются при объявлении.

Получить доступ к интерфейсной константе может любой клиент (класс и интерфейс), используя ее полное имя, независимо от того, расширяет ли клиент ил реализует ее интерфейс. Однако, если клиент является классом, который реализует такой интерфейс, или интерфейсом, который расширяет этот интерфейс, клиент может также получить доступ к таким константам непосредственно, без использования полного имени. Такой клиент наследует интерфейсные константы. Типичное использование констант интерфейса показано в примере 6.10, который демонстрирует и непосредственный доступ, и использование полного имени в строках (1) и (2) соответственно.

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

Пример 6.10. Переменные в интерфейсах

interface Constants {
    double PI_APPROXIMATION = 3.14;
    String AREA_UNITS = " sq.cm.";
    String LENGTH_UNITS = " cm.";
}

public class Client implements Constants {
    public static void main(String[] args) {
        double radius = 1.5;
        System.out.println("Area of circle is " +
                           (PI_APPROXIMATION*radius*radius) +
                           AREA_UNITS);             // (1) Непосредственный доступ.
        System.out.println("Circumference of circle is " +
                           (2*Constants.PI_APPROXIMATION*radius) +
                           Constants.LENGTH_UNITS); // (2) Полное имя.
    }
}

Вывод программы:

Area of circle is 7.0649999999999995 sq.cm.
Circumference of circle is 9.42 cm.
Ключевые слова this() и super()Иерархия типов