Warum wurden Schnittstellen in Java 8 Standardmethoden und statische Methoden hinzugefügt, als wir bereits abstrakte Klassen hatten?

99

In Java 8 können Schnittstellen implementierte Methoden, statische Methoden und sogenannte "Standard" -Methoden enthalten (die von den implementierenden Klassen nicht überschrieben werden müssen).

Aus meiner (wahrscheinlich naiven) Sicht bestand keine Notwendigkeit, solche Schnittstellen zu verletzen. Schnittstellen waren schon immer ein Vertrag, den Sie erfüllen müssen, und dies ist ein sehr einfaches und reines Konzept. Jetzt ist es eine Mischung aus mehreren Dingen. Meiner Meinung nach:

  1. statische Methoden gehören nicht zu Schnittstellen. Sie gehören zu den Gebrauchsklassen.
  2. "Standard" -Methoden sollten in Interfaces überhaupt nicht erlaubt sein. Zu diesem Zweck können Sie immer eine abstrakte Klasse verwenden.

Zusamenfassend:

Vor Java 8:

  • Sie können abstrakte und reguläre Klassen verwenden, um statische und Standardmethoden bereitzustellen. Die Rolle der Schnittstellen ist klar.
  • Alle Methoden in einer Schnittstelle sollten durch die Implementierung von Klassen überschrieben werden.
  • Sie können einer Schnittstelle keine neue Methode hinzufügen, ohne alle Implementierungen zu ändern, aber das ist eigentlich eine gute Sache.

Nach Java 8:

  • Es gibt praktisch keinen Unterschied zwischen einer Schnittstelle und einer abstrakten Klasse (außer Mehrfachvererbung). Tatsächlich können Sie eine reguläre Klasse mit einer Schnittstelle emulieren.
  • Bei der Programmierung der Implementierungen vergessen Programmierer möglicherweise, die Standardmethoden zu überschreiben.
  • Es liegt ein Kompilierungsfehler vor, wenn eine Klasse versucht, zwei oder mehr Schnittstellen mit einer Standardmethode mit derselben Signatur zu implementieren.
  • Durch Hinzufügen einer Standardmethode zu einer Schnittstelle erbt jede implementierende Klasse automatisch dieses Verhalten. Einige dieser Klassen wurden möglicherweise nicht mit dieser neuen Funktionalität entwickelt, was zu Problemen führen kann. Wenn beispielsweise jemand default void foo()einer Schnittstelle eine neue Standardmethode hinzufügt Ix, wird die Klasse , die eine private Methode mit derselben Signatur Cximplementiert Ixund foobesitzt, nicht kompiliert.

Was sind die Hauptgründe für diese großen Änderungen und welche neuen Vorteile (falls vorhanden) bringen sie mit sich?

Herr Smith
quelle
30
Bonusfrage: Warum haben sie stattdessen keine Mehrfachvererbung für Klassen eingeführt?
2
statische Methoden gehören nicht zu Schnittstellen. Sie gehören zu den Gebrauchsklassen. Nein, sie gehören in die @DeprecatedKategorie! statische Methoden sind aufgrund von Unwissenheit und Faulheit eines der am häufigsten missbrauchten Konstrukte in Java. Viele statische Methoden bedeuten normalerweise inkompetente Programmierer, erhöhen die Kopplung um mehrere Größenordnungen und sind ein Albtraum für Unit-Tests und Refactors, wenn Sie erkennen, warum sie eine schlechte Idee sind!
11
@JarrodRoberson Kannst du weitere Hinweise (Links wären großartig) zu "Sind ein Albtraum für Unit-Tests und Refactor, wenn du erkennst, warum sie eine schlechte Idee sind!" Geben? Das hätte ich nie gedacht und ich würde gerne mehr darüber erfahren.
wässrig
11
@Chris Die mehrfache Vererbung von Zuständen verursacht eine Reihe von Problemen, insbesondere bei der Speicherzuweisung ( das klassische Diamantproblem ). Die mehrfache Vererbung von Verhalten hängt jedoch nur von der Implementierung ab, die den bereits von der Schnittstelle festgelegten Vertrag erfüllt (die Schnittstelle kann andere deklarierte und erforderliche Methoden aufrufen). Eine subtile, aber sehr interessante Unterscheidung.
SSUBE
1
Sie möchten eine Methode zu einer bestehenden Schnittstelle hinzufügen, was vor Java8 umständlich oder fast unmöglich war. Jetzt können Sie es einfach als Standardmethode hinzufügen.
VdeX

Antworten:

59

Ein gutes motivierendes Beispiel für Standardmethoden ist die Java-Standardbibliothek, über die Sie jetzt verfügen

