1. Wprowadzenie
W Javie nie można dziedziczyć po kilku klasach jednocześnie. Zrobiono to celowo, aby uniknąć „problemu diamentu” — sytuacji, gdy dwie klasy bazowe definiują tę samą metodę i nie wiadomo, którą implementację wybrać. Za to interfejsów można implementować dowolnie wiele! Dlaczego? Bo interfejs to wyłącznie kontrakt, nie zawiera implementacji (do Javy 8 — bez implementacji; od Javy 8 — możliwe default- i static-metody). Dzięki temu nie ma zamieszania z dziedziczeniem kodu.
To przypomina sytuację w prawdziwym życiu: możesz być jednocześnie „Kierowcą”, „Użytkownikiem komputera” i „Pływakiem”. Każdy z tych „interfejsów” opisuje określone umiejętności, ale nie zmusza cię do bycia kopią innej osoby.
Składnia wielokrotnej implementacji interfejsów
W Javie klasa może implementować kilka interfejsów, wymieniając je po przecinku po słowie kluczowym implements. Oto podstawowy przykład:
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("Robot przemieszcza się do punktu (" + x + ", " + y + ")");
}
@Override
public void charge() {
System.out.println("Robot się ładuje.");
}
}
W tym przykładzie Robot to wszechstronny zawodnik: potrafi się poruszać i ładować. Jak w życiu: im więcej potrafisz, tym częściej zapraszają cię na rozmowy kwalifikacyjne!
2. Po co to? Praktyczne przykłady
Przykład 1. Różne „role” obiektu
Wyobraź sobie, że projektujesz postać w grze:
- Może się poruszać (Movable)
- Może atakować (Attackable)
- Może być zapisywana do pliku (Serializable — taki interfejs istnieje w standardowej bibliotece Javy)
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("Bohater przemieszcza się na nową pozycję.");
}
@Override
public void attack() {
System.out.println("Bohater zadaje cios!");
}
}
Teraz tej klasy można używać w bardzo różnych kontekstach: można ją przekazywać do metod, które wymagają dowolnego z tych interfejsów.
Przykład 2. Łączenie standardowych interfejsów
Bardzo często w standardowej bibliotece Javy spotyka się interfejsy Comparable (do porównywania obiektów) i Serializable (do zapisywania obiektów do pliku lub przesyłania przez sieć). Czasem potrzeba, aby obiekt był i jednym, i drugim:
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);
}
}
Teraz obiekty Person można sortować (np. na liście) i zapisywać do pliku.
3. Cechy i ograniczenia
Jedna implementacja na metodę
Jeśli dwa interfejsy definiują metodę o tej samej sygnaturze, należy zaimplementować ją tylko raz. Przykład:
public interface A {
void doSomething();
}
public interface B {
void doSomething();
}
public class MyClass implements A, B {
@Override
public void doSomething() {
System.out.println("Implementacja doSomething dla obu interfejsów.");
}
}
Java nie będzie się buntować — ważne, aby sygnatury się zgadzały. Jeśli metody różnią się sygnaturą, są traktowane jako różne metody — każdą trzeba zaimplementować osobno.
Brak „problemu diamentu”
W przeciwieństwie do wielodziedziczenia klas, przy implementacji wielu interfejsów nie pojawia się sytuacja, gdy dziedziczy się dwie różne implementacje tej samej metody. Do Javy 8 w interfejsach nie było implementacji w ogóle, a wraz z pojawieniem się metod default — jeśli powstaje konflikt, musisz go jawnie rozstrzygnąć (o tym więcej w następnej lekcji).
Brak stanu
Interfejsy nie mogą zawierać zwykłych pól (tylko stałe — public static final). Dzięki temu nie ma zamieszania z „dwoma polami rodzica o tej samej nazwie”.
4. Przykład: implementujemy kilka interfejsów w jednej klasie
Dodajmy do naszej przykładowej aplikacji (np. do zoo) nowe możliwości. Załóżmy, że mamy zwierzęta, które mogą się poruszać i wydawać dźwięki:
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 + " biegnie do (" + x + ", " + y + ")");
}
@Override
public void makeSound() {
System.out.println(name + " mówi: Hau, hau!");
}
}
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 + " skrada się do (" + x + ", " + y + ")");
}
@Override
public void makeSound() {
System.out.println(name + " mówi: Miau!");
}
}
Teraz możemy napisać uniwersalne metody do pracy z dowolnym „ruchomym” lub „wydającym dźwięk” obiektem:
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("Reks");
Cat murka = new Cat("Murka");
testMovable(rex); // Reks biegnie do (10, 20)
testSoundable(murka); // Murka mówi: Miau!
}
I oczywiście, jeśli obiekt implementuje oba interfejsy, można go przekazywać i tu, i tu!
6. Przydatne niuanse
A co jeśli interfejsy kolidują?
Czasem dwa interfejsy definiują metody o tej samej sygnaturze, ale o innym znaczeniu. Na przykład jeden interfejs oczekuje, że metoda reset() wyzeruje współrzędne, a inny — że ta sama metoda wyłączy urządzenie. W takim przypadku trzeba zachować ostrożność: metodę i tak trzeba zaimplementować tylko raz i powinna „obsłużyć” oba zachowania (albo przynajmniej zdecydować, co ma robić). W praktyce takie sytuacje są rzadkie, ale jeśli się zdarzą — warto przemyśleć poprawność projektu.
Przykład z kolekcją obiektów różnych interfejsów
Załóżmy, że mamy listę obiektów implementujących różne interfejsy. Możemy je przeiterować i wywołać potrzebne metody:
Movable[] movables = {
new Dog("Spot"),
new Cat("Felix"),
new Robot()
};
for (Movable m : movables) {
m.move(0, 0);
}
Analogicznie można postąpić dla dowolnego interfejsu.
7. Typowe błędy przy wielokrotnej implementacji interfejsów
Błąd nr 1: nie zaimplementowano wszystkich metod interfejsów.
Jeśli klasa zadeklarowała, że implementuje interfejs, ale nie zaimplementowała choć jednej jego metody — kompilator natychmiast zgłosi błąd. Pamiętaj o wszystkich metodach, nawet jeśli wydają się „zbędne”.
Błąd nr 2: konfliktujące metody o tej samej sygnaturze.
Jeśli dwa interfejsy definiują jednakowe metody, zaimplementować je trzeba tylko raz. Ale jeśli znaczenie tych metod jest różne, może to prowadzić do zamieszania i błędów. W takiej sytuacji lepiej przemyśleć architekturę.
Błąd nr 3: próba dziedziczenia interfejsu przez extends w klasie.
W klasie do implementacji interfejsu zawsze używamy implements, a nie extends. Na przykład:
public class MyClass implements A, B { ... } // poprawnie
public class MyClass extends A, B { ... } // błąd!
Błąd nr 4: próba utworzenia obiektu interfejsu.
Interfejs to kontrakt, nie można go utworzyć bezpośrednio:
Movable m = new Movable(); // błąd kompilacji
Tworzyć można tylko obiekty klas implementujących interfejs.
GO TO FULL VERSION