Интерфейсы - объявление и применение

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

abstract class Geom {
    int width, color;
    abstract void draw();
}

от которого наследуются дочерние классы графических примитивов:

class Line extends Geom {
    void draw() {
        System.out.println("Рисование линии");
    }
}
 
class Rectangle extends Geom {
    void draw() {
        System.out.println("Рисование прямоугольника");
    }
}
 
class Triangle extends Geom {
    void draw() {
        System.out.println("Рисование треугольника");
    }
}

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

Как это лучше всего сделать? Имея текущие знания, нам придется непосредственно в дочерних классах Rectangle и Triangle реализовать метод для вычисления площади, например, так:

class Rectangle extends Geom {
    void draw() {
        System.out.println("Рисование прямоугольника");
    }
 
    public double getSquare() {
        return 5*10;
    }
}
 
class Triangle extends Geom {
    void draw() {
        System.out.println("Рисование треугольника");
    }
 
    public double getSquare() {
        return 0.5*4*10;
    }
}

Но в этом случае, у нас не будет общего интерфейса для обращения к этому методу. То есть, если в функции main мы создадим несколько примитивов:

        final int N = 3;
        Geom g[] = new Geom[N];
        g[0] = new Line();
        g[1] = new Rectangle();
        g[2] = new Triangle();

то, во-первых, через ссылки g[0], g[1], g[2] мы не сможем вызывать этот метод и, во-вторых, даже если привести эти ссылки к типу дочернего класса, то как узнать, что в нем существует метод getSquare?

Мы могли бы относительно легко решить эту задачу, если бы в Java было множественное наследование, т.е. один дочерний класс можно было бы образовывать от нескольких базовых. Тогда достаточно было бы прописать еще один класс, например, MathGeom и указать его в качестве базового у классов Rectangle и Triangle. Но такого функционала в Java нет.

Как же все таки решить эту задачу? Здесь нам на помощь приходит еще одна конструкция ООП языка Java под названием интерфейс. Мы можем объявить MathGeom как интерфейс и подключить его к нужным нам классам.

Что же такое интерфейс и чем он отличается от классов? В целом, его можно воспринимать в виде абстрактного класса с набором, как правило, абстрактных методов (интерфейсов). Задается он ключевым словом interface, и в фигурных скобках идет список констант и методов:

[модификатор доступа] interface <имя интерфейса> {
       [константы;]
       [методы;]
}

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

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

В нашем случае MathGeom можно объявить как интерфейс, следующим образом:

interface MathGeom {
    double getSquare();
}

В нем идет определение одного единственного метода без реализации (в конце стоит точка с запятой) и возвращаемый тип double. Обратите внимание, у нас перед ключевым словом interface не указан никакой модификатор доступа, значит, используется модификатор по умолчанию и, как мы уже говорили, в этом случае интерфейс доступен только в рамках текущего проекта. Далее, всем методам без реализации внутри интерфейса автоматически применяется модификатор public, т.к. цель таких методов – публичный доступ из любого места программы.

Отлично, это мы сделали. Теперь нужно «привязать» интерфейс к выбранным классам (или, как говорят, реализовать интерфейс в классе), так как сам по себе он не может быть использован. Например, мы не можем создавать экземпляр интерфейса:

MathGeom m = new MathGeom();    // ошибка

Его можно лишь применять к тому или иному классу. Для этого используется следующий синтаксис:

class <имя класса> [extends <имя базового класса>] [implements <интерфейс1> [, <интерфейс2>, …, <интерфейсN>]] {
}

То есть, для подключения (реализации) интерфейсов следует писать ключевое слово implements и через запятую указывать интерфейсы, применяемые в текущем классе. В нашем случае это будет выглядеть так:

class Rectangle extends Geom implements MathGeom {
    void draw() {
        System.out.println("Рисование прямоугольника");
    }
 
    public double getSquare() {
        return 5*10;
    }
}
 
class Triangle extends Geom implements MathGeom {
    void draw() {
        System.out.println("Рисование треугольника");
    }
 
    public double getSquare() {
        return 0.5*4*10;
    }
}

Обратите внимание, после указания интерфейса MathGeom при объявлении класса, внутри него обязательно нужно определить метод getSquare, причем, сигнатура этого метода должна полностью совпадать с сигнатурой в интерфейсе MathGeom и стоять модификатор доступа public. Это строго обязательно. То же самое делается в классе Triangle.

