На этом занятии
начнем рассматривать новую тему – интерфейсы. И прежде чем начинать о них
говорить, давайте рассмотрим задачу, где оправдано применение этих интерфейсов.
Как всегда, предположим, что у нас есть абстрактный базовый класс 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. Вывести
информацию по объектам, а также число сотрудников и клиентов.