Java8: Warum ist es verboten, eine Standardmethode für eine Methode aus java.lang.Object zu definieren?

130

Standardmethoden sind ein schönes neues Tool in unserer Java-Toolbox. Ich habe jedoch versucht, eine Schnittstelle zu schreiben, die eine defaultVersion der toStringMethode definiert. Java sagt mir, dass dies verboten ist, da in deklarierte Methoden java.lang.Objectmöglicherweise nicht defaultbearbeitet werden. Warum ist das so?

Ich weiß, dass es die Regel "Basisklasse gewinnt immer" gibt, daher würde standardmäßig (pun;) jede defaultImplementierung einer ObjectMethode von der Methode Objectsowieso überschrieben . Ich sehe jedoch keinen Grund, warum es keine Ausnahme für Methoden aus Objectder Spezifikation geben sollte. Insbesondere kann toStringes sehr nützlich sein, eine Standardimplementierung zu haben.

Was ist der Grund, warum Java-Designer beschlossen haben, defaultMethoden, die Methoden überschreiben , nicht zuzulassen Object?

Gexizid
quelle
1
Ich fühle mich jetzt so gut in Bezug auf mich selbst und stimme dies 100 Mal zu und somit ein goldenes Abzeichen. gute Frage!
Eugene

Antworten:

186

Dies ist ein weiteres Problem des Sprachdesigns, das "offensichtlich eine gute Idee" zu sein scheint, bis Sie anfangen zu graben und feststellen, dass es tatsächlich eine schlechte Idee ist.

Diese Mail hat viel zu diesem Thema (und auch zu anderen Themen). Es gab mehrere Designkräfte, die zusammen kamen, um uns zum aktuellen Design zu bringen:

  • Der Wunsch, das Vererbungsmodell einfach zu halten;
  • Die Tatsache, dass Sie, wenn Sie die offensichtlichen Beispiele hinter sich lassen (z. B. sich AbstractListin eine Schnittstelle verwandeln ), feststellen, dass das Erben von equals / hashCode / toString stark an die einzelne Vererbung und den Status gebunden ist und die Schnittstellen mehrfach vererbt und zustandslos sind.
  • Dass es möglicherweise die Tür für einige überraschende Verhaltensweisen geöffnet hat.

Sie haben bereits das Ziel "Einfach halten" angesprochen. Die Vererbungs- und Konfliktlösungsregeln sind sehr einfach gestaltet (Klassen gewinnen über Schnittstellen, abgeleitete Schnittstellen gewinnen über Superschnittstellen und alle anderen Konflikte werden von der implementierenden Klasse gelöst.) Natürlich könnten diese Regeln angepasst werden, um eine Ausnahme zu machen, aber Ich denke, Sie werden feststellen, wenn Sie an dieser Schnur ziehen, dass die inkrementelle Komplexität nicht so gering ist, wie Sie vielleicht denken.

Natürlich gibt es einen gewissen Nutzen, der mehr Komplexität rechtfertigen würde, aber in diesem Fall ist er nicht vorhanden. Die Methoden, über die wir hier sprechen, sind equals, hashCode und toString. Bei diesen Methoden geht es ausschließlich um den Objektstatus, und es ist die Klasse, die den Status besitzt, nicht die Schnittstelle, die am besten bestimmen kann, was Gleichheit für diese Klasse bedeutet (insbesondere, da der Vertrag für Gleichheit ziemlich stark ist; siehe Effektiv) Java für einige überraschende Konsequenzen); Interface-Writer sind einfach zu weit entfernt.

Es ist einfach, das AbstractListBeispiel herauszuholen . Es wäre schön, wenn wir AbstractListdas Verhalten loswerden und in die ListBenutzeroberfläche einfügen könnten . Wenn Sie jedoch über dieses offensichtliche Beispiel hinausgehen, gibt es nicht viele andere gute Beispiele. An der Wurzel AbstractListist für die Einzelvererbung ausgelegt. Schnittstellen müssen jedoch für Mehrfachvererbung ausgelegt sein.

Stellen Sie sich außerdem vor, Sie schreiben diese Klasse:

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

