Warum wurden Java-Sammlungen mit „optionalen Methoden“ in der Oberfläche implementiert?

69

Bei meiner ersten Implementierung zur Erweiterung des Java-Erfassungsframeworks war ich ziemlich überrascht, dass die Erfassungsschnittstelle als optional deklarierte Methoden enthält. Es wird erwartet, dass der Implementierer UnsupportedOperationExceptions auslöst, wenn dies nicht unterstützt wird. Dies fiel mir sofort als schlechte Wahl für das API-Design auf.

Nachdem er viel von Joshua Blochs exzellentem Buch "Effective Java" gelesen und später erfahren hatte, dass er möglicherweise für diese Entscheidungen verantwortlich ist, schien es nicht mit den in dem Buch vertretenen Prinzipien übereinzustimmen. Ich würde denken, zwei Schnittstellen zu deklarieren: Collection und MutableCollection, die Collection mit den "optionalen" Methoden erweitern, hätten zu viel besser wartbarem Client-Code geführt.

Es gibt eine ausgezeichnete Zusammenfassung der Probleme hier .

Gab es einen guten Grund, warum anstelle der Implementierung der beiden Schnittstellen optionale Methoden ausgewählt wurden?

Glenviewjeff
quelle
IMO gibt es keinen guten Grund. Die C ++ STL wurde in den frühen 80ern von Stepanov entwickelt. Obwohl die Benutzerfreundlichkeit durch die umständliche C ++ - Schablonensyntax eingeschränkt war, ist sie im Vergleich zu den Java-Auflistungsklassen ein Musterbeispiel für Konsistenz und Benutzerfreundlichkeit.
Kevin Cline
Es gab eine C ++ - STL-Portierung nach Java. Aber es war 5-mal größer in der Anzahl der Klassen. Aus diesem Grund kaufe ich nicht, dass der größere konsistenter und benutzerfreundlicher ist. docs.oracle.com/javase/6/docs/technotes/guides/collections/…
m3th0dman
@ m3th0dman In Java ist es viel größer, da Java keine vergleichbare Leistung wie C ++ - Vorlagen hat.
Kevin Cline
Vielleicht ist es nur ein seltsamer Stil, den ich entwickelt habe, aber mein Code tendiert dazu, alle Sammlungen als "schreibgeschützt" zu behandeln (genauer gesagt, nur etwas, was Sie zählen oder worüber Sie iterieren), mit Ausnahme der wenigen Methoden, mit denen die Sammlungen tatsächlich erstellt werden. Wahrscheinlich sowieso gute Praxis (vor allem für Parallelität). Und die optionalen Methoden, über die sich so viele beschweren, waren für mich nie ein wirkliches Problem. Auch die Generics-Albträume mit "Super" und "Erweitert" waren noch nie ein Thema. Fragen Sie sich nur, ob andere diese allgemeine Praxis anwenden?
user949300

Antworten:

28

Die FAQ liefert die Antwort. Kurz gesagt, sie sahen eine potenzielle kombinatorische Explosion der benötigten Schnittstellen mit modifizierbarer, nicht modifizierbarer Ansicht, Nur-Löschen, Nur-Hinzufügen, fester Länge, unveränderlich (für Threading) usw. für jeden möglichen Satz implementierter Optionsmethoden.

Ratschenfreak
quelle
6
All dies wäre vermieden worden, wenn Java ein constSchlüsselwort wie C ++ hätte.
Etienne de Martel
@Etienne: Besser wären Metaklassen wie Python. Dann können Sie die kombinatorische Anzahl von Schnittstellen programmgesteuert erstellen. Das Problem mit const ist , dass es gibt Ihnen nur eine oder zwei Dimensionen: vector<int>, const vector<int>, vector<const int>, const vector<const int>. So weit so gut, aber dann versuchen Sie, Diagramme zu implementieren, und Sie möchten die Diagrammstruktur konstant machen, aber die Knotenattribute modifizierbar usw.
Neil G
2
@Etienne, noch ein Grund mehr, "Learn Scala" auf meine To-Do-Liste zu setzen!
Glenviewjeff
2
Was ich nicht verstehe ist, warum sie keine canMethode entwickelt haben, um zu testen, ob eine Operation möglich ist? Es würde die Schnittstelle einfach und schnell halten.
Mehrdad
3
@ Etienne de Martel Unsinn. Wie würde das bei der kombinatorischen Explosion helfen?
Tom Hawtin - Tackline
10

