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


99

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

Dal mio punto di vista (probabilmente ingenuo), non era necessario violare interfacce come questa. Le interfacce sono sempre state un contratto che devi rispettare e questo è un concetto molto semplice e puro. Ora è un mix di diverse cose. Secondo me:

  1. i metodi statici non appartengono alle interfacce. Appartengono a classi di utilità.
  2. i metodi "predefiniti" non avrebbero dovuto essere ammessi 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 è possibile aggiungere un nuovo metodo in un'interfaccia senza modificare tutte le implementazioni, ma in realtà è una buona cosa.

Dopo Java 8:

  • Non c'è praticamente alcuna differenza tra un'interfaccia e una classe astratta (oltre all'ereditarietà multipla). In effetti puoi emulare una classe normale con un'interfaccia.
  • Durante la programmazione delle implementazioni, i programmatori potrebbero dimenticare di ignorare i metodi predefiniti.
  • Si verifica 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 tenendo presente questa nuova funzionalità e ciò può causare problemi. Ad esempio, se qualcuno aggiunge un nuovo metodo predefinito default void foo()a un'interfaccia Ix, la classe che Cximplementa Ixe che ha un foometodo privato con la stessa firma non viene compilata.

Quali sono i motivi principali di tali importanti cambiamenti e quali nuovi vantaggi (se presenti) aggiungono?


30
Domanda bonus: perché non hanno introdotto l'ereditarietà multipla per le classi?

2
i metodi statici non appartengono alle interfacce. Appartengono a classi di utilità. No, appartengono alla @Deprecatedcategoria! i metodi statici sono uno dei costrutti più abusati di Java, a causa dell'ignoranza e della pigrizia. Molti metodi statici di solito significano programmatore incompetente, aumentano l'accoppiamento di diversi ordini di grandezza e sono un incubo per unit test e refactor quando ti rendi conto del perché sono una cattiva idea!

11
@JarrodRoberson Puoi fornire qualche suggerimento in più (i collegamenti sarebbero fantastici) su "sono un incubo per unit test e refactor quando ti rendi conto perché sono una cattiva idea!"? Non l'ho mai pensato e mi piacerebbe saperne di più.
acquoso,

11
@Chris L'ereditarietà multipla dello stato causa un sacco di problemi, specialmente con l'allocazione della memoria ( il classico problema del diamante ). L'ereditarietà multipla del comportamento, tuttavia, dipende solo dall'implementazione che soddisfa il contratto già impostato dall'interfaccia (l'interfaccia può chiamare altri metodi che dichiara e richiede). Una distinzione sottile, ma molto interessante.
ssube,

1
vuoi aggiungere metodo all'interfaccia esistente, era ingombrante o quasi impossibile prima di java8 ora puoi semplicemente aggiungerlo come metodo predefinito.
VdeX,

Risposte:


59

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 identica implementazione di List.sort.


19
C # risolve questo problema con i metodi di estensione.
Robert Harvey,

5
e consente a un elenco collegato di utilizzare un O (1) spazio extra e O (n log n) tempo di fusione, perché gli elenchi collegati possono essere uniti in posizione, in Java 7 esegue il dump su un array esterno e quindi ordina quello
maniaco del cricchetto

5
Ho trovato questo documento in cui Goetz spiega il problema. Quindi segnerò questa risposta come soluzione per ora.
Mister Smith,

1
@RobertHarvey: crea un elenco <Byte> di duecento milioni di elementi, usa IEnumerable<Byte>.Appendper unirti a loro, quindi chiama Count, quindi dimmi come i metodi di estensione risolvono il problema. Se CountIsKnowne Countfossero membri di IEnumerable<T>, il ritorno da Appendpotrebbe pubblicizzare CountIsKnownse le raccolte costituenti lo facessero, ma senza tali metodi ciò non è possibile.
supercat

6
@supercat: non ho la minima idea di cosa tu stia parlando.
Robert Harvey,

48

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

[d] i metodi efault ti consentono di aggiungere nuove funzionalità alle interfacce delle tue librerie e assicurano la compatibilità binaria con il codice scritto per le versioni precedenti di quelle interfacce.

Questa è stata una fonte di dolore di lunga data in Java, perché le interfacce tendevano ad essere impossibili da evolvere una volta rese pubbliche. (Il contenuto della documentazione è correlato al documento a cui si è collegati 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 stream) può essere effettuata solo estendendo il interfacce di raccolte esistenti e fornitura di implementazioni predefinite. Rompere la compatibilità binaria o introdurre nuove API significherebbe che passerebbero diversi anni prima che le funzionalità più importanti di Java 8 siano di uso comune.

