Heim  >  Artikel  >  Java  >  Beispielanalyse der Vererbung in Java

Beispielanalyse der Vererbung in Java

WBOY
WBOYnach vorne
2023-04-20 17:19:081211Durchsuche

 Schnittstelle und Klasse?
 Einmal nahm ich an einem Treffen einer Java-Benutzergruppe teil. Auf der Konferenz hielt James Gosling (der Vater von Java) die Initiatorrede. In diesem denkwürdigen Frage-und-Antwort-Segment wurde er gefragt: „Wenn Sie Java umgestalten würden, was würden Sie ändern?“ „Ich möchte den Unterricht abschaffen“, antwortete er. Nachdem das Gelächter nachgelassen hatte, wurde erklärt, dass das eigentliche Problem nicht die Klasse selbst sei, sondern die Umsetzung der Vererbungsbeziehung. Die Schnittstellenvererbung (implementiert die Beziehung) ist besser. Sie sollten die Implementierung einer Vererbung so weit wie möglich vermeiden.
 Verlust der Flexibilität
 Warum sollte man eine Erbschaft vermeiden? Das erste Problem besteht darin, dass Sie durch die explizite Verwendung konkreter Klassennamen an eine bestimmte Implementierung gebunden sind, was zugrunde liegende Änderungen unnötig erschwert.
 In der aktuellen agilen Programmiermethode ist der Kern das Konzept des parallelen Designs und der parallelen Entwicklung. Sie beginnen mit der Programmierung, bevor Sie das Programm im Detail entwerfen. Diese Technik unterscheidet sich vom herkömmlichen Ansatz, bei dem der Entwurf abgeschlossen sein sollte, bevor mit dem Codieren begonnen wird. Viele erfolgreiche Projekte haben jedoch bewiesen, dass Sie qualitativ hochwertigen Code schneller entwickeln können als mit herkömmlichen Schritt-für-Schritt-Methoden. Aber der Kern der parallelen Entwicklung ist Flexibilität. Sie müssen Ihren Code so schreiben, dass neu entdeckte Anforderungen möglichst problemlos in vorhandenen Code integriert werden können.
 Anstatt Funktionen zu implementieren, die Sie möglicherweise benötigen, müssen Sie nur Funktionen implementieren, die Sie eindeutig benötigen, und gegenüber Änderungen mäßig tolerant sein. Ohne diese Art der flexiblen, parallelen Entwicklung ist das einfach unmöglich.

 Programmierung für Schnittstellen ist der Kern flexibler Architektur. Um zu veranschaulichen, warum, werfen wir einen Blick darauf, was passiert, wenn sie verwendet werden. Betrachten Sie den folgenden Code:

 f()
 {
 LinkedList list = new LinkedList();
  //...
 g( list );
 }

 g( LinkedList list )
 {
  list.add( . .. );
 g2( list )
 }

  Angenommen, es besteht Bedarf an einer schnellen Abfrage, so dass diese LinkedList diese nicht lösen kann. Sie müssen stattdessen HashSet verwenden. In vorhandenem Code können Änderungen nicht lokalisiert werden, da Sie nicht nur f(), sondern auch g() (das einen LinkedList-Parameter akzeptiert) und jeden Code, an den g() die Liste übergibt, ändern müssen. Schreiben Sie den Code wie folgt um:

  f()
 {
 Collection list = new LinkedList();
  //...
 g( list );
 }

 g( Collection list )
 {
 list.add ( ... );
 g2( list )
 }

  Um die verknüpfte Liste in einen Hash umzuwandeln, kann es so einfach sein, new HashSet() anstelle von new LinkedList() zu verwenden. das ist alles. Es gibt nichts anderes zu ändern.

 Vergleichen Sie als weiteres Beispiel die folgenden zwei Codeteile:

  f()
 {
 Collection c = new HashSet();
  //...
 g( c );
 }

 g( Collection c )
  {
 for( Iterator i = c.iterator(); i.hasNext() )
  do_something_with( i.next() );
  }

  und

 f2()
 {
  Collection c = new ( ).
Die g2()-Methode kann jetzt über die Sammlungsableitung iterieren, genau wie Sie Schlüssel-Wert-Paare aus einer Map abrufen können. Tatsächlich können Sie Iteratoren schreiben, die Daten generieren, anstatt eine Sammlung zu durchlaufen. Sie können Iteratoren schreiben, die Informationen aus dem Testframework oder der Testdatei abrufen. Dies ermöglicht eine enorme Flexibilität.

 
Kopplung

Bei der Implementierung der Vererbung ist die Kopplung ein kritischeres Thema – eine lästige Abhängigkeit, also die Abhängigkeit eines Teils des Programms von einem anderen Teil. Globale Variablen sind ein klassisches Beispiel dafür, warum starke Kopplung Probleme verursachen kann. Wenn Sie beispielsweise den Typ einer globalen Variablen ändern, sind möglicherweise alle Funktionen betroffen, die diese Variable verwenden. Daher muss der gesamte Code überprüft, geändert und erneut getestet werden. Darüber hinaus sind alle Funktionen, die diese Variable verwenden, über diese Variable miteinander gekoppelt. Das heißt, wenn ein Variablenwert zu einem Zeitpunkt geändert wird, an dem er schwierig zu verwenden ist, kann es sein, dass eine Funktion das Verhalten einer anderen Funktion fälschlicherweise beeinflusst. Dieses Problem ist in Multithread-Programmen deutlich verborgen.
Als Designer sollten Sie danach streben, Kopplungsbeziehungen zu minimieren. Sie können die Kopplung nicht vollständig beseitigen, da Methodenaufrufe von Objekten einer Klasse an Objekte einer anderen Klasse eine Form der losen Kopplung darstellen. Ohne Kopplung kann es kein Programm geben. Sie können jedoch eine gewisse Kopplung minimieren, indem Sie die OO-Regeln befolgen (am wichtigsten ist, dass die Implementierung eines Objekts vollständig vor den Objekten verborgen bleiben sollte, die es verwenden). Beispielsweise sollten die Instanzvariablen (Felder, die keine Konstanten sind) eines Objekts immer privat sein. Ich meine, für einen bestimmten Zeitraum, ausnahmslos kontinuierlich. (Sie können geschützte Methoden gelegentlich effektiv verwenden, aber geschützte Instanzvariablen sind eine Abscheulichkeit.) Aus dem gleichen Grund sollten Sie keine get/set-Funktionen verwenden – sie wirken einfach zu kompliziert, weil sie für eine Domäne öffentlich sind (trotz des Rückgabemodifikators). Der Grund für den Zugriff auf Funktionen von Objekten anstelle von Grundwerten liegt in einigen Fällen darin, dass die zurückgegebene Objektklasse eine Schlüsselabstraktion im Entwurf ist.

Hier bin ich nicht buchstäblich. In meiner eigenen Arbeit finde ich einen direkten Zusammenhang zwischen der Strenge meines OO-Ansatzes, der schnellen Codeentwicklung und der einfachen Codeimplementierung. Immer wenn ich gegen zentrale OO-Prinzipien verstoße, wie z. B. das Ausblenden von Implementierungen, schreibe ich diesen Code neu (normalerweise, weil der Code nicht debuggbar ist). Ich habe keine Zeit, den Code neu zu schreiben, also folge ich diesen Regeln. Sind mir rein praktische Gründe wichtig? Ich interessiere mich nicht für saubere Gründe.

   Das fragile Basisklassenproblem

  Wenden wir nun das Konzept der Kopplung auf die Vererbung an. In einem Implementierungssystem, das Extends verwendet, ist die abgeleitete Klasse sehr eng an die Basisklasse gekoppelt, und diese enge Kopplung ist unerwünscht. Um dieses Verhalten zu beschreiben, haben Designer den Spitznamen „fragiles Basisklassenproblem“ verwendet. Basisklassen gelten als anfällig, da Sie die Basisklasse ändern, obwohl sie scheinbar sicher ist. Beim Erben von einer abgeleiteten Klasse kann das neue Verhalten jedoch dazu führen, dass die abgeleitete Klasse nicht mehr funktioniert. Sie können nicht einfach feststellen, ob Änderungen an einer Basisklasse sicher sind, indem Sie die Basisklasse isoliert untersuchen. Sie müssen vielmehr auch alle abgeleiteten Klassen betrachten (und testen). Darüber hinaus müssen Sie den gesamten Code überprüfen, der auch in Basis- und abgeleiteten Klassenobjekten verwendet wird, da dieser Code durch das neue Verhalten möglicherweise beschädigt wird. Eine einfache Änderung an einer Basisklasse kann dazu führen, dass das gesamte Programm nicht mehr funktionsfähig ist.

  Lassen Sie uns das Problem fragiler Basisklassen und Basisklassenkopplung untersuchen. Die folgende Klasse erweitert die ArrayList-Klasse von Java, damit sie sich wie ein Stapel verhält:

 class Stack erweitert ArrayList
 {
 private int stack_pointer = 0;

 public void push( Object Article )
 {
 add( stack_pointer++, Article );
}

 public Object pop()
 {
 return remove( --stack_pointer );
  }

 public void push_many( Object[] Articles )
 {
 for( int i = 0; i < Articles. length; + +i )
  push( Articles[i] );
  }
  }

 Sogar eine einfache Klasse wie diese hat Probleme. Überlegen Sie, wann ein Benutzer die Vererbung ausgleicht und die Methode „clear()“ von ArrayList verwendet, um den Stapel zu öffnen:

 Stack a_stack = new Stack();
 a_stack.push("1");
 a_stack.push("2");
a_stack .clear();

 Dieser Code wird erfolgreich kompiliert, aber da die Basisklasse den Stapelzeigerstapel nicht kennt, befindet sich das Stapelobjekt derzeit in einem undefinierten Zustand. Der nächste Aufruf von push() setzt das neue Element auf Index 2. (der aktuelle Wert von stack_pointer), sodass der Stapel effektiv aus drei Elementen besteht – die beiden unteren sind Müll. (Die Stack-Klasse von Java hat genau dieses Problem, verwenden Sie es nicht.)

Die Lösung für dieses lästige Problem mit geerbten Methoden besteht darin, alle ArrayList-Methoden für Stack zu überschreiben, die den Status des Arrays ändern können, sodass die Überschreibung korrekt ist den Stapelzeiger oder lösen Sie eine Ausnahme aus. (Die Methode „removeRange()“ ist ein guter Kandidat zum Auslösen einer Ausnahme.)

 Diese Methode hat zwei Nachteile. Erstens: Wenn Sie alles abdecken, sollte die Basisklasse eigentlich eine Schnittstelle und keine Klasse sein. Wenn Sie keine geerbten Methoden verwenden, macht die Implementierung der Vererbung keinen Sinn. Zweitens, und was noch wichtiger ist, kann ein Stack nicht alle ArrayList-Methoden unterstützen. Beispielsweise führt die lästige Funktion „removeRange()“ zu nichts. Der einzig vernünftige Weg, eine nutzlose Methode zu implementieren, besteht darin, sie eine Ausnahme auslösen zu lassen, da sie niemals aufgerufen werden sollte. Diese Methode verwandelt Kompilierungsfehler effektiv in Laufzeitfehler. Das Schlimme ist, dass der Compiler den Fehler „Methode nicht gefunden“ ausgibt, wenn die Methode einfach nicht definiert ist. Wenn die Methode vorhanden ist, aber eine Ausnahme auslöst, erfahren Sie den Aufruffehler erst, wenn das Programm tatsächlich ausgeführt wird.

Eine bessere Lösung für dieses Basisklassenproblem besteht darin, die Datenstruktur zu kapseln, anstatt Vererbung zu verwenden. Dies ist die neue und verbesserte Version von Stack:
class Stack
{
private int stack_pointer = 0;
private ArrayList the_data = new ArrayList(); , Article );
 }

 public Object pop()
 {
 return the_data .remove( --stack_pointer );
 }

 public void push_many( Object[] Articles )
 {
 for( int i = 0; i < o.length; ++i )
  push( Articles[i] ) ;
  }
  }

  So weit, so gut, aber angesichts der fragilen Basisklassenproblematik sagen wir, dass Sie eine Variable im Stapel erstellen möchten, die verwendet wird, um die maximale Stapelgröße über einen bestimmten Zeitraum zu verfolgen. Eine mögliche Implementierung könnte so aussehen:

class Monitorable_stack erweitert Stack

  high_water_mark = current_size;
  super.push( Article );
 }

 publish Object pop()
 {
 --current_size;
 . ​​Rücksendung super. pop();
 }

  public int maximum_size_so_far()
 {
 return high_water_mark;
 }
 }
Diese neue Klasse hat zumindest eine Zeit lang einwandfrei funktioniert. Leider nutzt dieser Code die Tatsache aus, dass push_many() durch den Aufruf von push() funktioniert. Zunächst einmal scheint dieses Detail keine schlechte Wahl zu sein. Es vereinfacht den Code und Sie können die abgeleitete Version von push() auch dann erhalten, wenn auf Monitorable_stack über eine Stack-Referenz zugegriffen wird, sodass high_water_mark korrekt aktualisiert wird.

Das obige ist der detaillierte Inhalt vonBeispielanalyse der Vererbung in Java. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:yisu.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen