5. Абстрактные классы и интерфейсы. Механизм обратного вызова

Интерфейсы и абстрактные классы улучшают структуру кода и способствуют отделению интерфейса от реализации.

Абстрактный класс

Вернемся к примеру с классами фигур

class Shape {
    public String draw() {
        return null;
    }

    public String erase() {
        return null;
    }
}
    
class Circle extends Shape {
    @Override
    public String draw() {
        return "Рисуем круг";
    }

    @Override
    public String erase() {
        return "Стираем круг";
    }
}

Методы базового класса Shape всегда были "фиктивными". Попытка вызова метода из класса Shape привела бы к ошибке в программе. Такая логика написания кода связана с тем, что класс Shape нам нужен лишь для того, чтобы определить общий интерфейс всех классов, производных от него, а уже производные классы переопределяли эти методы и реализовывали их по-своему.

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

В языке Java для решения подобных задач применяются абстрактные методы. Абстрактный метод является незавершенным, он состоит только из объявления и не имеет тела. Приведем пример абстрактного метода:

abstract Shape draw();

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

Компилятор запрещает создавать объекты абстрактного класса.

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

Объявление класса как abstract не подразумевает, что все его методы должны быть абстрактными.

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

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

Интерфейс

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

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

Кроме этого, в отличие от абстрактного класса, интерфейс позволяет реализовать своего рода, множественное наследование.

Чтобы создать интерфейс, используйте ключевое слово interface вместо class. Как и в случае с классами, перед ключевым словом interface указывается модификатор доступа (public, protected и так далее). Интерфейс также может содержать поля, они автоматически являются статическими (static) константами (final).

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

В классе, который реализует интерфейс, реализуемые методы должны быть объявлены как public.

Неважно, приводите ли вы преобразование к "обычному" классу с именем Shape, к абстрактному классу Shape или к интерфейсу Shape - действие будет одинаковым.

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

Применение интерфейсных ссылок

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

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

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

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

Реализация механизма обратного вызова с помощью интерфейсов

Механизм обратного вызова (callback) является ключевым в программировании. При обратном вызове, программист задает действия, которые должны выполняться всякий раз, когда происходит некоторое событие. Например, можно задать действие, которое должно быть выполнено после нажатия на кнопку или при выборе определенного пункта меню.

Приведем небольшой пример. В стандартной библиотеке классов Java нам доступен класс javax.swing.Timer, который используется для отсчета интервалов времени.

Устанавливая таймер, мы задаем интервал времени и указываем, что должно произойти по его истечении. Как указать таймеру, что он должен делать по истечении времени?

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

Разумеется, таймер должен знать, какой метод объекта он должен вызвать и этот метод в объекте гарантированно должен быть реализован. Для этого таймеру нужно указать объект класса, который реализует интерфейс ActionListener. Этот интерфейс входит в состав стандартной библиотеки и выглядит следующим образом

public interface ActionListener extends EventListener {
    public void actionPerformed(ActionEvent e);
}

По истечении заданного интервала времени таймер вызывает метод actionPerformed() и передает ему объект класса Event (класс Event описывает событие в Java):

Как мы видим, конструктор класса Timer запрашивает задержку и объект, у которого будет вызван метод actionPerformed.

Создадим класс, который будет реализовывать интерфейс ActionListener

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

Создадим слушатель MessageDialogPoster и передадим этот объект таймеру

Запустим приложение и посмотрим на результат.

Обратите внимание, что метод actionPerformed принимает на вход объект класса ActionEvent. При вызове метода actionPerformed(), таймер передает в метод объект класса ActionEvent, который содержит различную информацию о событии. Таким образом, мы можем запрограммировать те или иные действия, в зависимости от параметров события.

Рассмотрим еще один пример, на этот раз будем использовать кнопку. Создадим объект окна, объект кнопки и добавим кнопку в окно.

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

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

В данном случае нас интересует метод addActionListener(), который принимает объект слушателя, который реализует интерфейс ActionListener

С помощью этого метода, мы передаем кнопке объект класса MouseClickHandler. Когда произойдет какое-то событие, кнопка возьмет этот переданный объект и вызовет метод actionPerformed() этого объекта.

Запустим приложение и нажмем кнопку.

Та

Last updated