Viele Junior-Entwickler machen den Fehler, Interfaces, abstrakte und konkrete Klassen als leichte Variationen derselben Sache zu betrachten und eine davon rein aus technischen Gründen zu wählen: Brauche ich Mehrfachvererbung? Brauche ich einen Ort für gemeinsame Methoden? Muss ich mich um etwas anderes kümmern als nur um eine konkrete Klasse? Das ist falsch, und in diesen Fragen verbirgt sich das Hauptproblem: "Ich". Wenn du Code nur für dich selbst schreibst, denkst du selten an andere gegenwärtige oder zukünftige Entwickler, die an deinem Code arbeiten oder mit ihm interagieren werden.
Interfaces und abstrakte Klassen haben trotz ihrer technischen Ähnlichkeit vollkommen unterschiedliche Bedeutungen und Zwecke.
Zusammenfassung
-
Ein Interface definiert einen Vertrag, den irgendeine Implementierung für dich erfüllen wird.
-
Eine abstrakte Klasse bietet ein Standardverhalten, das deine Implementierung wiederverwenden kann.
Die obigen beiden Punkte sind das, wonach ich bei Bewerbungsgesprächen suche, und sie sind eine kompakte Zusammenfassung. Lies weiter für mehr Details.
Alternative Zusammenfassung
- Ein Interface dient zum Definieren öffentlicher APIs
- Eine abstrakte Klasse dient für den internen Gebrauch und zum Definieren von SPIs
Am Beispiel erklärt
Um es anders auszudrücken: Eine konkrete Klasse erledigt die eigentliche Arbeit, auf eine sehr spezifische Weise. Zum Beispiel verwendet ein ArrayList
einen zusammenhängenden Speicherbereich, um eine Liste von Objekten in kompakter Weise zu speichern, was schnellen zufälligen Zugriff, Iteration und Änderungen vor Ort bietet, aber schlecht bei Einfügungen, Löschungen und manchmal sogar bei Ergänzungen ist; währenddessen verwendet ein LinkedList
doppelt verkettete Knoten, um eine Liste von Objekten zu speichern, was stattdessen schnelle Iteration, Änderungen vor Ort und Einfügung/Löschung/Ergänzung bietet, aber schlecht bei zufälligem Zugriff ist. Diese beiden Arten von Listen sind für verschiedene Anwendungsfälle optimiert, und es ist wichtig, wie du sie verwenden wirst. Wenn du Leistung aus einer Liste herausholen möchtest, mit der du stark interagierst, und die Wahl des Listentyps bei dir liegt, solltest du sorgfältig überlegen, welchen du instanziierst.
Andererseits interessieren sich hochrangige Nutzer einer Liste nicht wirklich dafür, wie sie tatsächlich implementiert ist, und sie sollten von diesen Details abgeschirmt werden. Stell dir vor, Java nicht das List
Interface offengelegt hätte, sondern nur eine konkrete List
Klasse hätte, die tatsächlich das ist, was LinkedList
jetzt ist. Alle Java-Entwickler hätten ihren Code an die Implementierungsdetails angepasst: Vermeide zufälligen Zugriff, füge einen Cache hinzu, um den Zugriff zu beschleunigen, oder implementiere einfach ArrayList
selbst, obwohl es mit dem gesamten anderen Code, der nur mit List
funktioniert, inkompatibel wäre. Das wäre schrecklich... Aber stell dir jetzt vor, dass die Java-Meister tatsächlich erkennen, dass eine verkettete Liste für die meisten tatsächlichen Anwendungsfälle nicht geeignet ist, und beschließen, zu einer Array-List für ihre einzige verfügbare List
Klasse zu wechseln. Dies würde die Leistung jedes Java-Programms auf der ganzen Welt beeinflussen, und die Leute wären nicht glücklich darüber. Und der Hauptübeltäter ist, dass Implementierungsdetails verfügbar waren, und die Entwickler davon ausgegangen sind, dass diese Details einen dauerhaften Vertrag darstellen, auf den sie sich verlassen können. Deshalb ist es wichtig, Implementierungsdetails zu verbergen und nur einen abstrakten Vertrag zu definieren. Das ist der Zweck eines Interfaces: Definiere, welche Art von Eingabe eine Methode akzeptiert und welche Art von Ausgabe erwartet wird, ohne alle internen Details preiszugeben, die Programmierer dazu verleiten könnten, ihren Code an die internen Details anzupassen, die sich bei jedem zukünftigen Update ändern könnten.
Eine abstrakte Klasse steht zwischen Interfaces und konkreten Klassen. Sie soll Implementierungen dabei unterstützen, gemeinsamen oder langweiligen Code zu teilen. Zum Beispiel bietet die AbstractCollection
grundlegende Implementierungen für isEmpty
, basierend auf der Größe 0, contains
als Iteration und Vergleich, addAll
als wiederholtes add
usw. Dies ermöglicht es Implementierungen, sich auf die entscheidenden Teile zu konzentrieren, die sie voneinander unterscheiden: wie man Daten tatsächlich speichert und abruft.
Eine andere Perspektive: APIs gegenüber SPIs
Interfaces sind niedrig kohäsive Gateways zwischen verschiedenen Teilen des Codes. Sie ermöglichen es Bibliotheken zu existieren und sich weiterzuentwickeln, ohne jedes Mal alle Bibliotheksbenutzer zu brechen, wenn sich intern etwas ändert. Es heißt Application Programming Interface, nicht Application Programming Klassen. Auf kleinerer Ebene ermöglichen sie auch mehreren Entwicklern eine erfolgreiche Zusammenarbeit an groß angelegten Projekten, indem sie verschiedene Module durch gut dokumentierte Schnittstellen trennen.
Abstrakte Klassen sind hoch kohäsive Helfer, die verwendet werden, wenn eine Schnittstelle implementiert wird und einige Implementierungsdetails angenommen werden. Alternativ werden abstrakte Klassen zur Definition von SPIs, Service Provider Interfaces, verwendet.
Der Unterschied zwischen einer API und einem SPI ist subtil, aber wichtig: Bei einer API liegt der Fokus darauf, wer sie nutzt, und bei einem SPI liegt der Fokus darauf, wer sie implementiert.
Das Hinzufügen von Methoden zu einer API ist einfach, alle bestehenden Benutzer der API werden noch kompilieren. Das Hinzufügen von Methoden zu einem SPI ist schwierig, da jeder Dienstanbieter (konkrete Implementierung) die neuen Methoden implementieren muss. Wenn Interfaces verwendet werden, um ein SPI zu definieren, muss ein Anbieter eine neue Version veröffentlichen, wenn sich der SPI-Vertrag ändert. Werden stattdessen abstrakte Klassen verwendet, können neue Methoden entweder in Bezug auf bestehende abstrakte Methoden definiert werden oder als leere throw not implemented exception
Stubs, die es zumindest erlauben, dass eine ältere Version einer Dienstimplementierung noch kompiliert und funktioniert.
Eine Anmerkung zu Java 8 und Standardmethoden
Obwohl Java 8 Standardmethoden für Interfaces eingeführt hat, was die Grenze zwischen Interfaces und abstrakten Klassen noch verschwommener macht, geschah dies nicht, damit Implementierungen Code wiederverwenden können, sondern um es einfacher zu machen, Schnittstellen zu ändern, die sowohl als API als auch als SPI dienen (oder fälschlicherweise für die Definition von SPIs anstelle von abstrakten Klassen verwendet werden).
"Buchwissen"
Die technischen Details, die in der Antwort des OP bereitgestellt werden, gelten als "Buchwissen", weil dies normalerweise der Ansatz ist, der in der Schule und in den meisten Technologiebüchern über eine Sprache verwendet wird: was eine Sache ist, nicht wie man sie in der Praxis verwendet, insbesondere in groß angelegten Anwendungen.
Hier ist eine Analogie: Angenommen, die Frage war:
Was ist besser für den Abschlussball zu mieten, ein Auto oder ein Hotelzimmer?
Die technische Antwort hört sich so an:
Nun, in einem Auto kannst du es schneller machen, aber in einem Hotelzimmer kannst du es bequemer machen. Auf der anderen Seite ist das Hotelzimmer nur an einem Ort, während du in einem Auto an mehreren Orten, wie zum Beispiel am Aussichtspunkt für eine schöne Aussicht, oder im Autokino, oder vielen anderen Orten, oder sogar an mehreren Orten, machen kannst. Außerdem hat das Hotelzimmer eine Dusche.
Das ist alles wahr, aber verfehlt vollkommen den Punkt, dass es sich um zwei völlig verschiedene Dinge handelt und beide gleichzeitig für unterschiedliche Zwecke verwendet werden können und der "den es zu machen" Aspekt nicht das Wichtigste an einer der beiden Optionen ist. Die Antwort fehlt der Perspektive, und zeigt eine unreife Denkweise, während sie korrekte "Fakten" präsentiert.