CodeGym /Kursy /JAVA 25 SELF /Metody domyślne w interfejsach

Metody domyślne w interfejsach

JAVA 25 SELF
Poziom 21 , Lekcja 2
Dostępny

1. Wprowadzenie

Dawniej (przed Java 8) interfejs był bardzo rygorystyczny: można było w nim deklarować tylko metody abstrakcyjne (bez implementacji) i stałe (public static final). Było to wygodne, dopóki nie pojawił się jeden duży problem: rozwój bibliotek.

Wyobraźmy sobie sytuację

Opracowałeś popularną bibliotekę, w której jest interfejs:

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

Tysiące programistów na całym świecie pisze swoje klasy implementujące ten interfejs. Po paru latach rozumiesz, że wszystkim brakuje metody reset(), która przywraca obiekt do pozycji początkowej. Dodajesz ją do interfejsu:

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

I zaczyna się apokalipsa: wszystkie projekty używające twojego interfejsu przestają się kompilować! Teraz muszą zaimplementować nową metodę, o której nikt nie wiedział. Migracja zamienia się w ból.

Metody domyślne — rozwiązanie!

Java 8 wprowadziła metody domyślne: teraz można dodać metodę z implementacją bezpośrednio w interfejsie! Wszystkie stare klasy automatycznie otrzymują standardową implementację i ich kod się nie psuje. A jeśli chcesz — możesz nadpisać metodę po swojemu.

2. Składnia metod domyślnych

Metoda domyślna to zwykła metoda z implementacją wewnątrz interfejsu, oznaczona słowem kluczowym default.

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

    default void reset() {
        // Typowa implementacja: wracamy do początku układu współrzędnych
        move(0, 0);
    }
}

Wyjaśnienie:

  • Wszystkie metody interfejsu są domyślnie public i abstract, ale metody domyślne nie są abstrakcyjne, mają ciało.
  • Słowo kluczowe default zawsze zapisuje się przed typem zwracanym metody.

Jak to wygląda w klasie?

public class Robot implements Movable {
    private int x, y;

    @Override
    public void move(int x, int y) {
        this.x = x;
        this.y = y;
        System.out.println("Robot przeniesiony do (" + x + ", " + y + ")");
    }

    // Nie trzeba implementować reset() — zadziała wersja domyślna!
}

Teraz, jeśli wywołamy reset() na obiekcie Robot, zadziała implementacja z interfejsu Movable:

public class Main {
    public static void main(String[] args) {
        Movable robot = new Robot();
        robot.move(10, 20); // Robot przeniesiony do (10, 20)
        robot.reset();      // Robot przeniesiony do (0, 0)
    }
}

3. Metody domyślne w bibliotece standardowej

Metody domyślne dodano nie bez powodu, lecz po to, by umożliwić rozwój dużych standardowych interfejsów Javy bez psucia starego kodu.

Przykład: interfejs List (Java 8+)

W Java 8 do interfejsu List dodano metody z implementacją, na przykład forEach, replaceAll, sort:

default void forEach(Consumer<Entity> action) {
    for (Entity e : this) {
        action.accept(e);
    }
}

Jeśli implementujesz własną listę i nie nadpiszesz forEach, i tak będzie działać — dzięki metodzie domyślnej.

Więcej o typach generycznych (Consumer<Entity>) poznasz na 26. poziomie :P

4. Po co są metody domyślne?

  • Rozwój API bez psucia kodu: można dodawać nowe metody do interfejsu bez konieczności implementowania ich we wszystkich istniejących klasach.
  • Uniwersalne wzorce zachowań: można zadeklarować domyślne zachowanie, z którego klasy mogą korzystać lub je nadpisać.
  • Zmniejszenie duplikacji: jeśli zachowanie jest takie samo dla większości implementacji — nie trzeba kopiować kodu do każdej klasy.

Analogia

Wyobraź sobie, że masz umowę najmu mieszkania (interfejs). Kiedyś było w niej napisane: „Najemca jest zobowiązany płacić za wodę”. Potem dodano: „Najemca jest zobowiązany płacić za prąd”. Gdyby nie metody domyślne, trzeba by przepisać wszystkie umowy ze wszystkimi najemcami! A z metodami domyślnymi — po prostu dodajesz punkt, a jeśli komuś potrzeba — mogą umówić się po swojemu.

5. Ograniczenia i cechy metod domyślnych

Metody domyślne nie mogą przesłaniać metod klasy Object

Nie możesz zadeklarować w interfejsie metody domyślnej o sygnaturze zgodnej z equals, hashCode lub toString z klasy Object. To ochrona przed zamieszaniem: każdy obiekt w Javie już ma te metody.