Es hört sich für mich so an, als wäre das Interface Segregation Principledamals nicht so gut erforscht wie heute. Diese Vorgehensweise (dh Ihre Schnittstelle enthält alle möglichen Operationen und Sie haben "entartete" Methoden, die Ausnahmen für diejenigen auslösen, die Sie nicht benötigen) war beliebt, bevor SOLID und ISP zum De-facto-Standard für Qualitätscode wurden.

Wayne Molina
quelle
2
Warum eine Gegenstimme? Jemand mag den ISP nicht?
Wayne Molina
Es ist auch erwähnenswert, dass es in Frameworks, die Varianz unterstützen, ein RIESIGER Vorteil ist, die Aspekte einer Schnittstelle, die kovariant oder kontravariant sein können, von denjenigen zu trennen, die grundsätzlich invariant sind. Selbst ohne diese Unterstützung wäre es in Frameworks, die keine Typlöschung verwenden, von Vorteil, Aspekte einer Schnittstelle zu trennen, die typunabhängig sind (z. B. das Abrufen einer CountSammlung, ohne sich Gedanken darüber zu machen, welche Arten von Elementen enthalten sind). In Frameworks wie Java, die auf Typ-Erasure basieren, ist dies jedoch kein Problem.
Supercat
4

Während einige Leute "optionale Methoden" hassen, bieten sie in vielen Fällen eine bessere Semantik als stark getrennte Schnittstellen. Unter anderem berücksichtigen sie die Möglichkeiten, dass ein Objekt im Laufe seiner Lebensdauer Fähigkeiten oder Eigenschaften erhält oder dass ein Objekt (insbesondere ein Wrapper-Objekt) möglicherweise nicht weiß, wann es konstruiert wurde, über welche genauen Fähigkeiten es berichten soll.

Während ich die Java-Collection-Klassen kaum als Paragone für gutes Design bezeichnen werde, würde ich vorschlagen, dass ein gutes Collections-Framework eine große Anzahl optionaler Methoden sowie Möglichkeiten enthält, eine Collection nach ihren Eigenschaften und Fähigkeiten zu befragen . Mit einem solchen Entwurf kann eine einzelne Wrapper-Klasse mit einer Vielzahl von Sammlungen verwendet werden, ohne dass versehentlich die Fähigkeiten der zugrunde liegenden Sammlung beeinträchtigt werden. Wenn Methoden nicht optional wären, müsste für jede Kombination von Features, die Auflistungen möglicherweise unterstützen, eine andere Wrapper-Klasse verwendet werden. In bestimmten Situationen könnten einige Wrapper nicht verwendet werden.

Wenn eine Auflistung beispielsweise das Schreiben eines Elements nach Index oder das Anhängen von Elementen am Ende unterstützt, das Einfügen von Elementen in der Mitte jedoch nicht unterstützt, ist für den Code, der es in einen Wrapper einschließen möchte, der alle auf ihm ausgeführten Aktionen protokolliert, eine Version erforderlich des Protokollierungs-Wrappers, der die genaue Kombination der unterstützten Fähigkeiten bereitstellte, oder wenn keiner verfügbar war, müsste ein Wrapper verwendet werden, der entweder Anhängen oder Schreiben nach Index, aber nicht beides unterstützt. Wenn jedoch eine vereinheitlichte Erfassungsschnittstelle alle drei Methoden als "optional" bereitstellte, dann aber Methoden enthielt, um anzugeben, welche der optionalen Methoden verwendet werden könnten, könnte eine einzelne Wrapper-Klasse Auflistungen verarbeiten, die eine beliebige Kombination von Funktionen implementieren. Auf die Frage, welche Funktionen unterstützt werden, könnte ein Wrapper einfach melden, was auch immer die gekapselte Sammlung unterstützt.