Der FooAutor betrachtet die Supertypen, sieht keine Implementierung von Gleichheit und kommt zu dem Schluss, dass er nur Gleichheit von erben muss, um Referenzgleichheit zu erhalten Object. Nächste Woche fügt der Bibliotheksbetreuer für Bar "hilfreich" eine Standardimplementierung hinzu equals. Ups! Jetzt wurde die Semantik von Foodurch eine Schnittstelle in einer anderen Wartungsdomäne "hilfreich" unterbrochen, indem ein Standard für eine allgemeine Methode hinzugefügt wurde.

Standardeinstellungen sollen Standardeinstellungen sein. Das Hinzufügen einer Standardeinstellung zu einer Schnittstelle, an der keine vorhanden war (irgendwo in der Hierarchie), sollte die Semantik konkreter Implementierungsklassen nicht beeinflussen. Aber wenn Standardeinstellungen Objektmethoden "überschreiben" könnten, wäre das nicht wahr.

Obwohl es als harmloses Feature erscheint, ist es in der Tat ziemlich schädlich: Es erhöht die Komplexität bei geringer inkrementeller Ausdruckskraft und macht es viel zu einfach, dass gut gemeinte, harmlos aussehende Änderungen an separat kompilierten Schnittstellen untergraben werden die beabsichtigte Semantik der Implementierung von Klassen.

Brian Goetz
quelle
13
Ich bin froh, dass Sie sich die Zeit genommen haben, dies zu erklären, und ich schätze alle Faktoren, die berücksichtigt wurden. Ich bin damit einverstanden, dass dies für hashCodeund gefährlich wäre equals, aber ich denke, es wäre sehr nützlich für toString. Zum Beispiel, einige Displayablekönnten Schnittstelle eine Definition String display()Methode, und es würde eine Tonne vorformulierten speichern zu können , definieren , default String toString() { return display(); }in Displayable, anstatt jeden einzelnen zu erfordern Displayableimplementieren toString()eine oder zu verlängern DisplayableToStringBasisklasse.
Brandon
8
@Brandon Sie haben Recht, dass das Erben von toString () nicht genauso gefährlich wäre wie für equals () und hashCode (). Auf der anderen Seite wäre das Feature jetzt noch unregelmäßiger - und Sie würden immer noch die gleiche zusätzliche Komplexität in Bezug auf Vererbungsregeln haben, um dieser einen Methode willen ... scheint es besser zu sein, die Grenze sauber zu ziehen, wo wir es getan haben .
Brian Goetz
5
@gexicide Wenn toString()es nur auf den Schnittstellenmethoden basiert, können Sie einfach etwas Ähnliches default String toStringImpl()zur Schnittstelle hinzufügen und toString()in jeder Unterklasse überschreiben , um die Schnittstellenimplementierung aufzurufen - ein bisschen hässlich, aber funktioniert und besser als nichts. :) Eine andere Möglichkeit , es zu tun ist , wie etwas zu machen Objects.hash(), Arrays.deepEquals()und Arrays.deepToString(). + 1'd für die Antwort von @ BrianGoetz!
Siu Ching Pong -Asuka Kenji-
3
Das Standardverhalten von toString () eines Lambdas ist wirklich widerlich. Ich weiß, dass die Lambda-Fabrik sehr einfach und schnell gestaltet ist, aber das Ausspucken eines abgeleiteten Klassennamens ist wirklich nicht hilfreich. Ein default toString()Override in einer funktionalen Schnittstelle würde es uns zumindest ermöglichen, die Signatur der Funktion und die übergeordnete Klasse des Implementierers auszuspucken. Noch besser, wenn wir einige rekursive Strings-Strategien zum Tragen bringen könnten, könnten wir durch den Abschluss gehen, um eine wirklich gute Beschreibung des Lambda zu erhalten und so die Lambda-Lernkurve drastisch zu verbessern.
Groostav
Jede Änderung von toString in einer Klasse, Unterklasse oder einem Instanzmitglied kann sich auf die Implementierung von Klassen oder Benutzern einer Klasse auswirken. Darüber hinaus würde jede Änderung einer der Standardmethoden wahrscheinlich auch alle implementierenden Klassen betreffen. Was ist das Besondere an toString, hashCode, wenn jemand das Verhalten einer Schnittstelle ändert? Wenn eine Klasse eine andere Klasse erweitert, können sie sich auch ändern. Oder wenn sie das Delegierungsmuster verwenden. Jemand, der Java 8-Schnittstellen verwendet, muss dies durch ein Upgrade tun. Eine Warnung / ein Fehler, der in der Unterklasse unterdrückt werden kann, könnte bereitgestellt worden sein.
mmm
30

