JavaRush /Java блог /Random /Интерфейсы для тех, кому "очень интересно, но ничего не п...
Lilly
25 уровень
Москва

Интерфейсы для тех, кому "очень интересно, но ничего не понятно"

Статья из группы Random
Всем привет! Данный пост написан, скорее, для тех, кто уже видел и даже решил на 12 уровне (2 уровень Java Core) несколько задач на Интерфейсы, но всё еще задаётся вопросом: "Так зачем же они нужны, если в них все равно нужно реализовывать методы, объявленные в интерфейсе??? То есть, они же не сокращают количество кода!!!". Не сомневаюсь, что эта тема раскрывается в последующих лекциях, и раскрываются более глубокие смыслы и варианты использования интерфейсов, но если вам совсем невтерпёж, то милости прошу. Изначально я хотела написать комментарий, но поняла, что это будет ооочень длинный комментарий, поэтому мы здесь. Это моя первая статья, разумную критику приветствую. Если я где-то не права - можете смело поправлять. Итак, как мы знаем, при создании своего класса мы можем наследоваться только от одного абстрактного или не абстрактного класса. Но при этом в нашем классе можно реализовывать довольно большое количество интерфейсов. При наследовании нужно руководствоваться здравым смыслом и в потомки записывать всё же родственные сущности (не будем же мы наследовать котика от бульдозера и наоборот). С интерфейсами логика немного иная: здесь важны не сами "сущности", а их "умения" или что можно с ними делать. Например, и котик, и бульдозер могут перемещаться в пространстве. То есть, в принципе, могут реализовать интерфейс CanMove или Moveable. Или же человек и кот. Они оба могут пить, только делают это по-разному: человек пьёт чай или кофе из чашки, а кот лакает водичку или молоко из миски. Но в целом они оба пьют, так что у каждого из них можно сделать свою реализацию интерфейса CanDrink. Какая нам от этого польза? Представьте, что вы делаете игру. Есть у вас, допустим, локация: река, с обеих сторон от нее берега, а дальше лес и горы. На берегу отдыхают разные виды живых существ. Внезапно приближается наводнение. Все, кто может летать - улетают. Кто не может лететь, но может бежать - бегут. Кто-то умеет плавать, так что им в принципе всё равно на это ваше наводнение (ну или они смогут выплыть на берег), хотя, некоторые из них сначала могут попытаться убежать (если умеют). Остальные, как это ни грустно, погибнут. Попробуем это реализовать. (Не пугайтесь сразу большого количества кода =) Там бОльшая часть - комментарии) Какие классы нам могут понадобиться? Начнем с абстрактного класса разных персонажей или юнитов (извините, не сильна в игровой терминологии, если ошибаюсь - поправьте). Пусть это будет класс Unit. Будем считать, что абсолютно все юниты могут перемещаться в пространстве и издавать звуки. Абстрактный класс Unit:

// Абстрактный класс для всех юнитов
public static abstract class Unit {
    // Базовый метод движения. 
    // По умолчанию (если не переопределено) будет у всех наследников.
    public void move (int x, int y) {
        System.out.println("Я ( " + getClassName() + " ) просто брожу по полю на " +
                           x + " метров вправо и " + y + " метров вперед");
    }

    // Абстрактный метод, который ДОЛЖЕН ПЕРЕОПРЕДЕЛИТЬ у себя
    // КАЖДЫЙ КЛАСС, унаследованный от Unit.
    public abstract void makeSound();

    // Вспомогательный метод получения имени класса
    // без всей лишней информации.
    public String getClassName() {
        return this.getClass().getSimpleName();
    }
}
Какие интерфейсы ("навыки") нужны нашим юнитам? Они могут бегать (CanRun), плавать (CanSwim) или летать (CanFly). Кто-то может обладать несколькими навыками сразу, а у некоторых несчастных может не быть ни одного.

// Интерфейсы. КАЖДЫЙ КЛАСС, "наследующий" какой-то интерфейс,
// ДОЛЖЕН РЕАЛИЗОВАТЬ его у себя.
interface CanRun {
    void run(String action);
}
interface CanSwim {
    void swim();
}
interface CanFly {
    void fly();
}
Дальше мы создаем классы-наследники абстрактного класса Unit. Путь это будет класс Человек (Human):

// Человек НАСЛЕДУЕТ абстрактный класс Unit,
// а также РЕАЛИЗУЕТ интерфейсы CanRun, CanSwim
public static class Human extends Unit implements CanRun, CanSwim {
    // Переопределяем метод public void makeSound()
    // родительского абстрактного класса Unit
    @Override
    public void makeSound() {
        System.out.print("Йу-хуу! ");
    }