list.sort(ordering);

Anstatt von

Collections.sort(list, ordering);

Ich glaube nicht, dass sie das sonst ohne mehr als eine identische Implementierung von getan hätten List.sort.

Soru
quelle
19
C # überwindet dieses Problem mit Erweiterungsmethoden.
Robert Harvey
5
und es erlaubt eine verknüpfte Liste eine O zu verwenden (1) besonders vielen Platz und O Zeit mergesort (n log n), da verkettete Listen können an Ort und Stelle zusammengefügt werden, in Java - 7 - dump zu einem externen Array und dann sortieren , die
Ratsche Ungewöhnlich
5
Ich habe dieses Papier gefunden, in dem Goetz das Problem erklärt. Deshalb werde ich diese Antwort vorerst als Lösung markieren.
Mister Smith
1
@RobertHarvey: Erstellen Sie eine Liste mit zweihundert Millionen Einträgen <Byte>, fügen Sie IEnumerable<Byte>.Appendsie hinzu, rufen Sie an und erklären Sie Countmir, wie Erweiterungsmethoden das Problem lösen. Wenn CountIsKnownund Countwaren Mitglieder von IEnumerable<T>, könnte die Rückkehr von Appendwerben, CountIsKnownwenn die konstituierenden Sammlungen dies taten, aber ohne solche Methoden ist das nicht möglich.
Supercat
6
@supercat: Ich habe nicht die geringste Ahnung, wovon du sprichst.
Robert Harvey
48

Die richtige Antwort findet sich tatsächlich in der Java-Dokumentation , in der es heißt:

Mit [d] efault-Methoden können Sie den Schnittstellen Ihrer Bibliotheken neue Funktionen hinzufügen und die Binärkompatibilität mit Code sicherstellen, der für ältere Versionen dieser Schnittstellen geschrieben wurde.

Dies hat Java schon seit langem geschmerzt, da sich Schnittstellen nach ihrer Veröffentlichung in der Regel nicht mehr weiterentwickeln ließen. (Der Inhalt der Dokumentation bezieht sich auf das Dokument, auf das Sie in einem Kommentar verwiesen haben: Schnittstellenentwicklung über virtuelle Erweiterungsmethoden .) Darüber hinaus kann eine schnelle Übernahme neuer Funktionen (z. B. Lambdas und die neuen Stream-APIs) nur durch Erweiterung der API erfolgen Schnittstellen für vorhandene Sammlungen und Bereitstellung von Standardimplementierungen. Wenn Sie die Binärkompatibilität aufheben oder neue APIs einführen, vergehen einige Jahre, bis die wichtigsten Funktionen von Java 8 gemeinsam genutzt werden.

Der Grund für das Zulassen statischer Methoden in Interfaces geht aus der Dokumentation erneut hervor: [t] Dies erleichtert Ihnen das Organisieren von Hilfsmethoden in Ihren Bibliotheken. Sie können statische Methoden, die für eine Schnittstelle spezifisch sind, in derselben Schnittstelle und nicht in einer separaten Klasse aufbewahren. Mit anderen Worten, statische Dienstprogrammklassen wie java.util.Collectionsjetzt (endlich) können allgemein (natürlich nicht immer ) als Antimuster betrachtet werden . Ich vermute, dass das Hinzufügen von Unterstützung für dieses Verhalten nach der Implementierung virtueller Erweiterungsmethoden trivial war, da dies sonst wahrscheinlich nicht erfolgt wäre.

Ein Beispiel dafür, wie diese neuen Funktionen von Nutzen sein können, ist die Betrachtung einer Klasse, die mich in letzter Zeit geärgert hat java.util.UUID. Es bietet keine Unterstützung für die UUID-Typen 1, 2 oder 5 und kann nicht ohne weiteres geändert werden. Es hängt auch an einem vordefinierten Zufallsgenerator, der nicht überschrieben werden kann. Das Implementieren von Code für die nicht unterstützten UUID-Typen erfordert entweder eine direkte Abhängigkeit von einer Drittanbieter-API anstelle einer Schnittstelle oder die Verwaltung des Konvertierungscodes und die damit verbundenen Kosten für die zusätzliche Garbage Collection. Bei statischen Methoden, UUIDkönnte stattdessen als eine Schnittstelle definiert, so dass echte Fremd Implementierungen der fehlenden Stücke. (Wenn UUIDwir ursprünglich als Schnittstelle definiert wären, hätten wir wahrscheinlich eine Art klobigesUuidUtil Klasse mit statischen Methoden, was auch schrecklich wäre.) Viele der Java-Kern-APIs werden dadurch herabgesetzt, dass sie sich nicht auf Schnittstellen stützen, aber seit Java 8 hat sich die Anzahl der Entschuldigungen für dieses schlechte Verhalten dankenswerterweise verringert.

