Java8: Perché è vietato definire un metodo predefinito per un metodo da java.lang.Object


130

I metodi predefiniti sono un bel nuovo strumento nella nostra casella degli strumenti Java. Tuttavia, ho provato a scrivere un'interfaccia che definisce una defaultversione del toStringmetodo. Java mi dice che questo è proibito, poiché i metodi dichiarati injava.lang.Object potrebbero non essere modificati default. Perché è così?

So che esiste la regola "la classe base vince sempre", quindi per impostazione predefinita (gioco di parole), qualsiasi defaultimplementazione di un Objectmetodo verrebbe sovrascritta dal metodo in Objectogni caso. Tuttavia, non vedo alcun motivo per cui non ci dovrebbe essere un'eccezione per i metodi Objectnelle specifiche. Soprattutto toStringperché potrebbe essere molto utile avere un'implementazione predefinita.

Quindi, qual è la ragione per cui i progettisti Java hanno deciso di non consentire i defaultmetodi che hanno la precedenza sui metodi Object?


1
Mi sento così bene con me stesso ora, votando questo per 100 volte e quindi un distintivo d'oro. buona domanda!
Eugene,

Risposte:


186

Questo è un altro di quei problemi di design del linguaggio che sembra "ovviamente una buona idea" fino a quando non inizi a scavare e ti rendi conto che in realtà è una cattiva idea.

