9. Абстрактные классы и интерфейсы
Интерфейс и абстрактные классы улучшают структуру кода и способствует отделению интерфейса от реализации. В первую очередь необходимо рассмотреть понятие абстрактного класса, который является промежуточной ступенью между обычным классом и интерфейсом.
Абстрактные классы
Рассмотрим еще раз пример с фигурами из темы про полиморфизм.
Методы базового класса Shape
всегда были "фиктивными". Попытка вызова метода из класса Shape
привела бы к ошибке в программе. Это было связано с тем, что класс Shape
был нужен лишь для того, чтобы определить общий интерфейс всех классов, производных от него, а уже производные классы переопределяли эти методы и реализовывали их по-своему.
Класс Shape
определял базовую форму, общность всех производных классов. Такие классы как Shape называют абстрактными базовыми классами или просто абстрактными классами.
Если в программе определяется такой абстрактный базовый класс вроде Shape
, создание объектов такого класса практически всегда бессмысленно. Абстрактный класс создается для работы с набором классов через общий интерфейс. А если Shape
только выражает интерфейс, а создание объектов такого класса не имеет смысла, лучше всего запретить пользователю создавать такие объекты, так как он может ненароком создать объект этого класса и попытаться с ним работать, что приведет к ошибкам в программе.
В языке Java для решения подобных задач используют механизм абстрактных методов. Абстрактный метод является незавершенным; он состоит только из объявления и не имеет тела. Синтаксис объявления абстрактных методов выглядит следующим образом:
Класс, содержащий хотя бы один абстрактный метод, называется абстрактным классом. Такой класс также должен помечаться ключевым словом abstract
(в противном случае, компилятор выдает сообщение об ошибке):
На уровне языка и компилятора создать экземпляр абстрактного класса невозможно
Если вы объявите класс, производный от абстрактного класса, но хотите иметь возможность создания новых объектов, то вы должны переопределить все абстрактные методы базового абстрактного класса. Если это не будет сделано, то производный класс тоже останется абстрактным, и компилятор заставит пометить новый класс ключевым словом abstract
.
Объявление класса как abstract
не подразумевает, что все его методы должны быть абстрактными.
Абстрактный класс может иметь другие, не абстрактные методы, поля и даже конструкторы.
Механизм абстрактных классов и методов очень полезен, так как он позволяет подчеркнуть абстрактность сущности, снижает риск возникновения ошибок в коде, а также сообщает пользователю и компилятору, как следует с ним обходиться.
Кроме того, абстрактные классы играют полезную роль при рефакторинге, потому что они позволяют легко перемещать общие методы вверх по иерархии наследования.
Интерфейс
Ключевое слово interface
становится следующим шагом на пути к абстракции. Ключевое слово abstract
позволяет создать в классе один или несколько неопределенных методов - разработчик предоставляет часть интерфейса без реализации, которая должна предоставляться производными классами.
Ключевое слово interface
используется для создания классов, вообще не имеющих реализации. Создатель интерфейса определяет имена методов, списки аргументов и типы возвращаемы значений, но не тела методов. Интерфейс описывает форму, но не реализацию.
Начиная с Java 8, в язык были добавлены различные механизмы для интерфейса - методы по умолчанию, статические и приватные методы, а также константы.
Это, безусловно, облегчает повседневную жизнь разработчику, но затрудняет изучение Java. Поэтому мы будет рассматривать механизм интерфейсов "в чистом виде", без упоминания различных дополнительных возможностей.
Ключевое слово interface
фактически означает - именно так должны выглядеть все классы, которые реализуют данный интерфейс. Поэтому любой код, использующий конкретный интерфейс, знает только то, какие методы вызываются для этого интерфейса, но не более того. Интерфейс определяет своего рода "протокол взаимодействия" между классами.
Кроме этого, в отличие от абстрактного класса, интерфейс позволяет реализовать своего рода, множественное наследование.
Чтобы создать интерфейс, используйте ключевое слово interface
вместо class
. Как и в случае с классами, перед словом interface
указывается модификатор доступа. Интерфейс также может содержать поля, они автоматически являются статическими (static
) и неизменяемыми (final
).
Обратите внимание, что мы не указываем для методов модификатор - все методы интерфейса являются public
.
Для создания класса, реализующего определенный интерфейс (или группу интерфейсов), используется ключевое слово implements
. Фактически это означает "интерфейс определяет форму, а данный класс определяет, как это будет реализовано".
В классе, который реализует интерфейс, реализуемые методы должны быть объявлены как public
.
Неважно, приводите ли вы преобразование к "обычному" классу с именем Shape
, к абстрактному классу Shape
или к интерфейсу Shape
- действие будет одинаковым.
Когда метод работает с классом вместо интерфейса, мы ограничены использованием базового класса и его подклассами. Это исключает возможность использовать метод для класса, который не входит в эту иерархию. Интерфейс, в значительной степени ослабляет это ограничение. В результате код становится более универсальным.
Применение интерфейсных ссылок
В Java можно объявлять переменные ссылочного интерфейсного типа, то есть переменные, хранящие ссылки на интерфейс. Такая переменная может ссылаться на любой объект, реализующий ее интерфейсный тип. При вызове метода для объекта по интерфейсной ссылке выполняется вариант этого метода, реализованный в классе данного объекта. Этот процесс аналогичен применению ссылки на суперкласс для доступа к объекту подкласса.
Реализация нескольких интерфейсов
Так как интерфейс по определению не имеет реализации, нет ничего что могло бы помешать совмещению нескольких интерфейсов. При объявлении класса, который совмещает несколько интерфейсов, имена интерфейсов перечисляются вслед за ключевым словом implements
и разделяются запятыми. Класс обязан предоставить реализацию для всех методов интерфейсов, которые он реализует.
Наследование интерфейсов
Многие языки программирования, в том числе Java, позволяют наследовать интерфейс один от другого. Синтаксис наследования интерфейсов аналогичен синтаксису наследования классов. Когда класс реализует интерфейс, он обязан реализовать все методы, определенные по цепочке наследования интерфейсов.
Частичные реализации
Если класс включает в себя интерфейс, но не полностью реализует определенные в нем методы, он должен быть объявлен как abstract
Реализация механизма обратного вызова с помощью интерфейса
Механизм обратного вызова широко распространен в программировании. При обратном вызове программист задает действия, которые должен выполнять всякий раз, когда происходит некоторое событие. Например, можно задать действие, которое должно быть выполнено после клика на кнопку или при выборе определенного пункта меню.
Приведем небольшой пример. В Java нам доступен класс Timer
, который используется для отсчета интервала времени.
Устанавливая таймер, мы задаем интервал времени и указываем, что должно произойти по его истечении. Как указать таймеру, что он должен делать по истечении времени?
В ООП для таких случаев существует механизм обратного вызова. Он заключается в том, что программист должен передать таймеру объект некоторого класса. После этого таймер вызывает один из методов данного объекта.
Разумеется, таймер должен знать, какой метод объекта он должен вызвать и должен иметь гарантию, что в классе объекта реализован этот метод. Для этого таймеру нужно указать объект класса, который реализует интерфейс ActionListener
. Этот интерфейс выглядит следующим образом
По истечении заданного интервала времени таймер обращается к объекту, вызывает метод actionPerformed()
и передает ему объект класса Event
(класс Event
описывает событие в Java).
Как мы видим, конструктор класса Timer
запрашивает задержку и объект, у которого будет вызван метод actionPerformed
. Создадим класс, который будет реализовывать интерфейс ActionListener
Как мы видим, данный класс ничего кроме реализации интерфейса не делает. То есть он нужен только для одной цели - он содержит метод, который будет вызван таймером после задержки. Такие классы и их объекты называют слушателем.
Слушатель - это объект, который как бы "слушает" события, которые происходят с другим объектом. Когда это "слушаемое" событие происходит, вызывается указанный в интерфейсе метод этого объекта.
Создадим объект слушателя TimerAction
и передадим этот объект таймеру
Запустим приложение и посмотрим на результат
Обратите внимание, что метод actionPerformed()
принимает на вход объект класса ActionPerformed
. При вызове метода actionPerformed()
, таймер передает в метод объект класса ActionEvent
, который содержит различную информацию о событии. Таким образом, мы можем запрограммировать те или иные действия в зависимости от параметров события. Рассмотрим еще один пример, на этот раз будем использовать кнопку.
Создадим объект окна, объект кнопки и добавим кнопку в окно
По умолчанию, при нажатию на кнопку ничего не происходит. По аналогии с таймером нам необходимо передать кнопке слушатель, который реализует определенный интерфейс. Кнопка - гораздо более сложный объект, чем просто таймер, поэтому в связи с кнопкой может произойти очень много событий. Для каждого типа событий кнопка принимает свой слушатель, который реализует свой определенный интерфейс
С помощью такого многообразия интерфейсов мы можем обработать самые разнообразные события, которые могут случиться с кнопкой.
В данном случае, нас интересует метод addActionListener(ActionListener l)
, который принимает на вход слушатель, который реализует интерфейс ActionListener
.
С помощью этого метода мы передаем кнопке объект класса ButtonListener
. Когда произойдет какое-то событие, кнопка обратится к переданному объекту и вызовет метод actionPerformed()
этого объекта.
Такая реализация в большинстве случаев нас устраивает. Но что, если нужно обработать какое-то специфическое событие и получить детальную информацию. Например, что если мы хотим, чтобы кнопка вела себя по разному при нажатии левой и правой кнопок мыши?
Для этого у кнопки необходимо вызвать метод addMouseListener(MouseListener l)
и передать ему слушатель, который реализует интерфейс MouseListener
. Таким образом, мы обрабатываем не просто некоторое событие, а событие мыши. Событие мыши - более специфическое событие и поэтому нам доступна большая информация о событии.
Создадим слушатель, который реализует интерфейс MouseListener
Обратим внимание на две особенности:
1) интерфейс MouseListener
определяет уже несколько методов, а не один, как ActionListener
. Как видите, мы можем запрограммировать реакцию на очень специфические события, например, если курсор мыши вошел в область, которую занимает кнопка. Так как нас интересует только события клика, все остальные методы у нас имеют пустую реализацию. То есть, например, курсор мыши войдет в область кнопки, то кнопка вызовет пустой метод mouseEntered()
и ничего не произойдет (поищите информацию о классе MouseAdapter
, который упрощает работу с событиями мыши);
2) в методы передается объект класс MouseEvent
. Объект класса MouseEvent
обладает информацией о событии мыши, что дает нам возможность узнать о событии много подробностей.
Мы видим, что мы можем, например, с помощью метод getButton()
узнать - какой кнопкой было произведено нажатие, с помощью методов getX()
и getY()
узнать координаты нажатия и так далее. Создадим объект слушателя MouseListener
и передадим этот объект кнопке
Запустим приложение и нажмем кнопку левой кнопкой мыши.
Last updated
Was this helpful?