Perché i metodi predefiniti e statici sono stati aggiunti alle interfacce in Java 8 quando avevamo già classi astratte?

94

In Java 8, le interfacce possono contenere metodi implementati, metodi statici e i cosiddetti metodi "predefiniti" (che le classi di implementazione non devono sovrascrivere).

Nella mia (probabilmente ingenua) visione, non c'era bisogno di violare interfacce come questa. Le interfacce sono sempre state un contratto da soddisfare, e questo è un concetto molto semplice e puro. Ora è un mix di diverse cose. A mio parere:

    I
  1. metodi statici non appartengono alle interfacce. Appartengono a classi di utilità.
  2. I metodi "predefiniti" non dovrebbero essere permessi nelle interfacce. Puoi sempre usare una classe astratta per questo scopo.

In breve:

Prima di Java 8:

  • È possibile utilizzare classi astratte e regolari per fornire metodi statici e predefiniti. Il ruolo delle interfacce è chiaro.
  • Tutti i metodi in un'interfaccia dovrebbero essere sostituiti implementando le classi.
  • Non puoi aggiungere un nuovo metodo in un'interfaccia senza modificare tutte le implementazioni, ma questa è effettivamente una buona cosa.

Dopo Java 8:

  • Non c'è praticamente alcuna differenza tra un'interfaccia e una classe astratta (diversa dall'ereditarietà multipla). In effetti puoi emulare una classe regolare con un'interfaccia.
  • Durante la programmazione delle implementazioni, i programmatori potrebbero dimenticare di sovrascrivere i metodi predefiniti.
  • C'è un errore di compilazione se una classe tenta di implementare due o più interfacce con un metodo predefinito con la stessa firma.
  • Aggiungendo un metodo predefinito a un'interfaccia, ogni classe di implementazione eredita automaticamente questo comportamento. Alcune di queste classi potrebbero non essere state progettate pensando a questa nuova funzionalità e questo può causare problemi. Ad esempio, se qualcuno aggiunge un nuovo metodo predefinito default void foo() a un'interfaccia Ix , la classe Cx che implementa Ix e un metodo privato foo con la stessa firma non viene compilata.

Quali sono le ragioni principali di tali grandi cambiamenti e quali nuovi vantaggi (se esistono) aggiungono?

    
posta Mister Smith 20.03.2014 - 16:01
fonte

5 risposte

56

Un buon esempio motivante per i metodi predefiniti è nella libreria standard Java, dove ora hai

list.sort(ordering);

invece di

Collections.sort(list, ordering);

Non penso che avrebbero potuto farlo diversamente senza più di una implementazione identica di List.sort .

    
risposta data 20.03.2014 - 16:47
fonte
44

Perché puoi ereditare solo una classe. Se hai due interfacce le cui implementazioni sono abbastanza complesse da richiedere una classe base astratta, queste due interfacce si escludono a vicenda nella pratica.

L'alternativa è convertire queste classi base astratte in una raccolta di metodi statici e trasformare tutti i campi in argomenti. Ciò consentirebbe a qualsiasi implementor dell'interfaccia di chiamare i metodi statici e ottenere la funzionalità, ma è un sacco di file standard in una lingua che è già troppo prolissa.

Come esempio motivante del fatto che essere in grado di fornire implementazioni nelle interfacce può essere utile, considera questa interfaccia Stack:

public interface Stack<T> {
    boolean isEmpty();

    T pop() throws EmptyException;
 }

Non c'è modo di garantire che quando qualcuno implementa l'interfaccia, pop genererà un'eccezione se lo stack è vuoto. Potremmo applicare questa regola separando pop in due metodi: un metodo public final che applica il contratto e un metodo protected abstract che esegue il popping effettivo.

public abstract class Stack<T> {
    public abstract boolean isEmpty();

    protected abstract T pop_implementation();

    public final T pop() throws EmptyException {
        if (isEmpty()) {
            throw new EmptyException();
        else {
            return pop_implementation();
        }
    }
 }

Non solo assicuriamo che tutte le implementazioni rispettino il contratto, ma le abbiamo anche liberate dal dover verificare se lo stack è vuoto e lanciare l'eccezione. È una grande vittoria! ... tranne il fatto che abbiamo dovuto cambiare l'interfaccia in una classe astratta. In una lingua con ereditarietà singola, si tratta di una grande perdita di flessibilità. Rende le tue interfacce potenziali reciprocamente esclusive. Essere in grado di fornire implementazioni che si basano solo sui metodi dell'interfaccia risolverà il problema.

Non sono sicuro che l'approccio di Java 8 all'aggiunta di metodi alle interfacce consenta l'aggiunta di metodi finali o metodi astratti protetti, ma conosco il linguaggio D lo consente e fornisce supporto nativo per Design by Contract . Non c'è alcun pericolo in questa tecnica poiché pop è finale, quindi nessuna classe di implementazione può sovrascriverla.

Per quanto riguarda le implementazioni predefinite di metodi sovrascrivibili, suppongo che eventuali implementazioni predefinite aggiunte alle API Java si basino solo sul contratto dell'interfaccia a cui sono state aggiunte, quindi qualsiasi classe che implementa correttamente l'interfaccia si comporterà anche correttamente con le implementazioni predefinite .

Inoltre,

There's virtually no difference between an interface and an abstract class (other than multiple inheritance). In fact you can emulate a regular class with an interface.

Questo non è del tutto vero dato che non è possibile dichiarare campi in un'interfaccia. Qualsiasi metodo che scrivi in un'interfaccia non può fare affidamento su alcun dettaglio di implementazione.

Come esempio a favore dei metodi statici nelle interfacce, prendi in considerazione classi di utilità come Raccolte nell'API Java. Quella classe esiste solo perché quei metodi statici non possono essere dichiarati nelle loro rispettive interfacce. Collections.unmodifiableList potrebbe essere stato dichiarato altrettanto bene nell'interfaccia List , e sarebbe stato più facile da trovare.

    
risposta data 20.03.2014 - 17:19
fonte
43

La risposta corretta si trova infatti nella documentazione Java , che afferma:

[d]efault methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces.

Questa è stata una fonte di sofferenza di vecchia data in Java, perché le interfacce erano tendenzialmente impossibili da evolvere una volta rese pubbliche. (Il contenuto della documentazione è correlato alla carta a cui si è collegato in un commento: Evoluzione dell'interfaccia tramite metodi di estensione virtuale ). Inoltre, l'adozione rapida di nuove funzionalità (ad esempio lambdas e le nuove API di flusso) può essere eseguita solo estendendo le interfacce di raccolte esistenti e fornendo implementazioni predefinite. Rompere la compatibilità binaria o introdurre nuove API significherebbe passare diversi anni prima che le funzionalità più importanti di Java 8 fossero di uso comune.

Il motivo per cui si consentono i metodi statici nelle interfacce è di nuovo rivelato dalla documentazione: [t] il suo rende più facile per te organizzare i metodi di supporto nelle tue librerie; puoi mantenere i metodi statici specifici per un'interfaccia nella stessa interfaccia piuttosto che in una classe separata. In altre parole, classi di utilità statiche come java.util.Collections ora può (finalmente) essere considerato un anti-pattern, in generale (ovviamente non sempre ). La mia ipotesi è che l'aggiunta del supporto per questo comportamento fosse banale una volta implementati i metodi di estensione virtuale, altrimenti probabilmente non sarebbe stato fatto.

Su una nota simile, un esempio di come queste nuove funzionalità possano essere di beneficio è considerare una classe che mi ha infastidito di recente, java.util.UUID . In realtà non fornisce supporto per tipi di UUID 1, 2 o 5 e non può essere modificato facilmente per farlo . È anche bloccato con un generatore casuale predefinito che non può essere ignorato. Il codice di implementazione per i tipi di UUID non supportati richiede una dipendenza diretta da un'API di terze parti piuttosto che un'interfaccia, oppure il mantenimento del codice di conversione e il costo della raccolta di dati inutili aggiuntiva per utilizzarlo. Con i metodi statici, UUID avrebbe potuto essere invece definita come interfaccia, consentendo reali implementazioni di terze parti dei pezzi mancanti. (Se UUID fosse originariamente definita come un'interfaccia, probabilmente avremmo una sorta di classe UuidUtil clunky con metodi statici, il che sarebbe terribile.) Molte API core di Java sono degradate non riuscendo a basarsi sulle interfacce ma a partire da Java 8 il numero di scuse per questo cattivo comportamento è diminuito per fortuna.

Non è corretto dire che [t] qui non c'è praticamente alcuna differenza tra un'interfaccia e una classe astratta , perché le classi astratte possono avere uno stato (ovvero dichiarare campi) mentre le interfacce non possono. Non è quindi equivalente all'ereditarietà multipla o all'ereditarietà in stile mixin. Miscelazioni appropriate (come i tratti ) di Groovy 2.3 hanno accesso allo stato . (Groovy supporta anche i metodi di estensione statica.)

Inoltre, non è una buona idea seguire l'esempio di Doval , secondo me. Si suppone che un'interfaccia definisca un contratto, ma non è previsto che faccia rispettare il contratto. (Non in Java comunque.) La corretta verifica di un'implementazione è responsabilità di una suite di test o di un altro strumento. La definizione dei contratti potrebbe essere effettuata con annotazioni e OVal è un buon esempio, ma non so se supporta i vincoli definiti sulle interfacce. Un tale sistema è fattibile, anche se attualmente non esiste. (Le strategie includono la personalizzazione in tempo reale di javac tramite processore di annotazione API e generazione di bytecode di runtime. Idealmente, i contratti dovrebbero essere applicati in fase di compilazione e nel peggiore dei casi utilizzando una suite di test, ma la mia comprensione è che l'applicazione runtime è disapprovata. Un altro strumento interessante che potrebbe aiutare la programmazione dei contratti in Java è il Framework Checker .

    
risposta data 03.09.2014 - 20:06
fonte
2

Forse l'intento era di fornire la possibilità di creare classi mixin sostituendo la necessità di iniettare informazioni o funzionalità statiche tramite una dipendenza.

Questa idea sembra correlata a come puoi usare i metodi di estensione in C # per aggiungere funzionalità implementate alle interfacce.

    
risposta data 20.03.2014 - 16:53
fonte
1

I due scopi principali che vedo nei metodi default (alcuni casi d'uso hanno entrambi gli scopi):

  1. Sintassi dello zucchero. Una classe di utilità potrebbe servire a tale scopo, ma i metodi di istanza sono più carini.
  2. Estensione di un'interfaccia esistente. L'implementazione è generica ma a volte inefficiente.

Se si trattava solo del secondo scopo, non lo vedresti in una nuova interfaccia come Predicate . Tutte le interfacce con annotazioni @FunctionalInterface sono necessarie per avere esattamente un metodo astratto in modo che un lambda possa implementarlo. I metodi default aggiunti come and , or , negate sono solo utilità e non è necessario sovrascriverli. Tuttavia, a volte i metodi statici farebbero meglio .

Come per l'estensione delle interfacce esistenti - anche lì, alcuni nuovi metodi sono solo lo zucchero di sintassi. Metodi di Collection come stream , forEach , removeIf - in fondo, è solo l'utilità che non è necessario sovrascrivere. E poi ci sono metodi come spliterator . L'implementazione predefinita è subottimale, ma, almeno, il codice viene compilato. Ricorri a questo solo se la tua interfaccia è già stata pubblicata e ampiamente utilizzata.

Per quanto riguarda i metodi static , suppongo che gli altri lo coprano abbastanza bene: consente all'interfaccia di essere la sua classe di utilità. Forse potremmo sbarazzarci di Collections nel futuro di Java? Set.empty() farebbe rock.

    
risposta data 24.12.2016 - 22:57
fonte

Leggi altre domande sui tag