CodeGym /Corsi /JAVA 25 SELF /Metodi di default nelle interfacce

Metodi di default nelle interfacce

JAVA 25 SELF
Livello 21 , Lezione 2
Disponibile

1. Introduzione

Molto tempo fa (prima di Java 8) un’interfaccia era molto rigorosa: al suo interno si potevano dichiarare solo metodi astratti (senza implementazione) e costanti (public static final). Era comodo finché non è emerso un grande problema: l’evoluzione delle librerie.

Immagina la situazione

Hai sviluppato una libreria popolare che contiene un’interfaccia:

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

Migliaia di programmatori in tutto il mondo scrivono le loro classi che implementano questa interfaccia. Dopo un paio d’anni ti rendi conto che a tutti manca il metodo reset(), che riporta l’oggetto alla posizione iniziale. Lo aggiungi nell’interfaccia:

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

E qui inizia l’apocalisse: tutti i progetti che usano la tua interfaccia smettono di compilare! Ora, infatti, sono obbligati a implementare il nuovo metodo, di cui nessuno sapeva. La migrazione diventa dolorosa.

I metodi di default — la soluzione!

Java 8 ha introdotto i metodi di default: ora è possibile aggiungere un metodo con implementazione direttamente in un’interfaccia! Tutte le classi esistenti ottengono automaticamente un’implementazione standard e il loro codice non si rompe. E, se vuoi, puoi sempre ridefinire il metodo a modo tuo.

2. Sintassi dei metodi di default

Un metodo di default è un normale metodo con implementazione all’interno dell’interfaccia, contrassegnato dalla parola chiave default.

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

    default void reset() {
        // Implementazione tipica: ritorno all’origine degli assi
        move(0, 0);
    }
}

Spiegazione:

  • Tutti i metodi di un’interfaccia sono implicitamente public e abstract, ma i metodi di default non sono astratti: hanno un corpo.
  • La parola chiave default si scrive sempre prima del tipo di ritorno del metodo.

Come appare in una classe?

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("Il robot è stato spostato a (" + x + ", " + y + ")");
    }

    // Non è obbligatorio implementare reset() — funzionerà la versione di default!
}

Ora, se chiamiamo reset() su un oggetto Robot, verrà usata l’implementazione dell’interfaccia Movable:

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

3. Metodi di default nella libreria standard

I metodi di default sono stati introdotti proprio per consentire l’evoluzione dei grandi interfacce standard di Java senza rompere il codice esistente.

Esempio: l’interfaccia List (Java 8+)

In Java 8 sono stati aggiunti all’interfaccia List metodi con implementazione, ad esempio forEach, replaceAll, sort:

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

Se implementi una tua lista e non ridefinisci forEach, funzionerà comunque — grazie al metodo di default.

Maggiori dettagli sui tipi generici (Consumer<Entity>) li vedrai al livello 26 :P

4. Perché servono i metodi di default?

  • Evoluzione dell’API senza rompere il codice: puoi aggiungere nuovi metodi a un’interfaccia senza doverli implementare in tutte le classi esistenti.
  • Schemi di comportamento riutilizzabili: puoi dichiarare un comportamento predefinito che le classi possono usare o ridefinire.
  • Riduzione della duplicazione: se il comportamento è uguale per la maggior parte delle implementazioni, non serve copiare il codice in ogni classe.

Analogia

Immagina di avere un contratto di affitto di un appartamento (interfaccia). In passato c’era scritto: «L’inquilino è tenuto a pagare l’acqua». Poi si è aggiunto: «L’inquilino è tenuto a pagare l’elettricità». Se non esistessero i metodi di default, dovresti riscrivere tutti i contratti con tutti gli inquilini! Con i metodi di default, invece, aggiungi semplicemente una clausola e, se qualcuno ha bisogno di un accordo diverso, può pattuire diversamente.

5. Limitazioni e particolarità dei metodi di default

I metodi di default non possono ridefinire i metodi della classe Object

Non puoi dichiarare in un’interfaccia un metodo di default con una firma che coincida con equals, hashCode o toString della classe Object. È una protezione contro la confusione: qualunque oggetto in Java ha già questi metodi.

// Errore di compilazione!
interface Broken {
    default boolean equals(Object obj) { return false; }
}

Conflitti tra metodi di default

Che succede se una classe implementa due interfacce, ognuna delle quali ha un metodo di default con la stessa firma? Il compilatore Java dirà onestamente: «Decidilo tu, non so cosa fare!»

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 {
    // Risolvi il conflitto obbligatoriamente:
    @Override
    public void hello() {
        // Puoi scegliere quale metodo chiamare, oppure implementarne uno tuo
        A.super.hello(); // oppure B.super.hello();
    }
}

Se non implementi hello() nella classe C, otterrai un errore di compilazione.

I metodi di default possono chiamare altri metodi dell’interfaccia

Un metodo di default può chiamare altri metodi dell’interfaccia, anche astratti. L’importante è che l’implementazione esista nella classe.

interface Printer {
    void print(String text);

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

6. Esempio: facciamo evolvere un’app con un metodo di default

Guardiamo un esempio d’uso dei metodi di default nell’interfaccia Movable:

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

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

E c’è una classe Robot che implementa questa interfaccia:

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("Il robot è stato spostato a (" + x + ", " + y + ")");
    }

    // Non implementiamo reset() — usiamo il metodo di default!
}

Ora proviamo a chiamare entrambi i metodi:

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

Se volessimo che Robot si resettasse in modo particolare, basterebbe ridefinire reset() nella classe:

@Override
public void reset() {
    System.out.println("Il robot si spegne e torna alla base!");
    move(0, 0);
}

7. Metodi di default e implementazione multipla delle interfacce

I metodi di default sono particolarmente utili quando una classe implementa più interfacce. Attenzione però: se entrambe le interfacce hanno un metodo di default con la stessa firma, il compilatore richiederà una risoluzione esplicita del conflitto.

Esempio di conflitto

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() {
        // Scegliamo esplicitamente quale metodo di default usare
        A.super.show(); // oppure B.super.show();
    }
}

8. Schema: come funziona la chiamata a un metodo di default


+-------------------+
|   Movable         |
|-------------------|
| +move(int, int)   | <- metodo astratto
| +reset()          | <- metodo di default
+-------------------+
         ^
         |
+-------------------+
|   Robot           |
|-------------------|
| +move(int, int)   | <- implementa
|                   | (non implementa reset())
+-------------------+
         |
     Chiamata a reset()
         |
   Si usa l'implementazione
   dell'interfaccia Movable
Chiamata a un metodo di default: implementazione predefinita dall’interfaccia

9. Errori comuni con i metodi di default

Errore n. 1: tentare di dichiarare un metodo di default senza implementazione.
Un metodo di default deve avere un corpo! Se scrivi default void foo();, il compilatore dirà subito: «Hai dimenticato le parentesi graffe?»

Errore n. 2: conflitto tra metodi di default da interfacce diverse.
Se una classe implementa due interfacce con lo stesso metodo di default, devi risolvere il conflitto in modo esplicito — altrimenti il compilatore non permetterà di compilare il codice.

Errore n. 3: tentare di dichiarare un metodo di default con la firma di un metodo di Object.
Non si può avere in un’interfaccia un metodo di default equals, hashCode o toString — solo metodi astratti con questi nomi.

Errore n. 4: dimenticare che i metodi di default non sono «magia», ma solo uno strumento comodo.
I metodi di default non annullano il principio per cui un’interfaccia è un contratto. Se il comportamento predefinito non è adatto, ridefinisci sempre il metodo di default nella classe.

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