Es ist zu beachten, dass das Vorhandensein von "optionalen Fähigkeiten" in einigen Fällen ermöglichen kann, dass aggregierte Sammlungen bestimmte Funktionen auf eine Weise implementieren, die viel effizienter ist, als dies möglich wäre, wenn Fähigkeiten durch das Vorhandensein von Implementierungen definiert würden. Angenommen, eine concatenateMethode wurde verwendet, um eine zusammengesetzte Auflistung aus zwei anderen zu bilden, wobei die erste zufällig eine ArrayList mit 1.000.000 Elementen und die letzte eine Auflistung mit zwanzig Elementen war, die nur von Anfang an iteriert werden konnte. Wenn die zusammengesetzte Auflistung nach dem 1.000.013sten Element (Index 1.000.012) gefragt würde, könnte sie die ArrayList nach der Anzahl der enthaltenen Elemente (dh 1.000.000) fragen, diese vom angeforderten Index subtrahieren (Ergebnis 12), zwölf Elemente auslesen und überspringen Auflistung, und geben Sie dann das nächste Element zurück.

In einer solchen Situation wäre es, obwohl die zusammengesetzte Sammlung keine unmittelbare Möglichkeit hätte, ein Element nach Index zurückzugeben, viel schneller, die zusammengesetzte Sammlung nach dem 1.000.013sten Element zu fragen, als 1.000.013 Elemente einzeln auszulesen und alle außer den letzten zu ignorieren einer.

Superkatze
quelle
@ Kevincline: Sind Sie mit der zitierten Formulierung nicht einverstanden? Ich würde das Design der Mittel, mit denen Schnittstellenimplementierungen ihre wesentlichen Merkmale und Fähigkeiten beschreiben können, als einen der wichtigsten Aspekte des Schnittstellendesigns betrachten, auch wenn es nicht oft beachtet wird. Wenn verschiedene Implementierungen einer Schnittstelle unterschiedliche optimale Möglichkeiten zum Ausführen bestimmter gemeinsamer Aufgaben bieten, sollten Clients, denen die Leistung am Herzen liegt, die Möglichkeit haben, den besten Ansatz in Szenarien auszuwählen, in denen es darauf ankommt.
Supercat
1
Entschuldigung, unvollständiger Kommentar. Ich wollte gerade sagen, dass "'Möglichkeiten, nach einer Sammlung zu fragen' ..." eine Überprüfung zur Laufzeit zur Kompilierungszeit darstellt.
Kevin Cline
@kevincline: In Fällen, in denen ein Client eine bestimmte Fähigkeit haben muss und nicht ohne diese leben kann, können Überprüfungen zur Kompilierungszeit hilfreich sein. Auf der anderen Seite gibt es viele Situationen, in denen Sammlungen über Fähigkeiten verfügen können, deren Kompilierzeit garantiert werden kann. In einigen Fällen kann es sinnvoll sein, eine abgeleitete Schnittstelle zu haben, deren Vertrag festlegt, dass alle legitimen Implementierungen der abgeleiteten Schnittstelle bestimmte Methoden unterstützen müssen, die in der Basisschnittstelle optional sind Es hat einige Funktionen ...
Supercat
... aber es ist besser, den Code das Objekt fragen zu lassen, ob es das Feature unterstützt, als das Typensystem zu fragen, ob der Typ des Objekts verspricht, das Feature zu unterstützen. Wenn eine bestimmte "bestimmte" Schnittstelle häufig verwendet wird, kann AsXXXdie Basisschnittstelle eine Methode enthalten, die das Objekt zurückgibt, für das sie aufgerufen wird, wenn sie diese Schnittstelle implementiert, oder ein Wrapper-Objekt zurückgibt, das diese Schnittstelle unterstützt, sofern dies möglich ist eine Ausnahme, wenn nicht. Zum Beispiel ImmutableCollectionkönnte eine Schnittstelle vertraglich vorgeschrieben sein ...
supercat
2
Bei Ihrem Vorschlag ist ein Problem aufgetreten: Bei einem Muster "Ask Then Do" tritt ein Parallelitätsproblem auf, wenn sich die Funktionen eines Objekts während seiner Lebensdauer ändern können. (Wenn Sie ohnehin eine Ausnahme riskieren müssen, sollten Sie sich nicht die Mühe machen ...)
jhominal
-1