Questa mail ha molto sull'argomento (e anche su altri argomenti). Ci sono state diverse forze di progettazione che sono confluite per portarci al design attuale:

  • Il desiderio di mantenere semplice il modello ereditario;
  • Il fatto che una volta esaminati gli esempi ovvi (ad esempio, trasformandosi AbstractListin un'interfaccia), ti rendi conto che ereditare equals / hashCode / toString è fortemente legato alla singola eredità e stato e che le interfacce sono moltiplicate ereditate e apolidi;
  • Che potenzialmente ha aperto la porta ad alcuni comportamenti sorprendenti.

Hai già toccato l'obiettivo "Keep it simple"; le regole di ereditarietà e risoluzione dei conflitti sono progettate per essere molto semplici (le classi vincono sulle interfacce, le interfacce derivate vincono sulle superinterfacce e qualsiasi altro conflitto è risolto dalla classe di implementazione.) Naturalmente queste regole potrebbero essere modificate per fare un'eccezione, ma Penso che scoprirai quando inizi a tirare quella stringa, che la complessità incrementale non è così piccola come potresti pensare.

Certo, c'è un certo grado di beneficio che giustificherebbe una maggiore complessità, ma in questo caso non è lì. I metodi di cui stiamo parlando sono uguali, hashCode e toString. Questi metodi sono tutti intrinsecamente relativi allo stato dell'oggetto, ed è la classe che possiede lo stato, non l'interfaccia, che si trova nella posizione migliore per determinare cosa significhi l'uguaglianza per quella classe (specialmente perché il contratto per l'uguaglianza è abbastanza forte; vedi Efficace Java per alcune conseguenze sorprendenti); gli scrittori di interfacce sono troppo lontani.

È facile estrarre l' AbstractListesempio; sarebbe bello se potessimo sbarazzarci di AbstractListe mettere il comportamento Listnell'interfaccia. Ma una volta superato questo ovvio esempio, non ci sono molti altri buoni esempi da trovare. Alla radice, AbstractListè progettato per singola eredità. Ma le interfacce devono essere progettate per l'ereditarietà multipla.

Inoltre, immagina di scrivere questa lezione:

class Foo implements com.libraryA.Bar, com.libraryB.Moo { 
    // Implementation of Foo, that does NOT override equals
}

Lo Fooscrittore osserva i supertipi, non vede l'implementazione di uguali e conclude che per ottenere l'uguaglianza di riferimento, tutto ciò che deve fare è ereditare uguali Object. Quindi, la settimana prossima, il manutentore della libreria per la barra "utile" aggiunge equalsun'implementazione predefinita . Ops! Ora la semantica di Fooè stata interrotta da un'interfaccia in un altro dominio di manutenzione "utile" aggiungendo un valore predefinito per un metodo comune.

I valori predefiniti dovrebbero essere predefiniti. L'aggiunta di un valore predefinito a un'interfaccia in cui non era presente (in qualsiasi punto della gerarchia) non dovrebbe influire sulla semantica delle classi di implementazione concrete. Ma se i valori predefiniti potessero "sovrascrivere" i metodi Object, ciò non sarebbe vero.

Quindi, sebbene sembri una funzionalità innocua, in realtà è piuttosto dannosa: aggiunge molta complessità per una piccola espressività incrementale e lo rende troppo facile per modifiche minacciose e dall'aspetto innocuo di interfacce compilate separatamente per minare la semantica prevista delle classi di attuazione.


13
Sono lieto che tu abbia dedicato del tempo a spiegarlo e apprezzo tutti i fattori considerati. Sono d'accordo che questo sarebbe pericoloso per hashCodee equals, ma penso che sarebbe molto utile per toString. Ad esempio, alcuni Displayableinterfaccia potrebbe definire un String display()metodo, e sarebbe risparmiare un sacco di boilerplate per poter definire default String toString() { return display(); }in Displayable, invece di richiedere ogni singolo Displayableimplementare toString()o estendere una DisplayableToStringclasse base.
Brandon,

8
@Brandon Hai ragione nel dire che consentire l'ereditarietà di toString () non sarebbe pericoloso come lo sarebbe per equals () e hashCode (). D'altra parte, ora la funzionalità sarebbe ancora più irregolare - e avresti comunque avuto la stessa complessità aggiuntiva sulle regole di eredità, per il bene di questo metodo ... sembra meglio tracciare la linea in modo pulito dove abbiamo fatto .
Brian Goetz,

5
@gexicide Se toString()si basa solo sui metodi di interfaccia, è possibile semplicemente aggiungere qualcosa di simile default String toStringImpl()all'interfaccia e sovrascrivere toString()in ciascuna sottoclasse per chiamare l'implementazione dell'interfaccia: un po 'brutta, ma funziona e meglio di niente. :) Un altro modo per farlo è quello di fare qualcosa di simile Objects.hash(), Arrays.deepEquals()e Arrays.deepToString(). Ho fatto +1 per la risposta di @ BrianGoetz!
Siu Ching Pong -Asuka Kenji-

3
Il comportamento predefinito di toString () di lambda è davvero odioso. So che la fabbrica lambda è progettata per essere molto semplice e veloce, ma sputare un nome di classe deragliato non è davvero utile. Avere una default toString()sostituzione in un'interfaccia funzionale ci consentirebbe - almeno - di fare qualcosa come sputare la firma della funzione e la classe genitrice dell'implementatore. Ancora meglio, se potessimo applicare alcune strategie ricorsive alle strategie di String, potremmo attraversare la chiusura per ottenere una descrizione davvero buona della lambda e quindi migliorare drasticamente la curva di apprendimento della lambda.
Groostav,

Qualsiasi modifica a ToString in qualsiasi classe, sottoclasse, membro di istanza potrebbe avere questo effetto sull'implementazione di classi o utenti di una classe. Inoltre, qualsiasi modifica a uno qualsiasi dei metodi predefiniti influirebbe probabilmente anche su tutte le classi di attuazione. Quindi cosa c'è di così speciale in toString, hashCode quando si tratta di qualcuno che modifica il comportamento di un'interfaccia? Se una classe estende un'altra classe, potrebbe anche cambiare. O se stanno usando il modello di delega. Qualcuno che utilizza le interfacce Java 8 dovrà farlo aggiornando. Potrebbe essere stato fornito un avviso / errore che può essere eliminato nella sottoclasse.
mmm

30

È vietato definire metodi predefiniti nelle interfacce per i metodi in java.lang.Object, poiché i metodi predefiniti non sarebbero mai "raggiungibili".

I metodi di interfaccia predefiniti possono essere sovrascritti nelle classi che implementano l'interfaccia e l'implementazione della classe del metodo ha una precedenza più alta rispetto all'implementazione dell'interfaccia, anche se il metodo è implementato in una superclasse. Poiché tutte le classi ereditano da java.lang.Object, i metodi in java.lang.Objectavrebbero la precedenza sul metodo predefinito nell'interfaccia e sarebbero invece invocati.

Brian Goetz di Oracle fornisce alcuni dettagli in più sulla decisione di progettazione in questo post della mailing list .


3

Non vedo nella testa degli autori del linguaggio Java, quindi possiamo solo immaginare. Ma vedo molte ragioni e sono assolutamente d'accordo con loro in questo numero.

Il motivo principale per l'introduzione di metodi predefiniti è di poter aggiungere nuovi metodi alle interfacce senza interrompere la retrocompatibilità delle implementazioni precedenti. I metodi predefiniti possono anche essere utilizzati per fornire metodi di "convenienza" senza la necessità di definirli in ciascuna delle classi di attuazione.

Nessuno di questi si applica a String e ad altri metodi di Object. In poche parole, i metodi predefiniti sono stati progettati per fornire il comportamento predefinito in cui non esiste altra definizione. Non fornire implementazioni che "competeranno" con altre implementazioni esistenti.

Anche la regola "la classe base vince sempre" ha le sue solide ragioni. Si suppone che le classi definiscano implementazioni reali , mentre le interfacce definiscono l' impostazione predefinita implementazioni , che sono in qualche modo più deboli.

Inoltre, l'introduzione di QUALSIASI eccezione alle regole generali causa complessità inutili e solleva altre domande. L'oggetto è (più o meno) una classe come qualsiasi altra, quindi perché dovrebbe avere un comportamento diverso?

Tutto sommato, la soluzione che proporresti probabilmente porterebbe più svantaggi che vantaggi.


Non ho notato il secondo paragrafo della risposta di gexicide quando ho pubblicato il mio. Contiene un collegamento che spiega il problema in modo più dettagliato.
Marwin,

1

Il ragionamento è molto semplice, è perché Object è la classe base per tutte le classi Java. Quindi, anche se abbiamo il metodo Object definito come metodo predefinito in alcune interfacce, sarà inutile perché verrà sempre utilizzato il metodo Object. Ecco perché per evitare confusione, non possiamo avere metodi predefiniti che sovrascrivono i metodi della classe Object.


1

Per dare una risposta molto pedante, è vietato definire un defaultmetodo per un metodo pubblico da java.lang.Object. Esistono 11 metodi da considerare, che possono essere classificati in tre modi per rispondere a questa domanda.

  1. Sei dei Objectmetodi non può avere defaultmetodi perché sono finale non può essere ignorata a tutti: getClass(), notify(), notifyAll(), wait(), wait(long), e wait(long, int).
  2. Tre dei Objectmetodi non può avere defaultmetodi per le ragioni sopra esposte da Brian Goetz: equals(Object), hashCode(), e toString().
  3. Due dei Objectmetodi possono avere defaultmetodi, sebbene il valore di tali valori predefiniti sia discutibile nella migliore delle ipotesi: clone()e finalize().

    public class Main {
        public static void main(String... args) {
            new FOO().clone();
            new FOO().finalize();
        }
    
        interface ClonerFinalizer {
            default Object clone() {System.out.println("default clone"); return this;}
            default void finalize() {System.out.println("default finalize");}
        }
    
        static class FOO implements ClonerFinalizer {
            @Override
            public Object clone() {
                return ClonerFinalizer.super.clone();
            }
            @Override
            public void finalize() {
                ClonerFinalizer.super.finalize();
            }
        }
    }

.Qual e il punto? Non hai ancora risposto alla parte PERCHÉ - "Allora, qual è il motivo per cui i progettisti Java hanno deciso di non consentire ai metodi predefiniti di sostituire i metodi dall'Object?"
pro_cheats
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.