Containerunterstützung für Objekte in Java 1.0.2

Herbert Spencer schrieb: "Wissenschaft ist organisiertes Wissen." Die Folge könnte sein, dass Anwendungen organisierte Objekte sind. Nehmen wir uns einen Moment Zeit, um uns einigen Aspekten von Java zuzuwenden, die für die Entwicklung von Anwendungen und nicht von Applets von entscheidender Bedeutung sind.

Von denen, die von Java gehört haben, hat die Mehrheit durch die populäre Presse etwas über die Sprache gelernt. Die Aussage, die häufig auftaucht, ist, dass Java "kleine Anwendungen oder Applets programmiert, die in eine Webseite eingebettet werden können". Diese Definition ist zwar korrekt, vermittelt jedoch nur einen Aspekt der neuen Sprache. es beschreibt nicht das ganze Bild. Vielleicht kann Java besser als eine Sprache beschrieben werden, die entwickelt wurde, um Systeme - große Systeme - aus gut verstandenen tragbaren Teilen ausführbaren Codes zu erstellen, die ganz oder teilweise kombiniert werden können, um ein wünschenswertes Ganzes zu erzeugen.

In dieser Spalte werde ich mich mit den verschiedenen Tools befassen, mit denen Sie Java erstellen können. Ich werde zeigen, wie diese Tools kombiniert werden können, um eine größere Anwendung zu erstellen, und wie Sie, sobald Sie eine Anwendung haben, die Anwendung weiter zu noch größeren Systemen zusammenfassen können - alles möglich, da in Java kein Unterschied zwischen einer vollständigen Anwendung und besteht eine einfache Unterroutine.

Um das Quellcodefutter für diese und frühere Spalten bereitzustellen, habe ich einen BASIC-Interpreter erstellt. "Warum BASIC?" Sie könnten fragen, ob niemand mehr BASIC verwendet. Dies ist nicht ganz richtig. BASIC lebt in Visual Basic und in anderen Skriptsprachen weiter. Noch wichtiger ist jedoch, dass viele Menschen damit in Berührung gekommen sind und den folgenden konzeptionellen Sprung machen können: Wenn "Anwendungen" in BASIC programmiert sind und BASIC in Java geschrieben werden kann, können Anwendungen in Java geschrieben werden. BASIC ist nur eine andere interpretierte Sprache. Die Tools, die wir erstellen werden, können so geändert werden, dass sie eine beliebige Sprachsyntax verwenden. Daher stehen die Kernkonzepte im Mittelpunkt dieser Artikel. Was als Anwendung beginnt, wird daher zu einer Komponente anderer Anwendungen - vielleicht sogar von Applets.

Generische Klassen und Container

Das Erstellen generischer Klassen ist besonders relevant beim Erstellen von Anwendungen, da die Wiederverwendung von Klassen eine enorme Hebelwirkung bei der Reduzierung der Komplexität und der Markteinführungszeit bietet. In einem Applet wird der Wert einer generischen Klasse durch die Anforderung verringert, sie über das Netzwerk zu laden. Die negativen Auswirkungen des Ladens generischer Klassen über das Netzwerk werden in Suns Java Workshop (JWS) demonstriert. JWS erweitert die Standardversion des Abstract Winding Toolkit (AWT) um einige sehr elegante "Schatten" -Klassen. Der Vorteil ist, dass Applets einfach zu entwickeln und reich an Funktionen sind. Der Nachteil ist, dass das Laden dieser Klassen auf einer langsamen Netzwerkverbindung viel Zeit in Anspruch nehmen kann. Während dieser Nachteil irgendwann verschwinden wird, stellen wir fest, dass häufig eine Systemperspektive für die Klassenentwicklung erforderlich ist, um die beste Lösung zu erzielen.

Da wir uns etwas ernsthafter mit der Anwendungsentwicklung befassen, gehen wir davon aus, dass wir bereits festgestellt haben, dass generische Klassen eine gültige Lösung sind.

Java bietet, wie viele Allzwecksprachen, verschiedene Tools zum Erstellen generischer Klassen. Unterschiedliche Anforderungen erfordern die Verwendung

verschiedene Werkzeuge. In dieser Spalte werde ich die Entwicklung einer containerKlasse als Beispiel verwenden, da sie fast alle Tools aufnehmen kann, die ein Benutzer möglicherweise verwenden möchte.

Container: Eine Definition

Für diejenigen unter Ihnen, die noch nicht mit objektorientierten Dingen vertraut sind, ist ein Container eine Klasse, die andere Objekte organisiert. Übliche Container sind Binärbäume, Warteschlangen, Listen und Stapel. Java stellt mit der JDK 1.0.2-Version drei Containerklassen zur Verfügung: java.util.Hashtable, java.util.Stack und java.util.Vector.