Es ist nicht richtig zu sagen, dass [t] hier praktisch kein Unterschied zwischen einer Schnittstelle und einer abstrakten Klasse besteht , da abstrakte Klassen einen Zustand haben können (dh Felder deklarieren), während Schnittstellen dies nicht können. Es ist daher nicht gleichbedeutend mit Mehrfachvererbung oder sogar einer Vererbung im Mixin-Stil. Richtige Mixins (wie Groovy 2.3s Merkmale ) haben Zugriff auf den Status. (Groovy unterstützt auch statische Erweiterungsmethoden.)

Meiner Meinung nach ist es auch keine gute Idee, Dovals Beispiel zu folgen . Eine Schnittstelle soll einen Vertrag definieren, aber nicht den Vertrag erzwingen. (Auf jeden Fall nicht in Java.) Die ordnungsgemäße Überprüfung einer Implementierung liegt in der Verantwortung einer Testsuite oder eines anderen Tools. Das Definieren von Verträgen könnte mit Anmerkungen erfolgen, und OVal ist ein gutes Beispiel, aber ich weiß nicht, ob es Einschränkungen unterstützt, die für Schnittstellen definiert wurden. Ein solches System ist machbar, auch wenn es derzeit noch kein System gibt. (Zu den Strategien gehört die Anpassung zur Kompilierungszeit javacüber den AnmerkungsprozessorAPI- und Laufzeit-Bytecode-Generierung.) Im Idealfall werden Verträge zur Kompilierungszeit und im schlimmsten Fall mit einer Testsuite erzwungen, aber ich verstehe, dass die Laufzeiterzwingung verpönt ist. Ein weiteres interessantes Tool zur Unterstützung der Vertragsprogrammierung in Java ist das Checker Framework .

ngreen
quelle
1
Zur weiteren Bearbeitung meines letzten Absatzes (dh keine Verträge in Schnittstellen erzwingen ) ist darauf hinzuweisen, dass defaultMethoden nicht überschreiben equalskönnen hashCodeund toString. Eine sehr aussagekräftige Kosten-Nutzen-Analyse, warum dies nicht zulässig
ngreen 06.10.14
Es ist schade, dass Java nur eine einzige virtuelle equalsund eine einzige hashCodeMethode hat, da es zwei verschiedene Arten von Gleichheit gibt, die Sammlungen möglicherweise testen müssen, und Elemente, die mehrere Schnittstellen implementieren, möglicherweise mit widersprüchlichen vertraglichen Anforderungen verbunden sind. Die Verwendung von Listen, die sich nicht als hashMapSchlüssel ändern, ist hilfreich. Manchmal ist es jedoch hilfreich, Sammlungen in einer Reihenfolge zu speichern, hashMapdie eher auf der Äquivalenz als auf dem aktuellen Status basiert. [Äquivalenz impliziert Übereinstimmungsstatus und Unveränderlichkeit ] .
Supercat
Nun, Java hat eine Art Workaround mit den Schnittstellen Comparator und Comparable. Aber die sind irgendwie hässlich, denke ich.
ngreen
Diese Schnittstellen werden nur für bestimmte Auflistungstypen unterstützt und stellen ein eigenes Problem dar: Ein Komparator kann möglicherweise selbst den Status einkapseln (z. B. kann ein spezialisierter Zeichenfolgenkomparator eine konfigurierbare Anzahl von Zeichen am Anfang jeder Zeichenfolge ignorieren, in diesem Fall die Anzahl von Zu ignorierende Zeichen würden Teil des Komparatorstatus sein, der wiederum Teil des Status einer Sammlung wird, die danach sortiert wurde. Es gibt jedoch keinen definierten Mechanismus, um zwei Komparatoren zu fragen, ob sie gleichwertig sind.
Supercat
Oh ja, ich habe den Schmerz von Komparatoren. Ich arbeite an einer Baumstruktur, die eigentlich einfach sein sollte, aber nicht, weil es so schwer ist, den richtigen Vergleicher zu finden. Ich werde wahrscheinlich eine benutzerdefinierte Baumklasse schreiben, um das Problem zu lösen.
ngreen
44

Weil Sie nur eine Klasse erben können. Wenn Sie zwei Schnittstellen haben, deren Implementierungen so komplex sind, dass Sie eine abstrakte Basisklasse benötigen, schließen sich diese beiden Schnittstellen in der Praxis gegenseitig aus.

Die Alternative besteht darin, diese abstrakten Basisklassen in eine Sammlung statischer Methoden zu konvertieren und alle Felder in Argumente umzuwandeln. Das würde jedem Implementierer der Schnittstelle erlauben, die statischen Methoden aufzurufen und die Funktionalität zu erhalten, aber es ist eine Menge Boilerplate in einer Sprache, die bereits viel zu ausführlich ist.


Als ein motivierendes Beispiel dafür, warum es nützlich sein kann, Implementierungen in Schnittstellen bereitzustellen, betrachten Sie diese Stack-Schnittstelle:

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

    T pop() throws EmptyException;
 }

