Интерфейс в Java это контракт, который описывает поведение (набор методов), не реализуя его. Классы, использующие ключевое слово
implements, обязуются реализовать все методы интерфейса. Это обеспечивает слабую связанность и позволяет обойти ограничение на множественное наследование, так как класс может реализовать сколько угодно интерфейсов. Сегодня мы поговорим о важном понятии в Java, интерфейсах.
Слово тебе наверняка знакомо. Например, интерфейсы есть у большинства компьютерных программ и игр. В широком смысле интерфейс это некий «пульт», который связывает две взаимодействующие друг с другом стороны.
Кратко
- Интерфейс это контракт: он описывает набор методов (поведение), но не реализует их.
- Класс подключает интерфейс словом
implements и обязан реализовать все его методы.
- Один класс может реализовать сколько угодно интерфейсов (множественного наследования классов в Java нет).
- Интерфейсы дают слабую связанность: в коде используют тип-интерфейс, а конкретная реализация подставляется во время работы.
- С Java 8 в интерфейсах можно писать методы с реализацией по умолчанию через ключевое слово
default.
- Многие коллекции это реализации интерфейсов:
ArrayList и LinkedList реализуют List, HashMap реализует Map.
Простой пример интерфейса из повседневной жизни это пульт от телевизора.
Он связывает два объекта, человека и телевизор, и выполняет разные задачи: прибавить или убавить звук, переключить каналы, включить или выключить телевизор.
Одной стороне (человеку) нужно обратиться к интерфейсу (нажать на кнопку пульта), чтобы вторая сторона выполнила действие. Например, чтобы телевизор переключил канал на следующий. При этом пользователю не обязательно знать устройство телевизора и то, как внутри него реализован процесс смены канала.
![Интерфейс как пульт: связывает пользователя и устройство, скрывая реализацию]()
Все, к чему пользователь имеет доступ, это
интерфейс. Главная задача это получить нужный результат.
Какое это имеет отношение к программированию и Java? Прямое :)
Создание и реализация интерфейса в Java
Создание интерфейса очень похоже на создание обычного класса, только вместо слова
class мы указываем слово
interface.
Давай посмотрим на простейший Java-интерфейс, и разберемся, как он работает и для чего нужен:
public interface Swimmable {
public void swim();
}
Мы создали интерфейс
Swimmable, «
умеющий плавать». Это что-то вроде нашего пульта, у которого есть одна «кнопка»: метод
swim(), «плыть».
Как же нам этот «
пульт» использовать?
Для этого метод, т.е. кнопку нашего пульта, нужно имплементировать. Чтобы использовать интерфейс, его методы должны реализовать какие-то классы нашей программы.
Давай придумаем класс, объекты которого подойдут под описание «умеющий плавать». Например, подойдет класс утки,
Duck:
public class Duck implements Swimmable {
public void swim() {
System.out.println("Уточка, плыви!");
}
public static void main(String[] args) {
Duck duck = new Duck();
duck.swim();
}
}
Что же мы здесь видим?
Класс
Duck «связывается» с интерфейсом
Swimmable при помощи ключевого слова
implements. Если помнишь, мы использовали похожий механизм для связи двух классов в
наследовании, только там было слово «
extends».
«
public class Duck implements Swimmable» можно для понятности перевести дословно: «публичный класс
Duck реализует интерфейс
Swimmable».
Это значит, что класс, связанный с каким-то интерфейсом, должен реализовать все его методы.
Обрати внимание: в нашем классе
Duck прямо как в интерфейсе
Swimmable есть метод
swim(), и внутри него содержится какая-то логика.
Это обязательное требование. Если бы мы просто написали «
public class Duck implements Swimmable» и не создали бы метод
swim() в классе
Duck, компилятор выдал бы нам ошибку:
Duck is not abstract and does not override abstract method swim() in Swimmable
Почему так происходит?
Если объяснять ошибку на примере с телевизором, получится, что мы даем человеку в руки пульт с кнопкой «переключить канал» от телевизора, который не умеет переключать каналы.
Тут уж нажимай на кнопку сколько влезет, ничего не заработает. Пульт сам по себе не переключает каналы: он только дает сигнал телевизору, внутри которого реализован сложный процесс смены канала.
Так и с нашей уткой: она должна уметь плавать, чтобы к ней можно было обратиться с помощью интерфейса
Swimmable.
Если она этого не умеет, интерфейс
Swimmable не свяжет две стороны, человека и программу. Человек не сможет использовать метод
swim(), чтобы заставить объект
Duck внутри программы плыть.
Теперь ты увидел более наглядно, для чего нужны интерфейсы.
Интерфейс описывает поведение, которым должны обладать классы, реализующие этот интерфейс. «Поведение» это совокупность методов.
Если мы хотим создать несколько мессенджеров, проще всего сделать это, создав интерфейс
Messenger. Что должен уметь любой мессенджер? В упрощенном виде, принимать и отправлять сообщения.
public interface Messenger{
public void sendMessage();
public void getMessage();
}
И теперь мы можем просто создавать наши классы-мессенджеры, имплементируя этот интерфейс. Компилятор сам «заставит» нас реализовать их внутри классов.
Telegram:
public class Telegram implements Messenger {
public void sendMessage() {
System.out.println("Отправляем сообщение в Telegram!");
}
public void getMessage() {
System.out.println("Читаем сообщение в Telegram!");
}
}
WhatsApp:
public class WhatsApp implements Messenger {
public void sendMessage() {
System.out.println("Отправляем сообщение в WhatsApp!");
}
public void getMessage() {
System.out.println("Читаем сообщение в WhatsApp!");
}
}
Viber:
public class Viber implements Messenger {
public void sendMessage() {
System.out.println("Отправляем сообщение в Viber!");
}
public void getMessage() {
System.out.println("Читаем сообщение в Viber!");
}
}
Какие преимущества это дает? Самое главное из них это слабая связанность.
Представь, что мы проектируем программу, в которой у нас будут собраны данные клиентов. В классе
Client обязательно нужно поле, указывающее, каким именно мессенджером клиент пользуется.
Без интерфейсов это выглядело бы странно:
public class Client {
private WhatsApp whatsApp;
private Telegram telegram;
private Viber viber;
}
Мы создали три поля, но у клиента запросто может быть всего один мессенджер. Просто мы не знаем какой. И чтобы не остаться без связи с клиентом, приходится «заталкивать» в класс все возможные варианты. Получается, один или два из них всегда будут
null, и они вообще не нужны для работы программы.
Вместо этого лучше использовать наш интерфейс:
public class Client {
private Messenger messenger;
}
Это и есть пример «слабой связанности»! Вместо того, чтобы указывать конкретный класс мессенджера в классе
Client, мы просто упоминаем, что у клиента есть мессенджер. Какой именно, определится в ходе работы программы.
Практическая демонстрация
Чтобы увидеть, как это работает на практике, давай немного доработаем наш класс
Client и создадим отдельный класс для запуска. Это как раз тот наглядный пример с
public static void main, который покажет всю мощь интерфейсов.
1. Модифицированный класс Client
Добавим в наш класс
Client методы, которые позволят устанавливать мессенджер и использовать его.
public class Client {
private Messenger messenger;
public void setMessenger(Messenger messenger) {
this.messenger = messenger;
}
public void sendMessage() {
if (messenger != null) {
messenger.sendMessage();
} else {
System.out.println("Мессенджер не установлен!");
}
}
}
2. Демонстрационный класс для запуска
Теперь создадим класс с методом
main, из которого будем управлять нашим объектом
Client.
public class AppDemo {
public static void main(String[] args) {
Client client = new Client();
System.out.println("--- Клиент устанавливает Telegram ---");
client.setMessenger(new Telegram());
client.sendMessage();
System.out.println("\n--- Клиент переключается на WhatsApp ---");
client.setMessenger(new WhatsApp());
client.sendMessage();
System.out.println("\n--- Клиент пробует Viber ---");
client.setMessenger(new Viber());
client.sendMessage();
}
}
Вывод
--- Клиент устанавливает Telegram ---
Отправляем сообщение в Telegram!
--- Клиент переключается на WhatsApp ---
Отправляем сообщение в WhatsApp!
--- Клиент пробует Viber ---
Отправляем сообщение в Viber!
Как видишь, мы создали всего один объект
client. Затем, не меняя код самого класса
Client, мы последовательно «подключали» к нему разные мессенджеры. Код клиента прекрасно работает с любым из них, потому что ему важно только одно: чтобы объект соответствовал «контракту» интерфейса
Messenger и имел метод
sendMessage().
Интерфейсы против наследования: в чем разница?
Но зачем нам для этого именно интерфейсы? Зачем их вообще добавили в язык?
Вопрос хороший и правильный! Того же результата можно добиться с помощью обычного наследования, так ведь?
Класс
Messenger родительский, а
Viber,
Telegram и
WhatsApp наследники. Действительно, можно и так.
Но есть одна загвоздка. Как ты уже знаешь, множественного наследования в Java нет. А вот множественная реализация интерфейсов есть. Класс может реализовывать сколько угодно интерфейсов.
Представь, что у нас есть класс
Smartphone, у которого есть поле
Application, установленное на смартфоне приложение.
public class Smartphone {
private Application application;
}
Приложение и мессенджер, конечно, похожи, но все-таки это разные вещи. Мессенджер может быть и мобильным, и десктопным, в то время как Application это именно мобильное приложение.
Так вот, если бы мы использовали наследование, не смогли бы добавить объект
Telegram в класс
Smartphone. Ведь класс
Telegram не может наследоваться одновременно от
Application и от
Messenger! А мы уже успели унаследовать его от
Messenger, и в таком виде добавить в класс
Client.
Но вот реализовать оба интерфейса класс
Telegram запросто может! Поэтому в классе
Client мы сможем внедрить объект
Telegram как
Messenger, а в класс
Smartphone как
Application. Вот как это делается:
public class Telegram implements Application, Messenger {
}
public class Client {
private Messenger messenger;
public Client() {
this.messenger = new Telegram();
}
}
public class Smartphone {
private Application application;
public Smartphone() {
this.application = new Telegram();
}
}
Теперь мы используем класс
Telegram как захотим. Где-то он будет выступать в роли
Application, где-то в роли
Messenger.
Коротко разницу между наследованием и интерфейсами можно свести в таблицу:
| Критерий | Наследование (extends) | Интерфейсы (implements) |
| Множественность | только один родитель | сколько угодно интерфейсов |
| Что описывает | и поведение, и готовую реализацию (код родителя) | контракт (набор методов), без реализации |
| Ключевое слово | extends | implements |
| Реализация методов | наследуется от родителя | класс реализует сам (кроме default-методов с Java 8) |
Методы в интерфейсах
Наверняка ты уже обратил внимание, что методы в интерфейсах всегда «пустые», то есть они не имеют реализации.
Причина этого проста: интерфейс описывает поведение, а не реализует его.
«Все объекты классов, имплементирующих интерфейс
Swimmable, должны уметь плавать»: вот и все, что говорит нам интерфейс. Как там конкретно будет плавать рыба, утка или лошадь, вопрос к классам
Fish,
Duck и
Horse, а не к интерфейсу. Так же как переключение канала это задача телевизора. Пульт просто предоставляет тебе кнопку для этого.
Впрочем, в Java8 появилось интересное дополнение, методы по умолчанию (default method).
Например, в твоем интерфейсе есть 10 методов. 9 из них реализованы по-разному в разных классах, но один реализован одинаково у всех. Раньше, до выхода Java8, методы внутри интерфейсов вообще не имели реализации: компилятор сразу выдавал ошибку. Теперь же можно сделать вот так:
public interface Swimmable {
public default void swim() {
System.out.println("Плыви!");
}
public void eat();
public void run();
}
Используя ключевое слово
default, мы создали в интерфейсе метод с реализацией по умолчанию. Два других метода,
eat() и
run(), нам необходимо будет реализовать самим во всех классах, которые будут имплементировать
Swimmable. С методом
swim() этого делать не нужно: реализация будет во всех классах одинаковой.
Кстати, ты уже не раз сталкивался с интерфейсами в прошлых задачах, хоть и не замечал этого сам :) Вот очевидный пример:
![Иерархия коллекций Java: интерфейсы List, Set, Map и их реализации]()
Ты работал с интерфейсами
List и
Set! Точнее, с их реализациями:
ArrayList,
LinkedList,
HashSet и прочими.
На этой же схеме видно пример, когда один класс реализует сразу несколько интерфейсов. Например,
LinkedList реализует интерфейсы
List и
Deque (двусторонняя очередь).
Ты знаком и с интерфейсом
Map, а точнее, с его реализацией,
HashMap.
Кстати, на этой схеме ты можешь увидеть одну особенность: интерфейсы могут быть унаследованы друг от друга. Интерфейс
SortedMap унаследован от
Map, а
Deque наследуется от очереди
Queue. Это нужно, если ты хочешь показать связь интерфейсов между собой, но при этом один интерфейс является расширенной версией другого.
Давай рассмотрим пример с интерфейсом
Queue, очередь. Мы пока не проходили коллекции
Queue, но они достаточно простые и устроены как обычная очередь в магазине.
Добавлять элементы можно только в конец очереди, а забирать только из начала. На определенном этапе разработчикам понадобился расширенный вариант очереди, чтобы добавлять и получать элементы можно было с обеих сторон. Так создали интерфейс
Deque, двустороннюю очередь. В нем присутствуют все методы обычной очереди, ведь она является «родителем» двусторонней, но при этом добавлены новые методы.
Наглядно связь основных интерфейсов коллекций с их реализациями можно свести в таблицу:
| Интерфейс | Что описывает | Реализации |
List | список с доступом по индексу | ArrayList, LinkedList |
Set | множество без повторов | HashSet |
Map | пары ключ-значение | HashMap |
Queue / Deque | очередь / двусторонняя очередь | LinkedList |
Больше об интерфейсах можно прочитать в
документации Oracle (Interfaces).
Читайте также
Вопросы и ответы
Что такое интерфейс в Java?
Это контракт, описывающий набор методов (поведение), но без их реализации. Класс, реализующий интерфейс через
implements, обязан реализовать все его методы.
Чем интерфейс отличается от наследования?
От родителя (
extends) можно наследоваться только одного, а интерфейсов класс может реализовать сколько угодно. Наследование дает готовый код родителя, а интерфейс только описывает, какие методы должны быть.
Зачем нужны интерфейсы?
Для слабой связанности: код работает с типом-интерфейсом, а конкретная реализация подставляется в ходе программы. Это делает код гибким и легко заменяемым.
Как реализовать интерфейс?
Указать после имени класса ключевое слово
implements и имя интерфейса, а затем реализовать все его методы. Если метод не реализовать, компилятор выдаст ошибку.
Может ли класс реализовать несколько интерфейсов?
Да. В отличие от наследования классов (только один родитель), класс может реализовать любое число интерфейсов, перечислив их через запятую после
implements.
Что такое default-методы в интерфейсах?
С Java 8 в интерфейсе можно задать метод с реализацией по умолчанию через ключевое слово
default. Классы, реализующие интерфейс, могут не переопределять такой метод.