Ich würde es den ursprünglichen Entwicklern zuschreiben, die es damals einfach nicht besser wussten. Seit 1998, als Java 2 und Collections zum ersten Mal veröffentlicht wurden, haben wir im OO-Design einen langen Weg zurückgelegt. Was jetzt wie offensichtlich schlechtes Design aussieht, war in den frühen Tagen von OOP nicht so offensichtlich.

Möglicherweise wurde dies jedoch getan, um zusätzliches Wirken zu verhindern. Wenn es eine zweite Schnittstelle wäre, müssten Sie Ihre Kollektionsinstanzen umwandeln, um diese optionalen Methoden aufzurufen, was ebenfalls hässlich ist. So wie es jetzt ist, würden Sie sofort eine UnsupportedOperationException abfangen und Ihren Code korrigieren. Aber wenn es zwei Schnittstellen gäbe, müssten Sie instanceof verwenden und überall gießen. Vielleicht hielten sie das für einen gültigen Kompromiss. Auch in den frühen Java-2-Tagen wurde instanceof wegen seiner langsamen Leistung stark verpönt. Möglicherweise haben sie versucht, eine übermäßige Nutzung zu verhindern.

Natürlich handelt es sich um wilde Spekulationen, ich bezweifle, dass wir dies jemals mit Sicherheit beantworten könnten, wenn nicht einer der Architekten der Originalsammlung zustimmt.

Jberg
quelle
7
Was für ein Casting? Wenn eine Methode a Collectionund nicht a zurückgibt MutableCollection, ist dies ein klares Zeichen dafür, dass sie nicht geändert werden soll. Ich weiß nicht, warum irgendjemand sie besetzen müsste. Wenn Sie separate Schnittstellen haben, werden diese Fehler zur Kompilierungszeit angezeigt, anstatt zur Laufzeit eine Ausnahme zu erhalten. Je früher Sie den Fehler erhalten, desto besser.
Etienne de Martel
1
Da dies weniger flexibel ist, besteht einer der größten Vorteile der Auflistungsbibliothek darin, dass Sie die übergeordneten Schnittstellen überall zurückgeben können und sich nicht um die tatsächliche Implementierung kümmern müssen. Wenn Sie zwei Schnittstellen verwenden, sind Sie jetzt enger miteinander verbunden. In den meisten Fällen möchten Sie einfach List und nicht ImmutableList zurückgeben, da Sie dies normalerweise der aufrufenden Klasse überlassen möchten.
Jberg
6
Wenn ich Ihnen eine schreibgeschützte Sammlung gebe, ist das so, weil ich nicht möchte, dass sie geändert wird. Eine Ausnahme auszulösen und sich auf die Dokumentation zu verlassen, fühlt sich verdammt wie ein Patch an. In C ++ würde ich einfach ein constObjekt zurückgeben und dem Benutzer sofort mitteilen, dass das Objekt nicht geändert werden kann.
Etienne de Martel
7
1998 war nicht "die Anfangszeit von OO Design".
quant_dev