Es kann nicht garantiert werden, dass bei der Implementierung der Schnittstelle popeine Ausnahme ausgelöst wird, wenn der Stapel leer ist. Wir könnten diese Regel durchsetzen, indem wir sie popin zwei Methoden public finalaufteilen : eine Methode, die den Vertrag erzwingt, und eine protected abstractMethode, die das eigentliche Poppen durchführt.

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();
        }
    }
 }

Wir stellen nicht nur sicher, dass alle Implementierungen den Vertrag einhalten, sondern haben sie auch von der Notwendigkeit befreit, zu überprüfen, ob der Stack leer ist und die Ausnahme auszulösen. Es ist ein großer Gewinn! ... bis auf die Tatsache, dass wir das Interface in eine abstrakte Klasse verwandeln mussten. In einer Sprache mit einfacher Vererbung bedeutet dies einen großen Verlust an Flexibilität. Dadurch schließen sich Ihre potenziellen Schnittstellen gegenseitig aus. Implementierungen bereitstellen zu können, die nur auf den Schnittstellenmethoden selbst beruhen, würde das Problem lösen.

Ich bin nicht sicher, ob der Ansatz von Java 8 zum Hinzufügen von Methoden zu Schnittstellen das Hinzufügen endgültiger Methoden oder geschützter abstrakter Methoden zulässt , aber ich weiß, dass die D-Sprache dies zulässt und native Unterstützung für Design by Contract bietet . Diese Technik birgt keine Gefahr, da sie popendgültig ist und daher von keiner implementierenden Klasse außer Kraft gesetzt werden kann.

In Bezug auf Standardimplementierungen überschreibbarer Methoden gehe ich davon aus, dass Standardimplementierungen, die zu den Java-APIs hinzugefügt werden, nur vom Vertrag der Schnittstelle abhängen, zu der sie hinzugefügt wurden. Daher verhält sich jede Klasse, die die Schnittstelle korrekt implementiert, auch mit den Standardimplementierungen korrekt.

Außerdem,

Es gibt praktisch keinen Unterschied zwischen einer Schnittstelle und einer abstrakten Klasse (außer Mehrfachvererbung). Tatsächlich können Sie eine reguläre Klasse mit einer Schnittstelle emulieren.

Dies ist nicht ganz richtig, da Sie in einer Schnittstelle keine Felder deklarieren können. Jede Methode, die Sie in eine Schnittstelle schreiben, kann sich nicht auf Implementierungsdetails stützen.


Betrachten Sie als Beispiel für statische Methoden in Schnittstellen Dienstprogrammklassen wie Sammlungen in der Java-API. Diese Klasse existiert nur, weil diese statischen Methoden nicht in ihren jeweiligen Schnittstellen deklariert werden können. Collections.unmodifiableListhätte genauso gut in der ListOberfläche deklariert werden können, und es wäre leichter zu finden gewesen.

Doval
quelle
4
Gegenargument: Da statische Methoden, sofern sie ordnungsgemäß geschrieben wurden, in sich geschlossen sind, sind sie in einer separaten statischen Klasse sinnvoller, in der sie nach Klassennamen zusammengefasst und kategorisiert werden können, und weniger sinnvoll in einer Schnittstelle, in der sie im Wesentlichen eine Annehmlichkeit darstellen Dies führt zu Missbräuchen wie dem Halten des statischen Zustands im Objekt oder dem Verursachen von Nebenwirkungen, wodurch die statischen Methoden nicht mehr getestet werden können.
Robert Harvey
3
@RobertHarvey Was hindert Sie daran, genauso dumme Dinge zu tun, wenn sich Ihre statische Methode jedoch in einer Klasse befindet? Außerdem erfordert die Methode in der Schnittstelle möglicherweise überhaupt keinen Status. Sie versuchen möglicherweise nur, einen Vertrag durchzusetzen. Angenommen, Sie haben eine StackSchnittstelle und möchten sicherstellen, dass beim popAufrufen mit einem leeren Stapel eine Ausnahme ausgelöst wird. Angesichts abstrakter Methoden boolean isEmpty()und protected T pop_impl()könnten Sie implementieren. final T pop() { isEmpty()) throw PopException(); else return pop_impl(); }Dies erzwingt den Vertrag für ALLE Implementierer.
Doval
Warte was? Push- und Pop - Methoden auf einem Stapel sind nicht sein werden static.
Robert Harvey
@RobertHarvey Ich wäre klarer gewesen, wenn nicht die Zeichenbeschränkung für Kommentare, aber ich habe mich für Standardimplementierungen in einer Schnittstelle ausgesprochen, nicht für statische Methoden.
Doval
8
Ich denke, Standardschnittstellenmethoden sind eher ein Hack, der eingeführt wurde, um die Standardbibliothek erweitern zu können, ohne den vorhandenen Code darauf basierend anpassen zu müssen.
Giorgio
2