Container haben sowohl ein Organisationsprinzip als auch eine Schnittstelle. Stapel können beispielsweise als "first in, last out" (FILO) organisiert werden, und ihre Schnittstelle kann so definiert werden, dass sie zwei Methoden aufweist - push () und pop () . Bei einfachen Containern können die Standardmethoden hinzugefügt und entfernt werden . Außerdem haben sie die Möglichkeit, den gesamten Container aufzulisten, zu überprüfen, ob sich bereits ein Kandidatenobjekt im Container befindet, und die Anzahl der vom Container gehaltenen Elemente zu testen.

Die Java-Containerklassen zeigen einige Probleme mit Containern, insbesondere mit Schlüsselcontainern (solchen Containern, die einen Schlüssel zum Suchen eines Objekts verwenden). Die nicht verschlüsselten Container wie Stack und Vector stopfen Objekte einfach hinein und ziehen sie heraus. Der Schlüsselcontainer Hashtable verwendet ein Schlüsselobjekt, um ein Datenobjekt zu lokalisieren. Damit die Schlüsselfunktion funktioniert, muss das Schlüsselobjekt eine Methode HashCode unterstützen, die für jedes Objekt einen eindeutigen Hashcode zurückgibt. Diese Tastfunktion funktioniert, weil dieObjectKlasse definiert eine HashCode-Methode und wird daher von allen Objekten geerbt, aber es ist nicht immer das, was Sie wollen. Wenn Sie beispielsweise Objekte in Ihren Hash-Tabellencontainer einfügen und sie mit String-Objekten indizieren, gibt die Standard-HashCode-Methode einfach eine eindeutige Ganzzahl zurück, die auf dem Objektreferenzwert basiert. Bei Zeichenfolgen soll der Hash-Code wirklich eine Funktion des Zeichenfolgenwerts sein. Daher überschreibt Zeichenfolge HashCode und stellt eine eigene Version bereit. Dies bedeutet, dass Sie für jedes Objekt, das Sie entwickeln und in einer Hash-Tabelle mit einer Instanz des Objekts als Schlüssel speichern möchten, die HashCode-Methode überschreiben müssen. Dies stellt sicher, dass identisch konstruierte Objekte denselben Code haben.

Aber was ist mit sortierten Behältern? Die einzige in der ObjectKlasse bereitgestellte Sortierschnittstelle ist equals () , und es ist darauf beschränkt, zwei Objekte so zu setzen, dass sie dieselbe Referenz und nicht denselben Wert haben. Aus diesem Grund können Sie in Java den folgenden Code nicht schreiben:

 if (someStringObject == "this") dann {... mach etwas ...} 

Der obige Code vergleicht die Objektreferenzen, stellt fest, dass es hier zwei verschiedene Objekte gibt, und gibt false zurück. Sie müssen den Code wie folgt schreiben:

 if (someStringObject.compareTo ("this") == 0) dann {... etwas tun ...} 

Dieser letztere Test verwendet Wissen, das in der compareTo- Methode von String gekapselt ist , um zwei String-Objekte zu vergleichen und einen Hinweis auf Gleichheit zurückzugeben.

Verwenden Sie die Werkzeuge in der Box

Wie bereits erwähnt, stehen generischen Programmentwicklern zwei Hauptwerkzeuge zur Verfügung: Implementierungsvererbung (Erweiterung) und Verhaltensvererbung (Implementierung).

Um die Implementierungsvererbung zu verwenden, erweitern Sie eine vorhandene Klasse (Unterklasse). In der Erweiterung haben alle Unterklassen der Basisklasse die gleichen Funktionen wie die Stammklasse. Dies ist die Basis für die HashCodeMethode in der ObjectKlasse. Da alle Objekte von der java.lang.ObjectKlasse erben , verfügen alle Objekte über eine Methode HashCode, die einen eindeutigen Hash für dieses Objekt zurückgibt. Wenn Sie Ihre Objekte jedoch als Schlüssel verwenden möchten, beachten Sie die zuvor erwähnte Einschränkung bezüglich des Überschreibens HashCode.

Zusätzlich zur Implementierungsvererbung gibt es eine Verhaltensvererbung (Implementierung), die durch Angabe erreicht wird, dass ein Objekt eine bestimmte Java-Schnittstelle implementiert. Ein Objekt, das eine Schnittstelle implementiert, kann in eine Objektreferenz dieses Schnittstellentyps umgewandelt werden. Diese Referenz kann dann verwendet werden, um die von dieser Schnittstelle angegebenen Methoden aufzurufen. In der Regel werden Schnittstellen verwendet, wenn eine Klasse möglicherweise mehrere Objekte unterschiedlichen Typs auf gemeinsame Weise verarbeiten muss. Beispielsweise definiert Java die Runnable-Schnittstelle, die von den Thread-Klassen verwendet wird, um mit Klassen in ihrem eigenen Thread zu arbeiten.