    // РЕАЛИЗУЕМ метод public void run(String action) интерфейса CanRun
    @Override
    public void run(String action) {
        System.out.println("Я ( " + this.getClassName() + " ) " + action + 
                           " отсюда на двух ногах ");
    }

    // РЕАЛИЗУЕМ метод public void swim() интерфейса CanSwim
    @Override
    public void swim() {
        System.out.println("Я ( " + this.getClassName() + " ) " +
                           "с двумя руками и двумя ногами " +
                           "хорошо плаваю");
    }
}
Класс Птица (Bird). Да, я понимаю, что просто птиц не бывает, но для упрощения пусть будет просто птица, не будем делать ее абстрактной:

// Птица НАСЛЕДУЕТ абстрактный класс Unit,
// и РЕАЛИЗУЕТ интерфейс CanFly
public static class Bird extends Unit implements CanFly {
    // Переопределяем абстрактный метод public void makeSound()
    // родительского абстрактного класса Unit
    @Override
    public void makeSound() {
        System.out.print("Курлык! ");
    }

    // РЕАЛИЗУЕМ метод public void fly() интерфейса CanFly
    @Override
    public void fly() {
        System.out.println("Я ( " + this.getClassName() + " ) улетел отсюда");
    }
}
А теперь создаём еще один абстрактный класс Животные (Animal), который будет унаследован от Unit:

// Абстрактный класс Животных, НАСЛЕДУЕТ абстрактный класс Unit
public static abstract class Animal extends Unit {
    // тут могут быть какие-то данные и/или методы
}
И уже его наследники Барашек (Sheep):

// Баран НАСЛЕДУЕТ класс Animal,
// и РЕАЛИЗУЕТ интерфейс CanRun
public static class Sheep extends Animal implements CanRun {
    // Переопределяем абстрактный метод public void makeSound()
    // родительского абстрактного класса Unit
    @Override
    public void makeSound() {
        System.out.print("Беееее! ");
    }

    // РЕАЛИЗУЕМ метод public void run(String action) интерфейса CanRun
    @Override
    public void run(String action) {
        System.out.println("Я ( "+ this.getClassName() + " ) " + action +
                           " отсюда на четырёх копытах");
    }
}
и Ленивец (Sloth):

// Ленивец НАСЛЕДУЕТ класс Animal
// и внутри себя ПЕРЕОПРЕДЕЛЯЕТ метод
// void move(int x, int y) абстрактного класса Unit
public static class Sloth extends Animal {
    // Переопределяем абстрактный метод public void makeSound()
    // родительского абстрактного класса Unit
    @Override
    public void makeSound() {
        System.out.print("Зевает... ");
    }

	// Переопределяем метод public void move(int x, int y)
    // родительского абстрактного класса Unit
    @Override
    public void move(int x, int y) {
        System.out.println("Я ( "+ getClassName() + " ) очень медленный, поэтому " +
                           "переместился на " + (int)(x/12) + " вправо " +
                           "и на " + (int)(y/12) + " вперед");
    }
}
Метод move(int x, int y) у абстрактного класса Unit не является абстрактным, поэтому мы не обязаны его переопределять, но для Ленивца сделали небольшое замедление. Теперь переходим к действиям.

public static void main(String[] args) {
    // создаём список юнитов
    // и добавляем туда представителей разных классов
    List<Unit> units = new ArrayList<unit>();
    units.add(new Human());
    units.add(new Bird());
    units.add(new Sheep());
    units.add(new Sloth());

    // проходим в цикле по всем юнитам и вызываем метод move(int x, int y).
    // У всех, кроме ленивца, будет вызван метод базового класса.
    // У Ленивца будет вызван его переопределенный метод
    for (Unit unit : units) {
		// в самом начале ничего не происходит, юниты просто двигаются.
        unit.move((int)(Math.random()*50), (int)(Math.random()*50));
    }

    System.out.println("\n...Наводнение приближается....");
    int distanceOfFlood = (int)(Math.random()*1000); // Число от 0 до 1000
    System.out.println("...Наводнение на " + distanceOfFlood + " от берега...\n");

    // проходим в цикле по всем юнитам
    for (Unit unit : units) {
        unit.makeSound(); // у каждого юнита свой, переопределенный, звук
        // смотрим, кто что "умеет":
		// если юнит умеет летать 
        if(unit instanceof CanFly)
            ((CanFly) unit).fly(); // проблем нет, улетает
        // если юнит не умеет летать, но умеет бегать 
        else if(unit instanceof CanRun) {
			// смотрим на какое расстояние разлилась вода
            if(distanceOfFlood < 400) {
				// если меньше 400 (условно метров)
                ((CanRun) unit).run("убежал"); // юнит убежал
            }                
            else {
				// если больше 400, юнит безуспешно пытается убежать
                ((CanRun) unit).run("пытался убежать");
				// если юнит может не только бегать, но и плавать
                if (unit instanceof CanSwim) {
                    ((CanSwim) unit).swim(); // плывёт
                }
				// иначе умирает (он хотя бы пытался)
                else unitIsDead(unit);
            }
        }
		// если юнит не летает, не бегает, но может плавать				
        else if (unit instanceof CanSwim)
            ((CanSwim) unit).swim(); // плывёт
        else
			// если юнит не умеет совсем ничего - грустненько :(
            unitIsDead(unit); // умирает 
           
        System.out.println();
    }
}

