JavaRush /Курси /JAVA 25 SELF /Множинна реалізація інтерфейсів

Множинна реалізація інтерфейсів

JAVA 25 SELF
Рівень 20 , Лекція 2
Відкрита

1. Вступ

У Java не можна успадковувати водночас кілька класів. Так задумано, щоб уникнути «алмаза смерті» — ситуації, коли два батьківські класи визначають один і той самий метод, і незрозуміло, яку реалізацію обрати. Натомість інтерфейсів можна реалізовувати скільки завгодно. Чому так? Адже інтерфейс — це лише контракт: він не містить реалізації (до Java 8 — без реалізації; починаючи з Java 8 — можливі default- і static-методи). А отже, жодної плутанини зі спадкуванням коду не виникає.

Це схоже на ситуацію в реальному житті: ви можете одночасно бути «Водієм», «Користувачем компʼютера» і «Плавцем». Кожен із цих «інтерфейсів» описує певні вміння, але не змушує вас бути копією іншої людини.

Синтаксис множинної реалізації інтерфейсів

У Java клас може реалізовувати кілька інтерфейсів, перелічивши їх через кому після ключового слова implements. Ось базовий приклад:

public interface Movable {
    void move(int x, int y);
}

public interface Chargeable {
    void charge();
}

public class Robot implements Movable, Chargeable {
    @Override
    public void move(int x, int y) {
        System.out.println("Робот рухається до точки (" + x + ", " + y + ")");
    }

    @Override
    public void charge() {
      System.out.println("Робот заряджається.");
    }
}

У цьому прикладі Robot — універсальний: він і рухається, і заряджається. Усе як у житті: що ширший набір умінь, то частіше вас запрошують на співбесіди.

2. Навіщо це потрібно? Практичні приклади

Приклад 1. Різні «ролі» обʼєкта

Уявіть, що ви проєктуєте ігрового персонажа:

  • Він може рухатися (Movable)
  • Може атакувати (Attackable)
  • Може зберігатися у файл (Serializable — такий інтерфейс є у стандартній бібліотеці Java)
public interface Attackable {
    void attack();
}

public class Hero implements Movable, Attackable, java.io.Serializable {
    @Override
    public void move(int x, int y) {
        System.out.println("Герой переміщується на нову позицію.");
    }

    @Override
    public void attack() {
        System.out.println("Герой завдає удару!");
    }
}

Тепер ваш клас можна використовувати в найрізноманітніших контекстах: його можна передавати в методи, які вимагають будь-якого з цих інтерфейсів.

Приклад 2. Комбінування стандартних інтерфейсів

Дуже часто в стандартній бібліотеці Java трапляються інтерфейси Comparable (для порівняння обʼєктів) і Serializable (для збереження обʼєктів у файл або передавання мережею). Іноді потрібно, щоб обʼєкт був і тим, і іншим:

public class Person implements Comparable<Person>, java.io.Serializable {
    private String name;
    private int age;

    public Person(String name, int age) { this.name = name; this.age = age; }

    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
}

Тепер обʼєкти Person можна сортувати (наприклад, у списку) та записувати у файл.

3. Особливості та обмеження

Одна реалізація на метод

Якщо два інтерфейси визначають метод з однаковою сигнатурою, реалізувати його потрібно лише один раз. Приклад:

public interface A {
    void doSomething();
}
public interface B {
    void doSomething();
}
public class MyClass implements A, B {
    @Override
    public void doSomething() {
        System.out.println("Реалізація doSomething для обох інтерфейсів.");
    }
}

Компілятор не скаржитиметься — головне, щоб сигнатури збігалися. Якщо методи відрізняються сигнатурою, вони вважаються різними методами — реалізувати потрібно кожен із них.

Немає «алмаза смерті»

На відміну від множинного успадкування класів, під час реалізації кількох інтерфейсів не виникає ситуації, коли успадковано дві різні реалізації одного й того самого методу. До Java 8 в інтерфейсах узагалі не було реалізації, а з появою default-методів — якщо виникає конфлікт, ви зобовʼязані розвʼязати його явно (про це докладніше — у наступній лекції).

