CodeGym /Kursy /JAVA 25 SELF /Wielokrotna implementacja interfejsów

Wielokrotna implementacja interfejsów

JAVA 25 SELF
Poziom 20, Lekcja 2
Dostępny

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.

1
Zadanie
JAVA 25 SELF, poziom 20, lekcja 2
Niedostępne
Cyfrowe płótno: Rysowanie i wymazywanie
Cyfrowe płótno: Rysowanie i wymazywanie
1
Zadanie
JAVA 25 SELF, poziom 20, lekcja 2
Niedostępne
Profil użytkownika: imię i możliwość zapisu
Profil użytkownika: imię i możliwość zapisu
1
Zadanie
JAVA 25 SELF, poziom 20, lekcja 2
Niedostępne
Uniwersalna akcja: jeden przycisk do wszystkiego
Uniwersalna akcja: jeden przycisk do wszystkiego
1
Zadanie
JAVA 25 SELF, poziom 20, lekcja 2
Niedostępne
Centrum multimedialne: zarówno odtwarzanie, jak i nagrywanie
Centrum multimedialne: zarówno odtwarzanie, jak i nagrywanie
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION