Интерфейсы
Чему вы научитесь:
- описывать умение интерфейсом и реализовывать его в классах через
implements; - объяснять, когда выполняется
default-метод, и выбирать между интерфейсом и абстрактным классом; - заводить фиксированный набор значений через
enumи разбирать его вswitch; - описывать неизменяемые данные одной строкой
recordи сравнивать записи черезequals.
Представьте объявление о работе: «Требуется курьер. Нужно уметь доставить посылку». В объявлении не сказано, кем должен быть курьер. Работу может взять студент на велосипеде, водитель фургона и даже дрон: подходит любой, кто умеет делать то, что перечислено.
Тому, кто отправляет посылку, тоже неважно, кто за ней пришёл. Он говорит курьеру «доставь», а как доставлять, решает сам курьер.
Зачем такое нужно в программе? В прошлой теме общие кнопки классам давал родитель. Но у велосипедиста и дрона нет ничего общего, кроме умения доставлять: ни общих полей, ни общего предка. К тому же родитель у класса может быть только один. Для умения, которое бывает у совсем разных классов, в Java есть интерфейс:
public class Main {
public static void main(String[] args) {
Courier c = new Cyclist();
c.deliver("book"); // Cyclist brings book
}
}
interface Courier { void deliver(String parcel); }
class Cyclist implements Courier {
public void deliver(String parcel) {
System.out.println("Cyclist brings " + parcel);
}
}Давайте разберём программу по шагам:
interface Courier— объявление о работе. В фигурных скобках только заголовок методаdeliver, а вместо тела стоит точка с запятой: интерфейс говорит, что нужно уметь, но не говорит как.class Cyclist implements Courier— велосипедист берёт эту работу. Словоimplementsобещает, что в классе есть каждый метод интерфейса.- Внутри
Cyclistметодdeliverнаписан целиком, с телом. Courier c = new Cyclist();заводит переменную типаCourierи кладёт в неё ссылку на объектCyclist.c.deliver("book")выполняет метод велосипедиста и печатаетCyclist brings book.
Интерфейс и implements
Интерфейс — это тип, который перечисляет методы, но не пишет их тела. Методы без тела вы уже встречали в абстрактных классах: в интерфейсе они тоже абстрактные. Класс реализует интерфейс, когда пишет у себя каждый его метод, и объявляет об этом словом implements.
Что важно знать об интерфейсе:
- метод без тела в интерфейсе всегда
public, даже если слово не написано, поэтому и в классе его объявляютpublic; - объект интерфейса создать нельзя: на
new Courier()компилятор ответитCourier is abstract; cannot be instantiated; - компилятор проверяет, что класс написал все методы интерфейса, и если хоть одного нет, программа не скомпилируется.
Вот что javac сообщает о классе Cyclist, в котором забыли метод deliver:
error: Cyclist is not abstract and does not override abstract method deliver(String) in CourierНачало сообщения, is not abstract, не случайно: абстрактному классу из прошлой темы разрешено оставить метод своим наследникам. Обычный класс обязан написать всё.
Над такими методами можно ставить @Override, как в прошлой теме. Тогда компилятор проверит, что метод с таким заголовком действительно есть в интерфейсе.
Несколько интерфейсов
Один человек может работать и курьером, и фотографом. Так и класс может реализовать сколько угодно интерфейсов: их перечисляют после implements через запятую. Дрон умеет и доставлять, и снимать сверху:
interface Camera { String photo(); }
class Drone implements Courier, Camera {
public void deliver(String parcel) {
System.out.println("Drone drops " + parcel);
}
public String photo() {
return "view from above";
}
}Родитель через extends у класса по-прежнему один, а интерфейсов — сколько нужно. Если есть и то и другое, сначала пишут extends, потом implements: class Van extends Vehicle implements Courier, Camera.
Этот вопрос часто задают на собеседовании: есть ли в Java множественное наследование? Ответ: класс наследует только один класс, но может реализовать несколько интерфейсов.
Переменная типа интерфейса
Переменная Courier c работает как универсальный пульт из прошлой темы: её тип решает, какие кнопки есть, а объект решает, что они делают. В переменную типа Courier можно положить объект любого класса, который реализует Courier:
Courier first = new Cyclist();
Courier second = new Drone();
first.deliver("pizza"); // Cyclist brings pizza
second.deliver("pizza"); // Drone drops pizzaРазберём по строкам:
- В
firstлежит велосипедист, а вsecond— дрон, но тип у обеих переменных один:Courier. first.deliver("pizza")выполняет метод классаCyclist.second.deliver("pizza")записан так же, а работу делает другой объект: выполняется метод классаDrone.
У пульта Courier одна кнопка — deliver. Поэтому second.photo() не скомпилируется, хотя в second лежит дрон: компилятор ответит cannot find symbol. Чтобы снять фото, нужна переменная типа Camera.
Здесь картинка с объявлением неточна. Настоящий работодатель может взять человека, который умеет только половину, в надежде, что тот научится. Компилятор так не делает: класс получает работу, только если умеет всё, что перечислено.
Частая ошибка
В классе забыли написать public перед методом интерфейса:
class Cyclist implements Courier {
void deliver(String parcel) { // ✗
System.out.println("Cyclist brings " + parcel);
}
}error: deliver(String) in Cyclist cannot implement deliver(String) in CourierПрограмма не компилируется. Метод интерфейса — public, а метод без модификатора виден только классам своего пакета. Класс не может сузить доступ, который обещал интерфейс. Следующей строкой javac это поясняет: attempting to assign weaker access privileges; was public. Допишите public перед void deliver.
Главное
- Интерфейс перечисляет методы без тела, а класс реализует его через
implementsи пишет каждый метод сам, сpublic. - Если класс не написал хоть один метод интерфейса, программа не скомпилируется.
- Родитель через
extendsу класса один, а интерфейсов можно реализовать сколько угодно. - Переменная типа интерфейса принимает объект любого класса, который его реализует, и даёт вызвать только методы интерфейса.