Немає стану

Інтерфейси не можуть містити звичайних полів (лише константи — public static final). Тому не виникає плутанини з «двома батьківськими полями з однаковим іменем».

4. Приклад: реалізуємо кілька інтерфейсів в одному класі

Додаймо до нашого навчального застосунку (наприклад, до зоопарку) нові можливості. Нехай у нас є тварини, які можуть рухатися й подавати голос:

public interface Movable {
    void move(int x, int y);
}

public interface Soundable {
    void makeSound();
}

public class Dog implements Movable, Soundable {
    private String name;

    public Dog(String name) {
        this.name = name;
    }

    @Override
    public void move(int x, int y) {
        System.out.println(name + " біжить до (" + x + ", " + y + ")");
    }

    @Override
    public void makeSound() {
        System.out.println(name + " каже: Гав-гав!");
    }
}

public class Cat implements Movable, Soundable {
    private String name;

    public Cat(String name) {
        this.name = name;
    }

    @Override
    public void move(int x, int y) {
        System.out.println(name + " крадеться до (" + x + ", " + y + ")");
    }

    @Override
    public void makeSound() {
        System.out.println(name + " каже: Няв!");
    }
}

Тепер ми можемо написати універсальний метод для роботи з будь-яким «рухомим» або «звучним» обʼєктом:

public static void testMovable(Movable m) {
    m.move(10, 20);
}

public static void testSoundable(Soundable s) {
    s.makeSound();
}

public static void main(String[] args) {
    Dog rex = new Dog("Рекс");
    Cat murka = new Cat("Мурка");

    testMovable(rex);       // Рекс біжить до (10, 20)
    testSoundable(murka);   // Мурка каже: Няв!
}

І, звісно, якщо обʼєкт реалізує обидва інтерфейси, його можна передавати і туди, і туди!

6. Корисні нюанси

Якщо інтерфейси конфліктують?

Іноді буває, що два інтерфейси визначають методи з однаковою сигнатурою, але з різним змістом. Наприклад, один інтерфейс очікує, що метод reset() скине координати, а інший — що цей самий метод вимкне пристрій. У такому разі доведеться діяти уважно: реалізувати метод усе одно потрібно один раз, і він має забезпечити коректну поведінку в обох випадках (або принаймні визначити, що саме робити). У реальному житті такі ситуації трапляються нечасто, але якщо трапилися — варто переглянути проєктування.

Приклад із колекцією обʼєктів різних інтерфейсів

Припустімо, у нас є список обʼєктів, що реалізують різні інтерфейси. Ми можемо перебрати їх і викликати потрібні методи:

Movable[] movables = {
    new Dog("Бровко"),
    new Cat("Мурчик"),
    new Robot()
};

for (Movable m : movables) {
    m.move(0, 0);
}

Аналогічно можна зробити для будь-якого інтерфейсу.

7. Типові помилки під час множинної реалізації інтерфейсів

Помилка № 1: не реалізовано всі методи інтерфейсів.
Якщо клас оголосив, що реалізує інтерфейс, але не реалізував хоча б один його метод — компілятор одразу видасть помилку. Не забувайте про всі методи, навіть якщо вони здаються «зайвими».

Помилка № 2: конфліктні методи з однаковою сигнатурою.
Якщо два інтерфейси визначають однакові методи, реалізувати їх потрібно лише один раз. Але якщо зміст цих методів різний, це може призвести до плутанини та помилок. У такому разі краще переосмислити архітектуру.

Помилка № 3: спроба реалізувати інтерфейс через extends у класі.
У класі для реалізації інтерфейсу завжди використовується implements, а не extends. Наприклад:

public class MyClass implements A, B { ... } // правильно
public class MyClass extends A, B { ... }    // помилка!

Помилка № 4: спроба створити обʼєкт інтерфейсу.
Інтерфейс — це контракт, його не можна створити безпосередньо:

Movable m = new Movable(); // помилка компіляції

Створювати можна лише обʼєкти класів, що реалізують інтерфейс.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