Все, вот так мы объявили и применили интерфейс MathGeom к двум классам: Rectangle и Triangle. Причем, сделали это независимо от иерархии наследования классов. И, далее, благодаря этому общему интерфейсу, мы можем в функции main обращаться к методу getSquare единым образом, например, так:

        for(int i = 0;i < N; ++i)
            if( g[i] instanceof MathGeom ) {
                double s = ((MathGeom) g[i]).getSquare();
                System.out.println("i: " + i + ", s = " + s);
            }

Смотрите, здесь сначала идет проверка: определен ли интерфейс MathGeom в объекте g[i] и если да, то выполняется приведение типов к этому интерфейсу и через него уже вызывается метод getSquare, который в нем определен. Вот пример того, как можно использовать методы в конкретной задаче.

Интерфейс как тип данных

Давайте еще раз внимательно посмотрим вот на эту строчку:

double s = ((MathGeom) g[i]).getSquare();

Как я только что сказал, здесь происходит приведение типов от Geom к MathGeom. Но это значит, что имя интерфейса можно воспринимать как тип данных? Это действительно так. В частности, если добавить еще один интерфейс GeomInterface:

interface GeomInterface {
    double[] getCoords();
}

и применить его ко всем трем классам графических примитивов:

class Line extends Geom implements GeomInterface {
    void draw() {
        System.out.println("Рисование линии");
    }
 
    public double[] getCoords() {
        return new double[] {1, 2, 3, 4};
    }
}
 
class Rectangle extends Geom implements MathGeom, GeomInterface {
    void draw() {
        System.out.println("Рисование прямоугольника");
    }
 
    public double getSquare() {
        return 5*10;
    }
 
    public double[] getCoords() {
        return new double[] {10, 20, 30, 40};
    }
}
 
class Triangle extends Geom implements MathGeom, GeomInterface {
    void draw() {
        System.out.println("Рисование треугольника");
    }
 
    public double getSquare() {
        return 0.5*4*10;
    }
 
    public double[] getCoords() {
        return new double[] {11, 12, 13, 14};
    }
}

То в функции main, мы можем создать обобщенные ссылки типа GeomInterface:

        final int N = 3;
        GeomInterface g[] = new GeomInterface[N];
        g[0] = new Line();
        g[1] = new Rectangle();
        g[2] = new Triangle();

и через них обращаться к методу getCoords:

        for(int i = 0;i < N; ++i) {
            double coords[] = g[i].getCoords();
 
            for(int j = 0;j < coords.length; ++j)
                System.out.print(coords[j] + " ");
 
            System.out.println();
        }

Разумеется, ссылки g[0], g[1], g[2] будут иметь доступ только к методу getCoords, определенному в интерфейсе GeomInterface и ни к чему более. Также этот пример показывает, что класс может применять сразу несколько интерфейсов (в этом случае они записывают через запятую после ключевого слова implements). И тогда в нем следует определять методы из обоих интерфейсов.

Путь кодера

Подвиг 1. Реализовать интерфейс PersonInterface для единой работы с БД сотрудников. В этом интерфейсе объявить абстрактные методы:

  • getInfo() – для получения общей информации о сотруднике;
  • getStatus() – для получения информации о должности;
  • getFIO() – для получения ФИО сотрудника.

Объявить дочерние классы: Supervisers (для руководителей), Jobs (для рядовых сотрудников), Clients (для клиентов). В этих классах хранить информацию: ФИО, должность, год рождения, подразделение (если есть), телефон, адрес. Реализовать интерфейс PersonInterface с определением необходимых методов. Создать несколько экземпляров классов Supervisers, Jobs и Clients, используя обобщенные ссылки типа PersonInterface. Вызвать для этих объектов методы интерфейса и убедиться в их корректной работе.

Подвиг 2. Используя интерфейс и классы из подвига 1, добавить к классам базовый класс Persons для хранения общих полей: ФИО, год рождения, адрес. Кроме того, добавить две статические переменные: count и count_clients для подсчета числа сотрудников (классы Supervisers и Jobs) и клиентов организации (класс Clients). Создать несколько объектов, используя ссылки обобщенного типа PersonInterface. Вывести информацию по объектам, а также число сотрудников и клиентов.

Видео по теме