In einem meiner letzten Projekte habe ich eine Klasse mit dem folgenden Header definiert:
public class Node extends ArrayList<Node> {
...
}
Nach einer Diskussion mit meinem CS-Professor stellte er jedoch fest, dass die Klasse sowohl für das Gedächtnis "schrecklich" als auch für die "schlechte Praxis" sei. Ich habe nicht festgestellt, dass das erste besonders wahr und das zweite subjektiv ist.
Mein Grund für diese Verwendung ist, dass ich eine Idee für ein Objekt hatte, das als etwas definiert werden musste, das eine beliebige Tiefe haben könnte, wobei das Verhalten einer Instanz entweder durch eine benutzerdefinierte Implementierung oder durch das Verhalten mehrerer gleichartiger Objekte, die interagieren, definiert werden könnte . Dies würde die Abstraktion von Objekten ermöglichen, deren physikalische Implementierung aus vielen miteinander wechselwirkenden Unterkomponenten bestehen würde
Auf der anderen Seite sehe ich, wie schlecht das sein kann. Die Idee, etwas als eine Liste von sich selbst zu definieren, ist nicht einfach oder physikalisch umsetzbar.
Gibt es einen triftigen Grund, warum ich dies in meinem Code nicht verwenden sollte , wenn ich meine Verwendung dafür in Betracht ziehe?
¹ Wenn ich das näher erläutern muss, würde ich mich freuen; Ich versuche nur, diese Frage kurz zu halten.
quelle
ArrayList<Node>
, im Vergleich zu implementierenList<Node>
?Antworten:
Ehrlich gesagt sehe ich hier keine Notwendigkeit für Vererbung. Das ergibt keinen Sinn.
Node
ist einArrayList
vonNode
?Wenn dies nur eine rekursive Datenstruktur ist, schreiben Sie einfach etwas wie:
Welches macht Sinn; Knoten enthält eine Liste von untergeordneten oder untergeordneten Knoten.
quelle
Item
gibt es undItem
arbeitet unabhängig von den Listen.Node
ist einArrayList
vonNode
wieNode
enthält 0 bis vieleNode
Objekte. Ihre Funktion ist das gleiche, im Wesentlichen, aber anstatt ist eine Liste von Objekten wie, es hat eine Liste wie Objekte. Der ursprüngliche Entwurf für die geschriebene Klasse war der Glaube, dass jede KlasseNode
als eine Reihe von Unterarten ausgedrückt werden kannNode
, was beide Implementierungen tun, aber diese ist sicherlich weniger verwirrend. +1Children
nur ein- oder zweimal. Aus Gründen der Code-Klarheit ist es in solchen Fällen in der Regel vorzuziehen , ihn auszuschreiben.Das Argument "fürchterlich für das Gedächtnis" ist völlig falsch, aber es ist eine objektiv "schlechte Praxis". Wenn Sie von einer Klasse erben, erben Sie nicht nur die Felder und Methoden, die Sie interessieren. Stattdessen erben Sie alles . Jede deklarierte Methode, auch wenn sie für Sie nicht nützlich ist. Und vor allem erben Sie auch alle Verträge und Garantien, die die Klasse bietet.
Das Akronym SOLID bietet einige Heuristiken für eine gute objektorientierte Gestaltung. Hier haben das I nterface Segregation Principle (ISP) und das L iskov Substitution Pricinple (LSP) etwas zu sagen.
Der ISP fordert uns auf, unsere Schnittstellen so klein wie möglich zu halten. Aber durch das Erben von erhalten
ArrayList
Sie viele, viele Methoden. Ist es sinnvollget()
,remove()
,set()
(ersetzen) oderadd()
(Einsatz) ein Kind - Knoten an einem bestimmten Index? Ist es sinnvoll,ensureCapacity()
von der zugrunde liegenden Liste? Was bedeutet es fürsort()
einen Knoten? Sollen Nutzer Ihrer Klasse wirklich eine bekommensubList()
? Da Sie nicht gewünschte Methoden nicht ausblenden können, besteht die einzige Lösung darin, die ArrayList als Mitgliedsvariable zu haben und alle tatsächlich gewünschten Methoden weiterzuleiten:Wenn Sie wirklich alle Methoden wollen, die Sie in der Dokumentation sehen, können wir zum LSP übergehen. Der LSP sagt uns, dass wir in der Lage sein müssen, die Unterklasse überall dort zu verwenden, wo die Elternklasse erwartet wird. Wenn eine Funktion einen
ArrayList
as-Parameter annimmt und wirNode
stattdessen unseren übergeben , soll sich nichts ändern.Die Kompatibilität von Unterklassen beginnt mit einfachen Dingen wie Typensignaturen. Wenn Sie eine Methode überschreiben, können Sie die Parametertypen nicht strenger festlegen, da dies möglicherweise Verwendungen ausschließt, die für die übergeordnete Klasse zulässig waren. Aber das ist etwas, was der Compiler für uns in Java überprüft.
Der LSP geht jedoch viel tiefer: Wir müssen die Kompatibilität mit allem aufrechterhalten, was die Dokumentation aller übergeordneten Klassen und Schnittstellen verspricht. In ihrer Antwort hat Lynn einen solchen Fall gefunden , wo die
List
Schnittstelle (die Sie über geerbt habenArrayList
) garantiert , wie dieequals()
undhashCode()
Methoden funktionieren soll. DennhashCode()
Ihnen wird sogar ein bestimmter Algorithmus vorgegeben, der exakt implementiert werden muss. Nehmen wir an, Sie haben folgendes geschriebenNode
:Dies setzt voraus, dass die
value
nicht zum beitragenhashCode()
und nicht beeinflussen könnenequals()
. DieList
Schnittstelle, die Sie zu ehren versprechen, indem Sie sie erbennew Node(0).equals(new Node(123))
, muss wahr sein.Da durch das Erben von Klassen zu leicht versehentlich ein Versprechen einer übergeordneten Klasse gebrochen werden kann und in der Regel mehr Methoden verfügbar gemacht werden, als Sie beabsichtigt haben, wird im Allgemeinen vorgeschlagen, die Komposition der Vererbung vorzuziehen . Wenn Sie etwas erben müssen, wird empfohlen, nur Schnittstellen zu erben. Wenn Sie das Verhalten einer bestimmten Klasse wiederverwenden möchten, können Sie es als separates Objekt in einer Instanzvariablen speichern, sodass Ihnen nicht alle Versprechen und Anforderungen aufgezwungen werden.
Manchmal deutet unsere natürliche Sprache auf eine Vererbungsbeziehung hin: Ein Auto ist ein Fahrzeug. Ein Motorrad ist ein Fahrzeug. Soll ich Klassen definieren
Car
undMotorcycle
die von einerVehicle
Klasse erben ? Bei objektorientiertem Design geht es nicht darum, die reale Welt in unserem Code exakt wiederzugeben. Wir können die reichen Taxonomien der realen Welt nicht einfach in unserem Quellcode kodieren.Ein solches Beispiel ist das Problem der Mitarbeiter-Chef-Modellierung. Wir haben mehrere
Person
s, jedes mit einem Namen und einer Adresse. AnEmployee
ist einPerson
und hat einBoss
. ABoss
ist auch aPerson
. Soll ich also einePerson
Klasse erstellen , die vonBoss
und geerbt wirdEmployee
? Jetzt habe ich ein Problem: Der Chef ist auch Angestellter und hat einen anderen Vorgesetzten. So scheint es,Boss
sollte sich verlängernEmployee
. Aber dasCompanyOwner
ist einBoss
aber ist keinEmployee
? Jede Art von Vererbungsgraph wird hier irgendwie zusammenbrechen.Bei OOP geht es nicht um Hierarchien, Vererbung und Wiederverwendung vorhandener Klassen, sondern um die Verallgemeinerung des Verhaltens . In OOP geht es um „Ich habe eine Reihe von Objekten und möchte, dass sie etwas Besonderes tun - und es ist mir egal, wie.“ Dafür sind Schnittstellen gedacht. Wenn Sie die
Iterable
Schnittstelle für Ihre implementieren,Node
weil Sie sie iterabel machen möchten, ist das vollkommen in Ordnung. Wenn Sie dieCollection
Schnittstelle implementieren , weil Sie untergeordnete Knoten usw. hinzufügen / entfernen möchten, ist dies in Ordnung. Aber von einer anderen Klasse erben, weil es dir alles gibt, was nicht ist, oder zumindest nicht, wenn du es dir genau überlegt hast, wie oben beschrieben.quelle
Das Erweitern eines Containers an sich wird normalerweise als schlechte Praxis angesehen. Es gibt kaum einen Grund, einen Container zu verlängern, anstatt nur einen zu haben. Das Erweitern eines Containers von Ihnen macht es nur noch merkwürdiger.
quelle
Zusätzlich zu dem, was gesagt wurde, gibt es einen etwas Java-spezifischen Grund, um diese Art von Strukturen zu vermeiden.
Der Vertrag der
equals
Methode für Listen sieht vor, dass eine Liste einem anderen Objekt gleichgestellt wirdQuelle: https://docs.oracle.com/javase/7/docs/api/java/util/List.html#equals(java.lang.Object)
Konkret bedeutet dies, dass eine Klasse, die als eine Liste von sich selbst entworfen wurde, Gleichheitsvergleiche teuer machen kann (und Hash-Berechnungen auch, wenn die Listen veränderlich sind). Wenn die Klasse einige Instanzfelder enthält, müssen diese im Gleichheitsvergleich ignoriert werden .
quelle
Zum Gedächtnis:
Ich würde sagen, das ist eine Frage des Perfektionismus. Der Default-Konstruktor von
ArrayList
sieht so aus:Quelle . Dieser Konstruktor wird auch im Oracle-JDK verwendet.
Überlegen Sie nun, ob Sie mit Ihrem Code eine einfach verknüpfte Liste erstellen möchten. Sie haben gerade erfolgreich Ihren Speicherverbrauch um den Faktor 10x (um genau zu sein sogar geringfügig höher) aufgebläht. Bei Bäumen kann dies auch ohne besondere Anforderungen an die Baumstruktur recht schlecht werden. Die Verwendung einer
LinkedList
oder einer anderen Klasse sollte dieses Problem lösen.Um ehrlich zu sein, ist dies in den meisten Fällen nichts anderes als Perfektionismus, selbst wenn wir die Menge des verfügbaren Speichers ignorieren. A
LinkedList
würde den Code als Alternative etwas verlangsamen, so dass es ein Kompromiss zwischen Leistung und Speicherverbrauch ist. Meine persönliche Meinung dazu wäre jedoch, nicht so viel Speicherplatz zu verschwenden, wie dies leicht umgangen werden kann.EDIT: Klarstellung bezüglich der Kommentare (@amon um genau zu sein). In diesem Teil der Antwort wird nicht auf die Frage der Vererbung eingegangen. Der Vergleich der Speichernutzung wird mit einer einfach verknüpften Liste und mit der besten Speichernutzung durchgeführt (in tatsächlichen Implementierungen kann sich der Faktor etwas ändern, aber er ist immer noch groß genug, um einiges an verschwendetem Speicher zusammenzufassen).
In Bezug auf "schlechte Praxis":
Bestimmt. Dies ist aus einem einfachen Grund nicht die Standardmethode zum Implementieren eines Diagramms: Ein Diagrammknoten hat untergeordnete Knoten, nicht eine Liste von untergeordneten Knoten. Es ist eine wichtige Fähigkeit, genau auszudrücken, was Sie mit Code meinen. Sei es in Variablennamen oder in solchen Strukturen. Nächster Punkt: Schnittstellen auf ein Minimum beschränken: Sie haben
ArrayList
dem Benutzer der Klasse jede Methode über die Vererbung zur Verfügung gestellt. Es gibt keine Möglichkeit, dies zu ändern, ohne die gesamte Struktur des Codes zu brechen. Speichern Sie stattdessen dieList
als interne Variable und stellen Sie die benötigten Methoden über eine Adapter-Methode zur Verfügung. Auf diese Weise können Sie der Klasse auf einfache Weise Funktionen hinzufügen und entfernen, ohne alles zu vermasseln.quelle
new Object[0]
, dass a insgesamt 4 Wörter verwendet,new Object[10]
würde a nur 14 Wörter des Speichers verwenden.ArrayList
s ziemlich viel Gedächtnis verbrauchen .Dies scheint wie die Semantik des zusammengesetzten Musters auszusehen . Es wird verwendet, um rekursive Datenstrukturen zu modellieren.
quelle
public class Node extends ArrayList<Node> { public string Item; }
ist die Vererbungsversion Ihrer Antwort. Ihre Argumentation ist einfacher, aber beide erreichen das eine: Sie definieren nicht-binäre Bäume.