public static void unitIsDead(Unit unit) {
    System.out.println("Извините, я ( " + unit.getClassName() + " ) умер");
}
Числовые литералы 12, 50, 400 и 1000 взяты навскидку, можно задать и другие, но логика, надеюсь, понятна. Итак, как мы можем увидеть, имея абстрактный родительский класс с общими методами, мы можем вообще не задумываться какого конкретно класса тот или иной юнит, а просто вызывать эти методы (makeSound() и move()). После первого прохода в цикле, когда у всех юнитов вызывается метод move(), на экран будет выведено следующее: Интерфейсы для тех кому "очень интересно, но ничего не понятно" - 1 Очевидно, что у всех объектов, кроме ленивца выведено стандартное сообщение из метода move() абстрактного родительского класса Unit, а у ленивца метод move() был переопределен. Однако, абстрактный класс не поможет нам узнать, что "умеет" тот или иной юнит. Тут как раз в дело вступают интерфейсы. С помощью instanceof мы узнаём, может ли этот юнит совершать те или иные действия (поддерживает ли он нужный нам интерфейс), и если да, используем знакомое уже нам приведение типов, например, с помощью ((CanFly) unit).fly() приводим наш объект типа Unit к типу интерфейса CanFly и вызываем у него метод fly(). И не имеет значения какой класс у нашего объекта, важно лишь то, что он в своём "резюме" указал способность летать. Мы ему и говорим: "Ты же умеешь, вот и лети! Нам всё равно как ты это сделаешь". То есть для нас, как для разработчиков, это значит, что классы, реализующие интерфейс CanFly, могут когда угодно и как угодно менять реализацию метода fly(), могут появляться новые классы, реализующие его или же, наоборот, разработчики могут удалить некоторые из старых классов. Но до тех пор, пока этот метод выполняет заявленные функции, и объект летит, нас это не волнует. Наш код, работающий с объектами, реализующими этот интерфейс, останется прежним, нам ничего не придётся изменять. После второго цикла, когда все пытаются спастись, в зависимости от масштаба наводнения, на экране мы увидим либо Интерфейсы для тех кому "очень интересно, но ничего не понятно" - 2 либо Интерфейсы для тех кому "очень интересно, но ничего не понятно" - 3 Без интерфейса нам бы пришлось проверять каждый объект на соответствие какому-то классу (а проверить бы пришлось все) и держать в голове навыки каждого конкретного класса. То есть вот перед нами сейчас барашек и он вроде бы умеет бегать. А если у вас таких персонажей несколько десятков или сотен разных видов (классов)? А если еще и написали их не Вы, а другой программист, так что вы даже понятия не имеете кто и что там умеет? Это было бы куда сложнее... И небольшое дополнение уже после публикации: В реальной жизни над проектом работаете не вы один. Допустим, вы делаете только логику. Все объекты, с которыми вы взаимодействуете, пишут другие программисты. Вы можете даже не знать все классы, с которыми работает ваш код. Вам нужно от них только то, чтобы они выполняли то, что вам надо. При этом все они это могут делать совершенно по-разному. Но вы, допустим, в своём коде делаете метод, который работает только с объектами классов, поддерживающих определённый интерфейс

void doSomething(CanFly f)
то есть параметром методa устанавливаете интерфейс. Не конкретный класс, а интерфейс. И все другие программисты, вызывая у себя этот ваш метод void doSomething(CanFly ) аргументами должны передать либо явно объект класса, реализующего CanFly, либо какой-то объект какого-то класса, который может быть к нему приведен:

Object obj = new Bird();
doSomething(obj); // ошибка компиляции Object cannot be converted to CanFly
doSomething((CanFly) obj); // нет ошибки, потому что obj у нас класса Bird и реализует CanFly

Bird b = new Bird();
doSomething(b); // нет ошибки

