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
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.
GO TO FULL VERSION