JavaRush /Java блог /Java Developer /Интерфейсы в Java
Автор
Aditi Nawghare
Инженер-программист в Siemens

Интерфейсы в Java

Статья из группы Java Developer
Привет! Сегодня поговорим о важном понятии в Java — интерфейсы. Слово тебе наверняка знакомо. Например, интерфейсы есть у большинства компьютерных программ и игр. В широком смысле интерфейс — некий «пульт», который связывает две взаимодействующие друг с другом стороны. Простой пример интерфейса из повседневной жизни — пульт от телевизора. Он связывает два объекта, человека и телевизор, и выполняет разные задачи: прибавить или убавить звук, переключить каналы, включить или выключить телевизор. Одной стороне (человеку) нужно обратиться к интерфейсу (нажать на кнопку пульта), чтобы вторая сторона выполнила действие. Например, чтобы телевизор переключил канал на следующий. При этом пользователю не обязательно знать устройство телевизора и то, как внутри него реализован процесс смены канала. Для чего в Java нужны интерфейсы - 1Все, к чему пользователь имеет доступ — это интерфейс. Главная задача — получить нужный результат. Какое это имеет отношение к программированию и 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, мы просто упоминаем, что у клиента есть мессенджер. Какой именно — определится в ходе работы программы. Но зачем нам для этого именно интерфейсы? Зачем их вообще добавили в язык? Вопрос хороший и правильный! Того же результата можно добиться с помощью обычного наследования, так ведь? Класс 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. Наверняка ты уже обратил внимание, что методы в интерфейсах всегда «пустые», то есть они не имеют реализации. Причина этого проста: интерфейс описывает поведение, а не реализует его. «Все объекты классов, имплементирующих интерфейс 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 нужны интерфейсы - 2Ты работал с интерфейсами List и Set! Точнее, с их реализациями — ArrayList, LinkedList, HashSet и прочими. На этой же схеме видно пример, когда один класс реализует сразу несколько интерфейсов. Например, LinkedList реализует интерфейсы List и Deque (двусторонняя очередь). Ты знаком и с интерфейсом Map, а точнее, с его реализаций — HashMap. Кстати, на этой схеме ты можешь увидеть одну особенность: интерфейсы могут быть унаследованы друг от друга. Интерфейс SortedMap унаследован от Map, а Deque наследуется от очереди Queue. Это нужно, если ты хочешь показать связь интерфейсов между собой, но при этом один интерфейс является расширенной версией другого. Давай рассмотрим пример с интерфейсом Queue — очередь. Мы пока не проходили коллекции Queue, но они достаточно простые и устроены как обычная очередь в магазине. Добавлять элементы можно только в конец очереди, а забирать — только из начала. На определенном этапе разработчикам понадобился расширенный вариант очереди, чтобы добавлять и получать элементы можно было с обеих сторон. Так создали интерфейс Deque — двустороннюю очередь. В нем присутствуют все методы обычной очереди, ведь она является «родителем» двусторонней, но при этом добавлены новые методы.
Комментарии (217)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Максим Li Уровень 36
12 ноября 2023
Всё понятно!
Alexander Minaev Уровень 27
27 августа 2023
Не понял, почему вместо абстрактного класса messenger мы делаем интерфейс.. У всех плюс минус поля одинаковые, можно так же наделать абстрактных методов и реализовать в каждом классе.. Если только они уже не наследуются от чего то более важного, с другой стороны, можно тот же абстрактный класс так же наследовать, что бы в объектах было меньше дублирующегося кода.. Кажется, что пример с мессенджерами не очень удачный
chess.rekrut Уровень 25
21 августа 2023
easy
Rustam Уровень 35 Student
31 июля 2023
Зашел почитать толковые инсайты, а тут все просят лайки... Ставь лайк, если столкнулся с тем же!
Alexander Rozenberg Уровень 32
25 июля 2023
fine
Эмиль Уровень 16
13 июля 2023
поменяйте кнопку swim на пульте на sound, не те ассоциации
Ислам Уровень 33
5 июня 2023
Nice
Oraz Janov Уровень 34
5 апреля 2023
Если у кого-то проблемы с пониманием отличия Абстрактных классов от интерфейсов можете почитать здесь. Мне очень помогло
bazilradehiv1994 Уровень 20
16 марта 2023
дуже гарно написано
theylovevalera Уровень 51
6 марта 2023
Можно лайк будь-ласка?)