Vielleicht bestand die Absicht darin, die Möglichkeit zum Erstellen von Mixin- Klassen bereitzustellen, indem die Notwendigkeit, statische Informationen oder Funktionen über eine Abhängigkeit einzufügen , ersetzt wurde.

Diese Idee scheint damit zu tun zu haben, wie Sie Erweiterungsmethoden in C # verwenden können, um den Schnittstellen implementierte Funktionen hinzuzufügen.

rae1
quelle
1
Erweiterungsmethoden erweitern Schnittstellen nicht um Funktionen. Erweiterungsmethoden sind nur syntaktischer Zucker zum Aufrufen statischer Methoden für eine Klasse mithilfe des praktischen list.sort(ordering);Formulars.
Robert Harvey
Wenn Sie sich die IEnumerableSchnittstelle in C # ansehen , können Sie sehen, wie das Implementieren von Erweiterungsmethoden für diese Schnittstelle (wie es der LINQ to ObjectsFall ist) die Funktionalität für jede implementierte Klasse hinzufügt IEnumerable. Das habe ich mit Funktionalität gemeint.
rae1
2
Das ist das Tolle an Erweiterungsmethoden; Sie geben die Illusion, dass Sie die Funktionalität einer Klasse oder Schnittstelle hinzufügen. Verwechseln Sie dies nur nicht mit dem Hinzufügen tatsächlicher Methoden zu einer Klasse. Klassenmethoden haben Zugriff auf die privaten Member eines Objekts, Erweiterungsmethoden nicht (da sie in Wirklichkeit nur eine andere Art sind, statische Methoden aufzurufen).
Robert Harvey
2
Genau, und deshalb sehe ich eine gewisse Beziehung zum Vorhandensein statischer oder Standardmethoden in einer Schnittstelle in Java. Die Implementierung basiert auf dem, was der Schnittstelle zur Verfügung steht, nicht der Klasse selbst.
Rae1
1

Die beiden Hauptziele, die ich in defaultMethoden sehe (einige Anwendungsfälle dienen beiden Zwecken):

  1. Syntax Zucker. Eine Utility-Klasse könnte diesen Zweck erfüllen, aber Instanzmethoden sind besser.
  2. Erweiterung einer bestehenden Schnittstelle. Die Implementierung ist allgemein, aber manchmal ineffizient.

Wenn es nur um den zweiten Zweck ginge, würde man das nicht in einer brandneuen Benutzeroberfläche wie sehen Predicate. Alle mit @FunctionalInterfaceAnnotationen versehenen Schnittstellen müssen genau eine abstrakte Methode haben, damit ein Lambda sie implementieren kann. Hinzugefügt defaultMethoden wie and, or, negatesind nur Dienstprogramm, und Sie sind nicht zu überschreiben , sie soll. Doch manchmal statische Methoden täten besser .

Was die Erweiterung bestehender Schnittstellen angeht - selbst dort sind einige neue Methoden nur Syntaxzucker. Methoden Collectionwie stream, forEach, removeIf- im Grunde, es ist nur Dienstprogramm , das Sie nicht außer Kraft setzen müssen. Und dann gibt es Methoden wie spliterator. Die Standardimplementierung ist suboptimal, aber zumindest der Code wird kompiliert. Greifen Sie nur dann darauf zurück, wenn Ihr Interface bereits veröffentlicht und weit verbreitet ist.


Was die staticMethoden, ich denke , es die andere decken ganz gut: Es ermöglicht die Schnittstelle ihre eigene Utility - Klasse zu sein. Vielleicht könnten wir uns Collectionsin Javas Zukunft davonmachen? Set.empty()würde rocken.

Vlasec
quelle