// Błąd kompilacji!
interface Broken {
    default boolean equals(Object obj) { return false; }
}

Konflikty metod domyślnych

Co jeśli klasa implementuje dwa interfejsy, z których każdy ma metodę domyślną o tej samej sygnaturze? Kompilator Java szczerze powie: „Zdecyduj sam, nie wiem, co zrobić!”

interface A {
    default void hello() { System.out.println("Hello from A"); }
}

interface B {
    default void hello() { System.out.println("Hello from B"); }
}

class C implements A, B {
    // Należy rozwiązać konflikt:
    @Override
    public void hello() {
        // Można wybrać, czyją metodę wywołać, albo zaimplementować własną
        A.super.hello(); // albo B.super.hello();
    }
}

Jeśli nie zaimplementujesz hello() w klasie C, pojawi się błąd kompilacji.

Metody domyślne mogą wywoływać inne metody interfejsu

Metoda domyślna może wywoływać inne metody interfejsu, nawet abstrakcyjne. Ważne, by implementacja była w klasie.

interface Printer {
    void print(String text);

    default void printTwice(String text) {
        print(text);
        print(text);
    }
}

6. Przykład: rozwijamy aplikację z metodą domyślną

Spójrzmy na przykład użycia metod domyślnych w interfejsie Movable:

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

    default void reset() {
        move(0, 0);
    }
}

Jest też klasa Robot implementująca ten interfejs:

public class Robot implements Movable {
    private int x = 5;
    private int y = 7;

    @Override
    public void move(int x, int y) {
        this.x = x;
        this.y = y;
        System.out.println("Robot przeniesiony do (" + x + ", " + y + ")");
    }

    // Nie implementujemy reset() — używamy metody domyślnej!
}

Teraz spróbujmy wywołać obie metody:

public class Main {
    public static void main(String[] args) {
        Movable robot = new Robot();
        robot.move(10, 20); // Robot przeniesiony do (10, 20)
        robot.reset();      // Robot przeniesiony do (0, 0)
    }
}

Jeśli zechcemy, aby Robot resetował się w jakiś szczególny sposób — po prostu nadpiszemy reset() w klasie:

@Override
public void reset() {
    System.out.println("Robot się wyłącza i wraca do bazy!");
    move(0, 0);
}

7. Metody domyślne a wielokrotna implementacja interfejsów

Metody domyślne są szczególnie przydatne, gdy klasa implementuje kilka interfejsów. Jest jednak niuans: jeśli oba interfejsy mają metodę domyślną o tej samej sygnaturze, kompilator zażąda jawnego rozwiązania konfliktu.

Przykład konfliktu

interface A {
    default void show() { System.out.println("A"); }
}
interface B {
    default void show() { System.out.println("B"); }
}
class C implements A, B {
    @Override
    public void show() {
        // Jawnie wybieramy, której metody domyślnej użyć
        A.super.show(); // albo B.super.show();
    }
}

8. Schemat: jak działa wywołanie metody domyślnej


+-------------------+
|   Movable         |
|-------------------|
| +move(int, int)   | <- metoda abstrakcyjna
| +reset()          | <- metoda domyślna
+-------------------+
         ^
         |
+-------------------+
|   Robot           |
|-------------------|
| +move(int, int)   | <- implementuje
|                   | (reset() nie zaimplementowana)
+-------------------+
         |
     Wywołanie reset()
         |
   Używana jest implementacja
   z interfejsu Movable
Wywołanie metody domyślnej: implementacja domyślna z interfejsu

9. Typowe błędy przy pracy z metodami domyślnymi

Błąd nr 1: próba utworzenia metody domyślnej bez implementacji.
Metoda domyślna musi mieć ciało! Jeśli napiszesz default void foo();, kompilator od razu powie: „Zapomniałeś o nawiasach klamrowych?”

Błąd nr 2: konflikt metod domyślnych z różnych interfejsów.
Jeśli klasa implementuje dwa interfejsy z taką samą metodą domyślną, musisz jawnie rozwiązać konflikt — w przeciwnym razie kompilator nie pozwoli skompilować kodu.

Błąd nr 3: próba zadeklarowania metody domyślnej o sygnaturze metody z Object.
Nie można zrobić metody domyślnej equals, hashCode ani toString w interfejsie — tylko metody abstrakcyjne o tych nazwach.

Błąd nr 4: pamiętaj, że metody domyślne to nie „magia”, tylko wygodne narzędzie.
Metody domyślne nie znoszą zasady, że interfejs to kontrakt. Jeśli domyślne zachowanie nie pasuje — zawsze nadpisuj metodę domyślną w klasie.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION