it-swarm.com.de

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

In Java 8 können Schnittstellen implementierte Methoden, statische Methoden und die sogenannten "Standard" -Methoden enthalten (die die implementierenden Klassen nicht überschreiben müssen).

Meiner (wahrscheinlich naiven) Ansicht nach 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 Utility-Klassen.
  2. "Standard" -Methoden sollten in Schnittstellen überhaupt nicht erlaubt sein. Sie können zu diesem Zweck 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 von 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.
  • Beim Programmieren der Implementierungen vergessen Programmierer möglicherweise, die Standardmethoden zu überschreiben.
  • Es tritt ein Kompilierungsfehler auf, 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 für diese neue Funktionalität entwickelt, was zu Problemen führen kann. Wenn beispielsweise jemand einer Schnittstelle Ix eine neue Standardmethode default void foo() hinzufügt, implementiert die Klasse CxIx und verfügt über ein privates foo mit derselben Signatur wird nicht kompiliert.

Was sind die Hauptgründe für solche großen Änderungen und welche neuen Vorteile (falls vorhanden) bieten sie?

103
Mister Smith

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

list.sort(ordering);

anstatt

Collections.sort(list, ordering);

Ich glaube nicht, dass sie das anders hätten tun können, ohne mehr als eine identische Implementierung von List.sort.

59
soru

Die richtige Antwort finden Sie in der Java-Dokumentation , in der es heißt:

[d] Mit den Standardmethoden 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 ist seit langem eine Schmerzquelle in Java, da sich Schnittstellen nach ihrer Veröffentlichung in der Regel nicht mehr weiterentwickeln konnten. (Der Inhalt der Dokumentation bezieht sich auf das Dokument, auf das Sie in einem Kommentar verwiesen haben: Schnittstellenentwicklung über virtuelle Erweiterungsmethoden .) Darüber hinaus schnelle Übernahme neuer Funktionen (z. B. Lambdas und neue Stream-APIs) Dies kann nur durch Erweitern der vorhandenen Sammlungsschnittstellen und Bereitstellen von Standardimplementierungen erfolgen. Eine Unterbrechung der Binärkompatibilität oder die Einführung neuer APIs würde bedeuten, dass mehrere Jahre vergehen würden, bevor Java 8 allgemein verwendet würden).

Der Grund für das Zulassen statischer Methoden in Schnittstellen wird in der Dokumentation erneut aufgezeigt: [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.Collections kann nun (endlich) allgemein als Anti-Muster betrachtet werden (natürlich nicht immer ). 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 geschehen wäre.

In ähnlicher Weise ist ein Beispiel dafür, wie diese neuen Funktionen von Nutzen sein können, die Betrachtung einer Klasse, die mich kürzlich geärgert hat: Java.util.UUID . Es bietet keine wirkliche Unterstützung für ID-Typen 1, 2 oder 5 und kann nicht ohne weiteres geändert werden, um dies zu tun. Es steckt auch in 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 Wartung des Konvertierungscodes und die damit verbundenen Kosten für die zusätzliche Speicherbereinigung. Mit statischen Methoden könnte UUID stattdessen als Schnittstelle definiert worden sein, um echte Implementierungen der fehlenden Teile durch Dritte zu ermöglichen. (Wenn UUID ursprünglich als Schnittstelle definiert wäre, hätten wir wahrscheinlich eine klobige UuidUtil -Klasse mit statischen Methoden, was ebenfalls schrecklich wäre.) Viele der Java-Kern-APIs werden durch beeinträchtigt Sie konnten sich nicht auf Schnittstellen stützen, aber seit Java 8) hat sich die Anzahl der Ausreden 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 den Status haben können (dh deklarieren können) Felder), während Schnittstellen nicht können. Es ist daher nicht gleichbedeutend mit Mehrfachvererbung oder sogar Vererbung im Mixin-Stil. Richtige Mixins (wie Groovy 2.3s Merkmale ) haben Zugriff auf den Status. (Groovy unterstützt auch statische Erweiterungsmethoden.)