La ragione per consentire i metodi statici nelle interfacce è di nuovo rivelata dalla documentazione: [t] rende più semplice l'organizzazione dei metodi di supporto nelle librerie; puoi mantenere metodi statici specifici per un'interfaccia nella stessa interfaccia piuttosto che in una classe separata. In altre parole, le classi di utilità statiche come java.util.Collectionsora possono (finalmente) essere considerate un anti-pattern, in generale (ovviamente non sempre ). La mia ipotesi è che l'aggiunta del supporto per questo comportamento sia stata 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 funzioni possono essere di beneficio è quello di considerare una classe che mi ha recentemente infastidito, java.util.UUID. In realtà non fornisce supporto per i tipi UUID 1, 2 o 5 e non può essere prontamente modificato per farlo. È anche bloccato con un generatore casuale predefinito che non può essere ignorato. L'implementazione del codice per i tipi di UUID non supportati richiede una dipendenza diretta da un'API di terze parti piuttosto che da un'interfaccia, oppure la manutenzione del codice di conversione e il costo della garbage collection aggiuntiva sono necessari. Con i metodi statici, UUIDavrebbe potuto essere definita invece un'interfaccia, consentendo reali implementazioni di terze parti dei pezzi mancanti. (Se UUIDinizialmente fossimo definiti come interfaccia, probabilmente avremmo una sorta di goffaUuidUtil classe con metodi statici, che sarebbe anche terribile.) Molte delle API principali 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 affermare che [t] non esiste praticamente alcuna differenza tra un'interfaccia e una classe astratta , poiché le classi astratte possono avere stato (ovvero dichiarare i campi) mentre le interfacce no. Non è quindi equivalente all'eredità multipla o anche all'eredità in stile mixin. Mixin appropriati (come i tratti di Groovy 2.3 ) hanno accesso allo stato. (Groovy supporta anche metodi di estensione statica.)

Inoltre , secondo me, non è una buona idea seguire l'esempio di Doval . Un'interfaccia dovrebbe definire un contratto, ma non dovrebbe imporre il contratto. (Non comunque in Java.) La corretta verifica di un'implementazione è responsabilità di una suite di test o di altri strumenti. La definizione dei contratti potrebbe essere fatta con le 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 fase di compilazione javactramite il processore di annotazioneGenerazione di API e bytecode di runtime. Idealmente, i contratti verrebbero applicati in fase di compilazione e, nel caso peggiore, utilizzando una suite di test, ma la mia comprensione è che l'applicazione del runtime è disapprovata. Un altro strumento interessante che potrebbe aiutare la programmazione dei contratti in Java è il Checker Framework .


1
Per il follow-up più avanti il mio ultimo comma (vale a dire non rispettare i contratti in interfacce ), vale la pena sottolineare che defaulti metodi non possono ignorare equals, hashCodee toString. Un'analisi costi / benefici molto istruttiva del perché questo non è consentito può essere trovata qui: mail.openjdk.java.net/pipermail/lambda-dev/2013-March/…
ngreen

È un peccato che Java abbia solo equalsun singolo hashCodemetodo virtuale e uno singolo poiché ci sono due diversi tipi di uguaglianza che le raccolte potrebbero aver bisogno di testare e gli articoli che implementerebbero più interfacce potrebbero essere bloccati da requisiti contrattuali contrastanti. Essere in grado di utilizzare elenchi che non cambieranno come hashMapchiavi è utile, ma ci sono anche momenti in cui sarebbe utile archiviare raccolte in un hashMapoggetto abbinato basato sull'equivalenza piuttosto che sullo stato corrente [l'equivalenza implica lo stato corrispondente e l' immutabilità ] .
supercat,

Bene, Java ha una soluzione alternativa con le interfacce Comparator e Comparable. Ma quelli sono un po 'brutti, penso.
ngreen

Tali interfacce sono supportate solo per determinati tipi di raccolta e pongono il loro problema: un comparatore può a sua volta incapsulare lo stato (ad esempio un comparatore di stringhe specializzato potrebbe ignorare un numero configurabile di caratteri all'inizio di ciascuna stringa, nel qual caso il numero di i caratteri da ignorare farebbero parte dello stato del comparatore) che a sua volta diventerebbe parte dello stato di qualsiasi raccolta che è stata ordinata da esso, ma non esiste un meccanismo definito per chiedere a due comparatori se sono equivalenti.
supercat

Oh sì, ho il dolore dei comparatori. Sto lavorando su una struttura ad albero che dovrebbe essere semplice ma non è perché è così difficile ottenere il comparatore giusto. Probabilmente scriverò una classe di alberi personalizzata solo per risolvere il problema.
ngreen

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 quelle classi base astratte in una raccolta di metodi statici e trasformare tutti i campi in argomenti. Ciò consentirebbe a qualsiasi implementatore dell'interfaccia di chiamare i metodi statici e ottenere la funzionalità, ma è una quantità terribile di boilerplate in un linguaggio che è già troppo prolisso.


Come esempio motivante del perché 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 popgenererà un'eccezione se lo stack è vuoto. Potremmo applicare questa regola separando popin due metodi: un public finalmetodo che impone il contratto e un protected abstractmetodo che esegue lo scoppio 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 garantiamo che tutte le implementazioni rispettino il contratto, ma le abbiamo anche liberate dal dover controllare se lo stack è vuoto e generare l'eccezione. È una grande vittoria! ... tranne per il fatto che abbiamo dovuto cambiare l'interfaccia in una classe astratta. In una lingua con singola eredità, questa è una grande perdita di flessibilità. Rende le tue aspiranti interfacce reciprocamente esclusive. Essere in grado di fornire implementazioni che si basano solo sui metodi di interfaccia stessi risolverebbe 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 so che il linguaggio D lo consente e fornisce supporto nativo per Design by Contract . Non esiste alcun pericolo in questa tecnica poiché popè definitiva, quindi nessuna classe di implementazione può ignorarla.

Per quanto riguarda le implementazioni predefinite di metodi sostituibili, presumo 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à correttamente con le implementazioni predefinite.

Inoltre,

Non c'è praticamente alcuna differenza tra un'interfaccia e una classe astratta (oltre all'ereditarietà multipla). In effetti puoi emulare una classe normale con un'interfaccia.

Questo non è del tutto vero poiché 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, considerare le classi di utilità come Collezioni nell'API Java. Quella classe esiste solo perché quei metodi statici non possono essere dichiarati nelle rispettive interfacce. Collections.unmodifiableListavrebbe potuto essere dichiarato altrettanto bene Listnell'interfaccia e sarebbe stato più facile da trovare.


4
Contro-argomento: poiché i metodi statici, se scritti correttamente, sono autonomi, hanno più senso in una classe statica separata dove possono essere raccolti e classificati in base al nome della classe e meno senso in un'interfaccia, dove sono essenzialmente una comodità che invita ad abusi come mantenere lo stato statico nell'oggetto o causare effetti collaterali, rendendo i metodi statici non verificabili.
Robert Harvey,

3
@RobertHarvey Cosa ti impedisce di fare cose altrettanto stupide se il tuo metodo statico è in una classe? Inoltre, il metodo nell'interfaccia potrebbe non richiedere alcuno stato. Potresti semplicemente provare a far rispettare un contratto. Supponiamo che tu abbia Stackun'interfaccia e desideri assicurarti che quando popviene chiamato con uno stack vuoto, viene generata un'eccezione. Dati metodi astratti boolean isEmpty()e protected T pop_impl(), è possibile implementare final T pop() { isEmpty()) throw PopException(); else return pop_impl(); }Questo impone il contratto su TUTTI gli implementatori.
Doval,

Aspetta cosa? I metodi Push e Pop su una pila non lo saranno static.
Robert Harvey,

@RobertHarvey Sarei stato più chiaro se non fosse stato per il limite di caratteri nei commenti, ma stavo facendo valere le implementazioni predefinite in un'interfaccia, non i metodi statici.
Doval,

8
Penso che i metodi di interfaccia predefiniti siano piuttosto un hack introdotto per essere in grado di estendere la libreria standard senza la necessità di adattare il codice esistente basato su di esso.
Giorgio,

2

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

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


1
I metodi di estensione non aggiungono funzionalità alle interfacce. I metodi di estensione sono solo zucchero sintattico per chiamare metodi statici su una classe usando il comodo list.sort(ordering);modulo.
Robert Harvey,

Se guardi l' IEnumerableinterfaccia in C #, puoi vedere come l'implementazione dei metodi di estensione a quell'interfaccia (come LINQ to Objectsfa) aggiunge funzionalità per ogni classe che implementa IEnumerable. Questo è ciò che intendevo con l'aggiunta di funzionalità.
rae1,

2
Questa è la cosa grandiosa dei metodi di estensione; danno l' illusione che stai sfruttando la funzionalità di una classe o interfaccia. Basta non confonderlo con l'aggiunta di metodi effettivi a una classe; i metodi di classe hanno accesso ai membri privati ​​di un oggetto, i metodi di estensione no (poiché sono solo un altro modo di chiamare metodi statici).
Robert Harvey,

2
Esatto, ed è per questo che vedo qualche relazione sull'avere metodi statici o predefiniti in un'interfaccia in Java; l'implementazione si basa su ciò che è disponibile per l'interfaccia e non sulla classe stessa.
rae1,

1

I due scopi principali che vedo nei defaultmetodi (alcuni casi d'uso servono entrambi gli scopi):

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

Se fosse solo per il secondo scopo, non lo vedresti in una nuova interfaccia come Predicate. Tutte le @FunctionalInterfaceinterfacce annotate devono avere esattamente un metodo astratto in modo che un lambda possa implementarlo. Aggiunto defaultmetodi come and, or, negatesono solo l'utilità, e non dovrebbero ignorare loro. Tuttavia, a volte i metodi statici farebbero meglio .

Per quanto riguarda l'estensione delle interfacce esistenti, anche lì, alcuni nuovi metodi sono solo zucchero di sintassi. Metodi di Collectioncome stream, forEach, removeIf- in fondo, è solo l'utilità non è necessario eseguire l'override. E poi ci sono metodi simili spliterator. L'implementazione predefinita è non ottimale, ma ehi, almeno il codice viene compilato. Ricorrere a questo solo se l'interfaccia è già pubblicata e ampiamente utilizzata.


Per quanto riguarda i staticmetodi, immagino che gli altri lo coprano abbastanza bene: consente all'interfaccia di essere la propria classe di utilità. Forse potremmo liberarci del Collectionsfuturo di Java? Set.empty()oscillerebbe.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.