variable - Perché Java non consente l'ereditarietà multipla ma consente la conformità a più interfacce con le implementazioni predefinite




java interface variable (4)

Non sto chiedendo questo -> Perché non esiste un'eredità multipla in Java, ma l'implementazione di più interfacce è consentita?

In Java, l'ereditarietà multipla non è consentita, ma, dopo Java 8, le interfacce possono avere metodi predefiniti (possono implementare i metodi stessi), proprio come le classi astratte. In questo contesto, dovrebbe essere consentita anche l'ereditarietà multipla.

interface TestInterface 
{ 
    // abstract method 
    public void square(int a); 

    // default method 
    default void show() 
    { 
      System.out.println("Default Method Executed"); 
    } 
} 

I problemi principali con l'ereditarietà multipla sono l'ordine (per l'override e le chiamate a super ), i campi e i costruttori; le interfacce non hanno campi o costruttori, quindi non causano problemi.

Se guardi le altre lingue, di solito rientrano in due grandi categorie:

  1. Linguaggi con ereditarietà multipla e alcune funzionalità per disambiguare casi speciali: ereditarietà virtuale [C ++], chiamate dirette a tutti i supercostruttori nella classe più derivata [C ++], linearizzazione di superclassi [Python], regole complesse per super [Python], ecc. .

  2. Linguaggi con un concetto diverso, solitamente chiamati interfacce , tratti , mixin , moduli , ecc. Che impongono alcune limitazioni come: nessun costrutto [Java] o nessun costrutto con parametri [Scala fino a tempi molto recenti], nessun campo mutabile [Java], specifico regole per l'override (ad es. i mixin hanno la precedenza sulle classi di base [Ruby], quindi puoi includerli quando hai bisogno di molti metodi di utilità), ecc. Java è diventato un linguaggio come questi.

Perché solo disabilitando campi e costruttori si risolvono molti problemi legati all'ereditarietà multipla?

  • Non puoi avere campi duplicati in classi base duplicate.
    • La gerarchia delle classi principali è ancora lineare.
  • Non puoi costruire i tuoi oggetti di base nel modo sbagliato.
    • Immaginate se Object avesse campi pubblici / protetti e tutte le sottoclassi avessero costruttori che impostano quei campi. Quando si eredita da più di una classe (tutte derivate da Object), quale si ottiene per impostare i campi? L'ultima lezione? Diventano fratelli nella gerarchia, quindi non sanno nulla l'uno dell'altro. Dovresti avere più copie di Object per evitare questo? Tutte le classi interagirebbero correttamente?
  • Ricorda che i campi in Java non sono virtuali (sovrascrivibili), ma semplicemente archiviazione dei dati.
    • Si potrebbe creare un linguaggio in cui i campi si comportano come metodi e potrebbero essere sovrascritti (lo spazio di archiviazione effettivo sarebbe sempre privato), ma sarebbe un cambiamento molto più grande e probabilmente non verrebbe più chiamato Java.
  • Le interfacce non possono essere istanziate da sole.
    • Dovresti sempre combinarli con una classe concreta. Ciò elimina la necessità di costruttori e rende anche più chiaro l'intento del programmatore (ovvero, cosa si intende per essere una classe concreta e che cos'è un'interfaccia / mixin accessorio). Ciò fornisce anche un luogo ben definito per risolvere tutte le ambiguità: la classe concreta.

I progettisti di linguaggi ci hanno già pensato, quindi queste cose sono applicate dal compilatore. Quindi se si definisce:

interface First {
    default void go() {
    }
}

interface Second {
    default void go() {
    }
}

E si implementa una classe per entrambe le interfacce:

static class Impl implements First, Second {

}

otterrai un errore di compilazione; e avresti bisogno di ignorare go per non creare l'ambiguità intorno ad esso.

Ma potresti pensare che puoi ingannare il compilatore qui, facendo:

interface First {
    public default void go() {
    }
}

static abstract class Second {
    abstract void go();
}

static class Impl extends Second implements First {
}

Potresti pensare che First::go fornisce già un'implementazione per Second::go e dovrebbe andare bene. Anche questo è curato, quindi anche questo non viene compilato.

JLS 9.4.1.3: Analogamente, quando vengono ereditati un metodo astratto e un metodo predefinito con firme corrispondenti, viene generato un errore . In questo caso, sarebbe possibile dare la priorità a uno o l'altro - forse dovremmo assumere che il metodo predefinito fornisca anche un'implementazione ragionevole per il metodo astratto. Ma questo è rischioso, poiché oltre al nome e alla firma coincidenti, non abbiamo motivo di ritenere che il metodo predefinito si comporti in modo coerente con il contratto del metodo astratto: il metodo predefinito potrebbe non esistere nemmeno quando la subinterface è stata originariamente sviluppata . In questa situazione è più sicuro chiedere all'utente di affermare attivamente che l'implementazione predefinita sia appropriata (tramite una dichiarazione di priorità).

L'ultimo punto che vorrei introdurre, per consolidare che l'ereditarietà multipla non è consentita anche con le nuove aggiunte in java, è che i metodi statici delle interfacce non sono ereditati. i metodi statici sono ereditati di default:

static class Bug {
    static void printIt() {
        System.out.println("Bug...");
    }
}

static class Spectre extends Bug {
    static void test() {
        printIt(); // this will work just fine
    }
}

Ma se lo cambiamo per un'interfaccia (e puoi implementare più interfacce, a differenza delle classi):

interface Bug {
    static void printIt() {
        System.out.println("Bug...");
    }
}

static class Spectre implements Bug {
    static void test() {
        printIt(); // this will not compile
    }
}

Ora, questo è vietato anche dal compilatore e da JLS :

JLS 8.4.8: Una classe non eredita metodi statici dalle sue superinterfacce.


Le cose non sono così semplici.
Se una classe implementa più interfacce che definiscono metodi predefiniti con la stessa firma, il compilatore imporrà l'override di questo metodo per la classe.

Ad esempio con queste due interfacce:

public interface Foo {
    default void doThat() {
        // ...
    }
}

public interface Bar {    
    default void doThat() {
        // ...
    }       
}

Non verrà compilato:

public class FooBar implements Foo, Bar{
}

È necessario definire / sovrascrivere il metodo per rimuovere l'ambiguità.
Potresti ad esempio delegare all'implementazione della Bar come:

public class FooBar implements Foo, Bar{    
    @Override
    public void doThat() {
        Bar.super.doThat();
    }    
}

o delegare all'implementazione di Foo come:

public class FooBar implements Foo, Bar {
    @Override
    public void doThat() {
        Foo.super.doThat();
    }
}

o ancora definire un altro comportamento:

public class FooBar implements Foo, Bar {
    @Override
    public void doThat() {
        // ... 
    }
}

Questo vincolo mostra che Java non consente l'ereditarietà multipla anche per i metodi di interfaccia predefiniti.

Penso che non possiamo applicare la stessa logica per eredità multiple perché potrebbero verificarsi problemi multipli che sono i principali:

  • sovrascrivere il metodo di una classe ereditata potrebbe introdurre effetti collaterali e modificare il comportamento generale della classe ereditata se la classe si basa su questo metodo internamente.
  • come ereditare più campi? E anche se il linguaggio lo permettesse avresti esattamente lo stesso problema di questo precedentemente citato: effetto collaterale nel comportamento della classe ereditata: un campo int foo definito in una classe A e B che vuoi sottoclasse non ha il stesso significato e intenzione.

Questo è principalmente legato al "problema dei diamanti", credo. In questo momento se implementi più interfacce con lo stesso metodo, il compilatore ti costringe a sovrascrivere il metodo che vuoi implementare, perché non sa quale su usare. Immagino che i creatori di Java volessero rimuovere questo problema quando le interfacce non potevano usare i metodi predefiniti. Ora è venuta l'idea, è bello poter disporre di metodi con l'implementazione nelle interfacce, poiché è ancora possibile utilizzarli come interfacce funzionali nelle espressioni stream / lambda e utilizzare i loro metodi predefiniti nell'elaborazione. Non puoi farlo con le classi, ma il problema dei diamanti esiste ancora lì. Questa è la mia ipotesi :)





abstract