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(); // помилка компіляції
Створювати можна лише обʼєкти класів, що реалізують інтерфейс.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