Es ist meiner Meinung nach auch keine gute Idee, dem Beispiel von Doval zu folgen. Eine Schnittstelle soll einen Vertrag definieren, aber den Vertrag nicht durchsetzen. (Nicht in Java sowieso.) Die ordnungsgemäße Überprüfung einer Implementierung liegt in der Verantwortung einer Testsuite oder eines anderen Tools. Das Definieren von Verträgen kann mit Anmerkungen erfolgen, und OVal ist Ein gutes Beispiel, aber ich weiß nicht, ob es Einschränkungen unterstützt, die für Schnittstellen definiert sind. Ein solches System ist machbar, auch wenn es derzeit keines gibt. (Strategien umfassen die Anpassung von javac zur Kompilierungszeit über - Annotation Processor API- und Laufzeit-Bytecode-Generierung.) Im Idealfall werden Verträge zur Kompilierungszeit und im schlimmsten Fall mithilfe einer Testsuite erzwungen, aber nach meinem Verständnis ist die Durchsetzung der Laufzeit verpönt. Ein weiteres interessantes Tool Dies könnte die Vertragsprogrammierung in Java ist das Checker Framework .

50
ngreen

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 es jedem Implementierer der Schnittstelle ermöglichen, 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.


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

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

    T pop() throws EmptyException;
 }

Es gibt keine Garantie dafür, dass pop bei der Implementierung der Schnittstelle eine Ausnahme auslöst, wenn der Stapel leer ist. Wir könnten diese Regel durchsetzen, indem wir pop in zwei Methoden aufteilen: eine public final - Methode, die den Vertrag erzwingt, und eine protected abstract - Methode, die das eigentliche Poppen ausfü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 davon befreit, prüfen zu müssen, ob der Stapel leer ist, und die Ausnahme auszulösen. Es ist ein großer Gewinn! ... bis auf die Tatsache, dass wir die Schnittstelle in eine abstrakte Klasse ändern 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. Die Möglichkeit, Implementierungen bereitzustellen, 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 ermöglicht, aber ich kenne die D-Sprache erlaubt es und bietet native Unterstützung für Design by Contract . Bei dieser Technik besteht keine Gefahr, da pop endgültig ist und daher keine implementierende Klasse sie überschreiben kann.

Bei Standardimplementierungen überschreibbarer Methoden gehe ich davon aus, dass Standardimplementierungen, die 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 korrekt mit der Standardeinstellung Implementierungen.

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 keine Felder in einer Schnittstelle deklarieren können. Jede Methode, die Sie in eine Schnittstelle schreiben, kann sich nicht auf Implementierungsdetails verlassen.


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.unmodifiableList Hätte genauso gut in der List -Schnittstelle deklariert werden können, und es wäre einfacher zu finden gewesen.

44
Doval

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

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

2
rae1

Die zwei Hauptzwecke, die ich in default Methoden 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ürden Sie das in einer brandneuen Oberfläche wie Predicate nicht sehen. Alle mit @FunctionalInterface Annotierten Schnittstellen müssen genau eine abstrakte Methode haben, damit ein Lambda sie implementieren kann. Hinzugefügte default Methoden wie and, or, negate sind nur Dienstprogramme, und Sie sollten sie nicht überschreiben. Jedoch manchmal würden statische Methoden besser funktionieren .

Was die Erweiterung bestehender Schnittstellen betrifft - auch dort sind einige neue Methoden nur Syntaxzucker. Methoden von Collection wie stream, forEach, removeIf - im Grunde ist es nur ein Dienstprogramm, das Sie nicht überschreiben müssen. Und dann gibt es Methoden wie spliterator. Die Standardimplementierung ist nicht optimal, aber hey, zumindest der Code wird kompiliert. Greifen Sie nur darauf zurück, wenn Ihre Benutzeroberfläche bereits veröffentlicht und weit verbreitet ist.


Die static -Methoden werden wohl von den anderen recht gut behandelt: Sie ermöglicht, dass die Schnittstelle eine eigene Dienstprogrammklasse ist. Vielleicht könnten wir Collections in Javas Zukunft loswerden? Set.empty() würde rocken.

1
Vlasec