Container bauen

Um die Kompromisse beim Schreiben von generischem Code zu demonstrieren, werde ich Sie durch das Design und die Implementierung einer sortierten Containerklasse führen.

As I mentioned earlier, in the development of general-purpose applications, in many cases a good container would be useful. In my example application I needed a container that was both keyed, meaning that I wanted to retrieve contained objects by using a simple key, and sorted so that I could retrieve the contained objects in a specific order based on the key values.

When designing systems, it is important to keep in mind what parts of the system use a particular interface. In the case of containers, there are two critical interfaces -- the container itself and the keys that index the container. User programs use the container to store and organize objects; the containers themselves use the key interfaces to help them organize themselves. When designing containers, we strive to make them easy to use and to store a wide variety of objects (thus increasing their utility). We design the keys to be flexible so that a wide variety of container implementation can use the same key structures.

To solve my behavioral requirements, keying and sorting, I turn to a useful tree data structure called a binary search tree (BST). Binary trees have the useful property of being sorted, so they can be efficiently searched and can be dumped out in sorted order. The actual BST code is an implementation of the algorithms published in the book Introduction to Algorithms, by Thomas Cormen, Charles Leiserson, and Ron Rivest.

java.util.Dictionary

The Java standard classes have taken a first step toward generic keyed containers with the definition of an abstract class named java.util.Dictionary. If you look at the source code that comes with the JDK, you will see that Hashtable is a subclass of Dictionary.

The Dictionary class attempts to define the methods common to all keyed containers. Technically, what is being described could more properly be called a store as there is no required binding between the key and the object it indexes. However, the name is appropriate as nearly everyone understands the basic operation of a dictionary. An alternative name might be KeyedContainer, but that title gets tedious pretty quickly. The point is that the common superclass of a set of generic classes should express the core behavior being factored out by that class. The Dictionary methods are as follows:

size( )

This method returns the number of objects currently being held by the container.
isEmpty( ) This method returns true if the container has no elements.
keys( ) Return the list of keys in the table as an Enumeration.
elements( ) Return the list of contained objects as an Enumeration.
get(Objectk) Get an object, given a particular key k.
put(Objectk,Objecto) Store an object o using key k.
remove(Objectk) Remove an object that is indexed by key k.

By subclassing Dictionary, we use the tool of implementation inheritance to create an object that can be used by a wide variety of clients. These clients need know only how to use a Dictionary, and we can then substitute our new BST or a Hashtable without the client noticing. It is this property of abstracting out the core interface into the superclass that is crucial to reusability, general-purpose function, expressed cleanly.

Basically, Dictionary gives us two groups of behavior, accounting and administration -- accounting in the form of how many objects we've stored and bulk reading of the store, and administration in the form of get, put, and remove.

If you look at the Hashtable class source (it is included with all versions of the JDK in a file named src.zip), you will see that this class extends Dictionary and has two private internal classes, one named HashtableEntry and one named HashtableEnumerator. The implementation is straightforward. When put is called, objects are placed into a HashtableEntry object and stored into a hash table. When get is called, the key passed is hashed and the hashcode is used to locate the desired object in the hash table. These methods keep track of how many objects have been added or removed, and this information is returned in response to a size request. The HashtableEnumerator class is used to return results of the elements method or keys method.

First cut at a generic keyed container

The BinarySearchTree class is an example of a generic container that subclasses Dictionary but uses a different organizing principle. As in the Hashtable class, I've added a couple of classes to support holding the stored objects and keys and for enumerating the table.

The first is BSTNode, which is equivalent to a HashtableEntry. It is defined as shown in the code outline below. You can also look at the source.

class BSTNode { protected BSTNode parent; protected BSTNode left; protected BSTNode right; protected String key; protected Object payload; public BSTNode(String k, Object p) { key = k; payload = p; } protected BSTNode() { super(); } BSTNode successor() { return successor(this); } BSTNode precessor() { return predecessor(this); } BSTNode min() { return min(this); } BSTNode max() { return max(this); } void print(PrintStream p) { print(this, p); } private static BSTNode successor(BSTNode n) { ... } private static BSTNode predecessor(BSTNode n) { ... } private static BSTNode min(BSTNode n) { ... } private static BSTNode max(BSTNode n) { ... } private static void print(BSTNode n, PrintStream p) { ... } } 

Schauen wir uns diesen Code an, um zwei Dinge zu klären. Erstens gibt es den nullgeschützten Konstruktor, der vorhanden ist, damit Unterklassen dieser Klasse keinen Konstruktor deklarieren müssen, der einen der Konstruktoren dieser Klasse überschreibt. Zweitens sind die Methoden Nachfolger , Vorgänger , Min , Max und Print sehr kurz und nennen lediglich dasselbe private Äquivalent, um Speicherplatz zu sparen.