JavaRush /Java блог /Random UA /Інтерфейси для тих, кому "дуже цікаво, але нічого не зроз...
Lilly
25 рівень
Москва

Інтерфейси для тих, кому "дуже цікаво, але нічого не зрозуміло"

Стаття з групи Random UA
Всім привіт! Даний пост написаний, скоріше, для тих, хто вже бачив і навіть вирішив на 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)
тобто параметром методу встановлюєте інтерфейс. Чи не конкретний клас, а інтерфейс. І всі інші програмісти, викликаючи у себе цей ваш метод 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
Ось так інтерфейси можуть бути корисні. І це далеко не всі їхні можливості та способи застосування. Далі по курсу, напевно, дізнаємось більше =) Дякую, що дочитали до кінця =)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