Human h = new Human() ;
doSomething(h); // ошибка компиляции
doSomething((CanFly) h); // ошибка времени выполнения ClassCastException
Вот так интерфейсы и могут быть полезны. И это далеко не все их возможности и способы применения. Дальше по курсу, наверняка, узнаем больше =) Спасибо, что дочитали до конца =)
Комментарии (30)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Denis Gritsay Уровень 35
18 ноября 2023
шикарная статья , просто супер. редкий случай, когда понятно ЗАЧЕМ, а не углубление в частности из за которых каша в голове
Rustam Уровень 35 Student
31 июля 2023
Очень понравилась статья! Хороший и оригинальный пример с лесом)
Yakov Bashkurov Уровень 19
9 ноября 2021
Шикарно. Спасибо за труды. Стало понятнее. ЗЫ: Ленивца очень жалко. Не знал, что в статье про язык программирования может быть столько драмы.
🦔 Виктор Уровень 20 Expert
26 декабря 2020
Спасибо за труды, очень интересно, но все ещё ничего не понятно... На мой вкус перегружено примерами с избыточной реализацией не по теме, под конец можно забыть, о чём статья. Если расчёт был действительно на тех, кому очень тяжело понять, то вышло как-то нагромождённо. По интерфейсам мне очень понравились следующие материалы: Для чего в Java нужны интерфейсы. В начале этой статьи хорошо и ёмко описывается суть интерфейсов в сравнении с абстрактными классами. На пальцах и картинках доходчиво объясняют, хорошо заходит на первых парах. На Metanit'е более серьёзно и детально рассмотрены интерфейсы с ёмкими примерами в одноимённой алтимейт статье. Anyway, всё относительно и индивидуально. Ещё раз спасибо за труд! Всё получится!
hidden #2322530 Уровень 41
21 декабря 2020
блин. а оказывается можно было уже с 10+ уровня писать статьи? ((( я ждал до 41, а пока дождался, уже передумал.
Mejhoks Уровень 16
19 декабря 2020
Я еще не дошел до уровня с интерфейсами, и если там плохо описана цель его существования, а тут более подробно - то даже не знаю что и думать. Я понимаю все что тут написано - оно написано очень доступно (не знаю как там по лекциям). И понятно как это работает и взаимодействует, но так и не понятно для чего увеличивать код... Мы создаем абстрактный интерфейс с одним лишь названием метода - две строчки кода, потом везде его еще дописываем в поле после класса родителя - еще два слова, и в каждом классе Оверрайдим его... А без оверрайда он пуст, то есть нужно заполнить или с него толку нет. Я бы понял если бы там было дефолтное значение и хоть для какого-то объекта не нужно было прописывать как он использует абстрактный метод. Тогда мы имели бы экономию при создание тонн объектов. Но он ведь пуст... Для чего его делать, если всем объектам которым он нужен - нужно буквально с нуля его прописать? Или я чего то вообще не догнал... Я то понимаю что если добавить все методы летать\плавать\бегать в Юнит, то надо будет как-то ими управлять при наследование. Кстате вообще не представлю как это реализуется - но почему то уверен что, наверное можно поставить условия-тумблеры ОН\ОФФ для того что бы не затирать лишние методы Оверрайдом в каждом классе-наследнике(или не Оверрайдом, я только на 10 уровень зашел - так что многого не знаю), и у нас будет возможность дефолтное значение поставить... Я вижу удобство при создание условия использования Если-естьИнтерфейсТакойто-тоВыполнять, но и там же использовано цикл для метода MakeSound - что выглядит одинаково удобно... Или я не прав?
user Уровень 23
19 декабря 2020
Спасибо!
Умалат Уровень 24
18 декабря 2020
В самый раз
Lilly Уровень 25
18 декабря 2020
Роман, я это понимаю, спасибо! Просто на 12 уровне интерфейсы только вводятся, и я заметила, что некоторые вообще не видят, что в них есть хоть какой-то смысл. Это как один из примеров (и как мне кажется, он отчасти и о том, о чем Вы говорите). То есть, реализацию метода можно поменять в любой момент, при этом не разрушив совместимость. Мне кажется, из моего примера можно сделать такие выводы. В любом случае, это как пример для тех, кто только начал изучать интерфейсы (как и я), а до SOLID и Spring еще очень далеко =) Спасибо за комментарий!
Роман Уровень 24
18 декабря 2020
Все таки более глобальная цель (высокоуровневая) интерфейсов, это наверное - малосвязность модулей приложения, для лучшего маштабирования, добавления новых фич, уменьшения времени разработки приложения в будущем. SOLID + Роберт Мартин "Чистая архитектура".