Es ist verboten, Standardmethoden in Schnittstellen für Methoden in zu definieren java.lang.Object, da die Standardmethoden niemals "erreichbar" wären.

Standardschnittstellenmethoden können in Klassen, die die Schnittstelle implementieren, überschrieben werden, und die Klassenimplementierung der Methode hat eine höhere Priorität als die Schnittstellenimplementierung, selbst wenn die Methode in einer Oberklasse implementiert ist. Da alle Klassen von erben java.lang.Object, haben die Methoden in java.lang.ObjectVorrang vor der Standardmethode in der Schnittstelle und werden stattdessen aufgerufen.

Brian Goetz von Oracle bietet in diesem Mailinglistenbeitrag einige weitere Details zur Entwurfsentscheidung .

jarnbjo
quelle
3

Ich sehe nicht in den Kopf von Java-Sprachautoren, also können wir nur raten. Aber ich sehe viele Gründe und stimme ihnen in dieser Ausgabe absolut zu.

Der Hauptgrund für die Einführung von Standardmethoden besteht darin, Schnittstellen neue Methoden hinzufügen zu können, ohne die Abwärtskompatibilität älterer Implementierungen zu beeinträchtigen. Die Standardmethoden können auch verwendet werden, um "Convenience" -Methoden bereitzustellen, ohne dass sie in jeder der implementierenden Klassen definiert werden müssen.

Nichts davon gilt für String und andere Objektmethoden. Einfach ausgedrückt, wurden Standardmethoden entwickelt, um das Standardverhalten bereitzustellen, wenn es keine andere Definition gibt. Keine Implementierungen bereitzustellen, die mit anderen vorhandenen Implementierungen "konkurrieren".

Die Regel "Basisklasse gewinnt immer" hat auch ihre soliden Gründe. Es wird angenommen, dass Klassen reale Implementierungen definieren , während Schnittstellen Standardimplementierungen definieren , die etwas schwächer sind.

Die Einführung von Ausnahmen zu allgemeinen Regeln führt zu unnötiger Komplexität und wirft andere Fragen auf. Das Objekt ist (mehr oder weniger) eine Klasse wie jedes andere. Warum sollte es sich also anders verhalten?

Alles in allem würde die von Ihnen vorgeschlagene Lösung wahrscheinlich mehr Nachteile als Vorteile bringen.

Marwin
quelle
Ich habe den zweiten Absatz der Antwort von Gexicide nicht bemerkt, als ich meinen gepostet habe. Es enthält einen Link, der das Problem ausführlicher erklärt.
Marwin
1

Die Argumentation ist sehr einfach, da Object die Basisklasse für alle Java-Klassen ist. Selbst wenn die Methode von Object in einer Schnittstelle als Standardmethode definiert ist, ist sie nutzlos, da die Methode von Object immer verwendet wird. Aus diesem Grund können wir keine Standardmethoden verwenden, die Objektklassenmethoden überschreiben, um Verwirrung zu vermeiden.

Kumar Abhishek
quelle
1

Um eine sehr pedantische Antwort zu geben, ist es nur verboten, eine defaultMethode für eine öffentliche Methode aus zu definieren java.lang.Object. Es sind 11 Methoden zu berücksichtigen, die auf drei Arten kategorisiert werden können, um diese Frage zu beantworten.

  1. Sechs der ObjectMethoden können nicht haben defaultMethoden , weil sie sind finalund können überhaupt nicht außer Kraft gesetzt werden: getClass(), notify(), notifyAll(), wait(), wait(long), und wait(long, int).
  2. Drei der ObjectVerfahren haben nicht defaultMethoden aus den genannten Gründen oben von Brian Goetz: equals(Object), hashCode(), und toString().
  3. Zwei der ObjectMethoden können haben defaultMethoden, obwohl der Wert derartigen Ausfall bestenfalls fragwürdig ist: clone()und 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();
            }
        }
    }
jaco0646
quelle
.Was ist der Punkt? Sie haben den WARUM-Teil immer noch nicht beantwortet: "Was ist der Grund, warum Java-Designer beschlossen haben, keine Standardmethoden zuzulassen, die Methoden von Object überschreiben?"
pro_cheats