Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java 2 von Friedrich Esser
Designmuster und Zertifizierungswissen
Zum Katalog
gp Kapitel 14 Collection-Framework
  gp 14.1 Aufbau
  gp 14.2 Interface-Hierarchie
  gp 14.3 Kollektions-Klassen
  gp 14.4 Iterator-Pattern
  gp 14.5 Fundamentale vs. Template-Methoden
  gp 14.6 Dekorieren von Kollektionen
  gp 14.7 Zusammenfassung
  gp 14.8 Testfragen

Kapitel 14 Collection-Framework

Kollektionen sind Aggregationen von beliebigen Objekten im Speicher. Somit gehören selbst Arrays – integriert in Java – zu den Kollektionen.
Kollektionen haben viele gemeinsame Eigenschaften. Ein
Collection-Framework erscheint also durchaus sinnvoll.
Umgekehrt müssen Aggregationen verschiedenen Ansprüchen genügen und haben deshalb auch unterschiedliche Eigenschaften und Methoden.
In Java 1.2 wurde ein neues Collection-Framework auf Basis einer Interface-Hierarchie vorgestellt, das Eleganz und Mächtigkeit recht gut verbindet.
Obwohl diese Kollektionen transient und nicht thread-sicher sind, kann dies mit Hilfe von Serialisierung und Wrappern ermöglicht werden.


Galileo Computing

14.1 Aufbau  downtop

Das Collection-Framework in Java 1.2 basiert auf drei Säulen:

Interface-
basierend

gp  Interface-Framework: Interfaces definieren die verschiedenen Kollektionsarten mit ihren Methoden, ohne eine konkrete Implementierung festzulegen.

Algorithmen sind Templates

gp  Algorithmen: Viele Algorithmen basieren nur auf »»Interface-Objekten« bzw. –Methoden”. Hierzu zählen Standardaufgaben wie Iterieren, Sortieren und Suchen. Diese Templates können also ohne Kenntnis der konkreten Implementation benutzt werden.

Standard-Collection-Klassen

gp  Standard-Implementation: Bietet zu jeder Kollektionsart Klassen an, die zur Erzeugung der Objekte benötigt werden.

Die Wahl, Kollektionen auf Basis einer Interface-Hierarchie zu definieren, ist konsequent und zeigt den Stellenwert des Interface-Patterns.

Aufgrund der bereits besprochenen Limitationen von Interfaces wird jedes Interface mit langen Kommentaren versehen, um klar zu machen, welche Kontrakte die Implementierung einhalten müssen.


Galileo Computing

14.2 Interface-Hierarchie  downtop

Framework mit zwei Basisklassen:
Collection und Map

Das Framework besteht aus zwei separaten Teilen mit den Basis-Interfaces Collection und Map.

Ein Teil der Kollektionen leitet sich von Collection ab (siehe Abb. 14.1, optionale Methoden kursiv).

Collection-
Hierarchie


Abbildung
Abbildung 14.1   Collection-Hierarchie

Collection:
Interface ohne Klassen

Collection bildet nur den kleinsten gemeinsamen Nenner aller Kollektionen und hat keine konkrete Implementierung. Nur für die abgeleiteten Interfaces existieren Klassen im Framework.

gp  Das Collection-Interface dient häufig dazu, beliebige Kollektionen an andere Objekte und Methoden zu übergeben.

Map: Jeder Eintrag hat einen Schlüssel

Die Map verbindet mit jedem Objekt ein eindeutiges Schlüssel-Objekt und hat somit eine Sonderstellung. Die Gemeinsamkeiten mit den anderen Aggregationen für eine gemeinsame Root erschienen zu gering (siehe Abb. 14.2).

Map-Hierarchie


Abbildung
Abbildung 14.2   Map-Hierarchie

Kollektionen sind aus Sicht ihrer Elemente generell, da sie beliebige Objekte als Elemente akzeptieren.

Eleganz durch Minimalismus

Minimalismus

Wer andere Hierarchien vergleichbarer typisierter Sprachen wie C++ kennt, erkennt klare Unterschiede:

gp  Das Design ist praktisch typenlos, Elemente bzw. Schlüssel sind immer vom Typ Object. Primitive Typen sind damit ausgeschlossen.
gp  Es gibt vergleichsweise sehr wenige unterschiedliche Arten von Kollektionen. Viele spezialisierte Kollektionen fehlen.

Galileo Computing

14.2.1 Unterstützende Interfaces  downtop

Neben dem eigentlichen Kern des Frameworks gibt es noch unterstützende Interfaces (siehe Abb. 14.3).

Iteratoren und
Callback-Interfaces für Objektvergleich


Abbildung
Abbildung 14.3   Interfaces zur Unterstützung des Frameworks

Die Iteratoren vereinfachen das Durchlaufen der Elemente in einer Kollektion und die beiden Compare-Interfaces erlauben eine generelle Art des Objektvergleichs.


Galileo Computing

14.2.2 Bedeutung der Interfaces  downtop

Collection-Hierarchie

Collection:
»abstrakte« Multimenge (Bag)

Eine Kollektion ist eine ungeordnete Ansammlung von Objekten, Elemente genannt.

gp  Eine Kollektion (Collection) enthält keine Einschränkungen bezüglich Typ, Anordnung oder Häufigkeit eines Elements.

Mathematisch gesehen ist eine Kollektion eine Multimenge (Bag). Die Reihenfolge der Elemente ist ohne Bedeutung und nicht definiert.

Set: ohne
Doppeleinträge

gp  Eine Menge (Set) ist eine Kollektion, in der jedes Element höchstens einmal vorkommen kann.

List:
indizierte Elemente

gp  Eine Liste (List) ist eine Kollektion, in der die Elemente angeordnet sind (z.B. in der Reihenfolge des Einfügens). Damit können die Elemente indiziert werden.

Elementvergleich mit compare(), compareTo()

gp  Comparable bzw. Comparator vergleichen mit compareTo() bzw. compare() zwei Objekte auf kleiner, größer oder gleich. Sie liefern eine negative bzw. positive ganze Zahl, wenn das erste Objekt kleiner bzw. größer ist, oder Null, wenn die Objekte gleich sind.
gp  Zur sortierten Menge (SortedSet) werden die Elemente anhand eines Sortierkriteriums, welches durch einen Comparator festgelegt wird.

(List)Iteratoren durchlaufen Elemente

gp  Mit einem Iterator können alle Elemente in einer Kollektion genau einmal besucht werden, wobei die Reihenfolge nicht relevant ist.
gp  Ein ListIterator durchläuft speziell Listen in der vorgegebenen Ordnung.

Map:
Einträge als Schlüssel/Wert-Paare

Da Iteratoren einem gleichnamigen Pattern folgen, werden sie weiter unten in einem eigenen Abschnitt behandelt.

Map-Hierarchie

Logisch gesehen ist eine Map eine spezielle Kollektion.

gp  Bei einer Map besteht jedes Element – Eintrag (Entry) genannt – aus zwei assoziierten Werten, dem Schlüssel (Key), der eindeutig sein muss, und dem Wert (Value).

Schlüssel:
ein Primary Key

Der Schlüssel identifiziert aufgrund seiner Eindeutigkeit den Eintrag (bei Datenbanken auch als Unique- oder Primary-Key bezeichnet). Wird ein weiterer Eintrag mit demselben Schlüssel eingefügt, wird nur der alte Wert des bestehenden Eintrags gegen den neuen ausgetauscht.

SortedMap

gp  Bei einer SortedMap sind die Einträge anhand eines Sortierkriteriums (Comparator) geordnet.

Entry

gp  Ein Entry ist ein inneres Interface von Map, welches einen Eintrag kapselt und hierzu passende Methoden bereitstellt.

Galileo Computing

14.2.3 Konventionen, Kontrakte und Constraints  downtop

Icon
Kontrakte zu Kollektionen

In den Kommentaren zu den Interfaces sind Konventionen und Kontrakte enthalten, die nachfolgend kurz angesprochen werden sollen.

gp  Spezielle Ausnahmen vom Typ RuntimeException sollen bei konkreten Implementationen Kontraktverletzungen verhindern.

Beispiel: Immutable Kollektionen

Immutable Kollektionen dürfen keine Mutatoren enthalten, die die Kollektion nach der Anlage ändern. Deshalb sind die Mutatoren in Collection als optionale Methoden gekennzeichnet.

Unerlaubte optionale Methode

UnsupportedOperationException: unerwünschte Methode

Da aber Interfaces keine optionalen Methoden kennen, gilt folgende Konvention:

gp  UnsupportedOperationException: Diese Ausnahme soll von den unerlaubten optionalen Methoden ausgelöst werden.

Löst eine Operation bzw. Methode diese Ausnahme nicht aus, wird sie grundsätzlich unterstützt.

Unerlaubter Typ

ClassCastException:
unerwünschte Klasse

Will man Typen ausschließen, z.B. in einer Kollektion nur einen Typ von Objekten zulassen, lautet die Konvention:

gp  ClassCastException: Klasse ist nicht erlaubt.

Unerlaubter Aspekt

Für alle anderen Fällen, die eine Änderung der Kollektion ausschließen, gibt es:

IllegalArgumentException:
unerwünschter Aspekt

gp  IllegalArgumentException: Gewisse Aspekte verhindern eine Änderung.

Ein Beispiel ist eine Kollektion, welche keine null-Elemente zulässt.

Indikator: Rückgabetyp boolean

Bedeutung des Resultats false

Bei Methoden mit Rückgabetyp boolean bedeutet der Rückgabewert false nicht, dass die Methode ungültig ist. Der Wert false hat abhängig vom Kontext folgende Semantik:

»nicht vorhanden«, »bereits enthalten« etc.
Galileo Computing

14.2.4 Kontrakte  downtop

Kontrakte beschreiben Abmachungen oder Verträge. Sie können informell sein (»es wäre schön, wenn ...«) oder in Form von Zusicherungen bzw. Constraints auftreten (siehe 6.12).

Informeller Kontrakt

Informeller Kontrakt zwischen compare() und equals()

Es wäre schön, wenn zwischen compare() und equals() gilt:

o1.equals(o2)==true Û compare(o1,o2)==0

Werden informelle Kontrakte nicht eingehalten, sollte dies dem Anwender gegenüber klar ausgewiesen werden.

Constraints: unbedingter Kontrakt

gp  Verletzungen von Kontrakten in Form von Constraints dürfen dagegen erst gar nicht auftreten.

Wenn man Glück hat, treten Kontraktverletzungen in Form von Laufzeitfehlern auf, wenn man Pech hat, führen sie zu semantischen Fehlern.

Kontrakt-Management by Java

Da Interfaces keine Spezifikation von Constraints in der Form von Invarianten oder Pre- und Post-Konditionen kennen, haben die Designer zum Hilfsmittel »Kommentar« gegriffen.

Somit werden zu jeder Methode die Kontrakte in verbaler Form aufgeführt. Ein kurzes Beispiel (Tab. 14.1):

Kontrakt am Beispiel add()

Tabelle 14.1   Hinzufügen eines Elements mit add() in eine Collection
Betreff add()-Kontrakt bzw. -Constraints
Invariante Kollektion enthält immer Element nach (normalem) Ablauf der Operation
Rückgabewert true: Kollektion wurde geändert
false: Kollektion enthält bereits Element und es sind keine
           Duplikate erlaubt (z.B. Set)
Ausnahmen UnsupportedOperation: add() wird nicht unterstützt
ClassCast: Klasse nicht erlaubt
IllegalArgument: Gewisser Aspekt verhindert
Einfügen (z.B. null-Wert)

Icon

Kontraktverletzung

Kontraktverletzungen

Wird ein Kontrakt – wie in Tab. 14.1 formuliert – bei der Implementation nicht eingehalten, führt dies z.B. bei einer Methode wie add() zu unterschiedlichen Ergebnissen, je nach dem, welche konkrete Klasse »hinter« dem Interface steht.


Galileo Computing

14.3 Kollektions-Klassen  downtop

Bedingt durch das Interface-Framework unterteilen sich die Klassen-Implementationen in zwei Hierarchien (Abb. 14.4).

Klassen-
Hierarchie zum Framework


Abbildung
Abbildung 14.4   Klassen im Framework

Legacy-Klassen

Die mit «Java 1.0» gekennzeichneten Legacy-Klassen sind in das neue Framework integriert worden, obwohl sie eigentlich überflüssig sind.


Galileo Computing

14.3.1 Auswahl der Kollektions-Klasse  downtop

Klonbar/Serialisierbar, keine
Synchronisation

Grundsätzlich sind alle neuen Klassen serialisierbar und können geklont werden, sind aber nicht synchronisiert.

Primäre Implementation

Zu den grundlegenden Typen Set, List und Map gibt es jeweils zwei Implementationen. Als primäre (Standard-)Implementationen werden die folgenden Klassen angesehen.

HashSet

gp  HashSet: Basiert auf einer Hash-Tabelle, ist konstant schnell für die meisten Operationen, aber ungeordnet.

ArrayList

gp  ArrayList: Basiert auf einem Array, das dynamisch wachsen kann, ist konstant schnell im Zugriff, dafür linear ansteigendes Zeitverhalten beim Einfügen von Elementen am Kopf.

HashMap

gp  HashMap: Analog zu Set.

Icon

Bei der Wahl einer Klasse hilft ein Entscheidungsbaum (Abb. 14.5).

Entscheidungsbaum mit Auswahlkriterien


Abbildung
Abbildung 14.5   Auswahlkriterien einer konkreten Klasse

Tuning mit Hilfe von Konstruktoren

Tuning

Sofern man wirklich optimale Performance haben möchte, können mit Ausnahme von LinkedList und Trees den Konstruktoren die Anfangsgröße und evtl. noch der Füllungsgrad übergeben werden (siehe »Hash-Kollektionen und Tuning«).


Galileo Computing

14.3.2 Hash-Kollektionen und Tuning  downtop

Hashing:
optimaler Zugriff

HashMap und somit HashSet, die intern eine Hashmap verwendet, erlauben den schnellsten Zugriff auf ihre Elemente.

Zugriffe sowie Einfügungen hängen allerdings von der optimalen Größe der Hash-Tabelle und dem maximalen Füllungsgrad ab.

gp  Bei Kenntnis der voraussichtlichen Anzahl der Elemente in einer Hash-Kollektion sollten die Default-Werte im Konstruktor mit optimalen Werten überschrieben werden.

Die Grundlagen zu Hash-Tabellen und der Zusammenhang mit der hashCode()-Methode in Object wurde bereits in 10.1.3 und 10.1.4 behandelt.

Bucket

Unter einem Bucket versteht man die Einträge im Hash-Array, d.h. die Liste mit den Elementen. Die Default-Anzahl der Buckets ist 11.

Icon
Tuning über
Füllungsgrad/Lastfaktor

Der Füllungsgrad bzw. Lastfaktor gibt an, wann die Hash-Tabelle vergrößert werden soll, um neue Einträge aufzunehmen. Somit ist loadFactor eine positive Zahl und hat den Default-Wert 0.75f (Typ float).

gp  Ein kleinerer loadFactor erhöht die Zugriffsgeschwindigkeit auf Kosten des benötigten Platzes.
gp  Ein Rehashing ist sehr zeitintensiv und unbedingt zu vermeiden.

Der Default-Füllungsgrad 0.75 ist in der Regel ein guter Kompromiss zwischen optimaler Zugriffszeit und Speicherplatzverbrauch.

Eine Vergrößerung der Anzahl der Buckets mit anschließendem Rehashing erfolgt nach den beiden Formeln:

      numberOfElements > loadFactor 
* numberOfBuckets
newNumberOfBuckets= 2 * oldNumberOfBuckets + 1

Icon

Damit ergeben sich folgenden Performance-Regeln:

Performance-
Einflussfaktoren

1. Array-Anpassungen bzw. Rehashing wird vermieden, sofern gilt:
2. Primzahlen sind optimale Werte für numBuckets, da sie eine bessere Verteilung der Elemente auf die Buckets ergeben. Primzahlen mit besten Empfehlungen sind:
              89, 359, 719, 2879, 11519, 737279
3. Hashcodes, die sich in der Kollektion ändern bzw. sehr häufig verwendet werden, müssen bzw. sollten als Feld separiert werden.

Beispiele

Beispiel zur
1. und 2. Regel

Im ersten Test werden nur die ersten zwei Tuning-Regeln mit Hilfe von Strings getestet:

class Test {
  public static void main(String[] args) {
    //Set hs = new HashSet();                    // :: 
2200
    //Set hs = new HashSet(23039);     
          // :: 2180
    //Set hs = new HashSet(200000);              // :: 
2050
    Set hs = new HashSet(737279);      
          // :: 1500
    long t= System.currentTimeMillis();
    for (int i= 0; i<100000; i++) hs.add("String"+i);
    System.out.println(System.currentTimeMillis()-t);
  }
}

Beispiel zur
3. Regel bzw. zu einem informellen Kontrakt

Ergebnis: Nur die Kombination der ersten mit der zweiten Regel zeigt bei den Standard-Implementationen eine gute Wirkung.

Das nächste Beispiel verdeutlicht die dritte Regel bzw. das Zusammenspiel von hashCode() und equals().

class Element {
  String s;                           // mutable
  private int hashCode;               // immmutable
  public Element (String s) {
    this.s= s; 
    hashCode= s.hashCode();                               ¨
  }
  /* Variation mit hashCode()
  public int hashCode() {
    return s.hashCode();                                  ¦ 
    //return hashCode;                                    Æ 
  }
  */
  // ignoriert Groß-/Kleinschreibung bei Test auf Gleichheit
  public boolean equals(Object o) {
    if (this==o) return true;         // wie Object.equals()
    if (o==null || Element.class!=o.getClass()) 
      return false;
    return s.equalsIgnoreCase(((Element)o).s);
  }
}

Der Test bestätigt die Regeln in 10.1.3:

contains()-
Ergebnisse

class Test {
  public static void main(String[] args) {
    Set hs = new HashSet();
    Element e1= new Element("MutableElement"),
            e2= new Element("mutableElement");
    hs.add(e1);
    e1.s= "mutableelement";             // e1 wird geändert!
    System.out.println(hs.contains(e1)  // siehe unten!
                  +" "+hs.contains(e2));   
  }
}

Erklärung: Ohne die hashCode()-Variante kann man equals() weglassen. Denn Object.hashCode() liefert nur gleiche Werte für identische Instanzen. Folglich wird equals() immer nur für identische Objekte aufgerufen.

Variante: Erst mit der hashCode()-Methode wird die Sache interessant.

Zu ¨: Die Ausgabe ist false false, da die erste Regel aus 10.1.3 verletzt wird. e1 hat nach der Änderung nicht mehr denselben Hashcode.

Zu ¦: Die Ausgabe ist true false. Mit einem immutable Hash-Feld wird zumindest e1 wieder gefunden, e2 ist aber immer noch nicht gleich. Dies liegt aber an der Semantik in Zeile ¨.

Zu Æ: Die erste Regel aus 10.1.4 ist verletzt. Besser wäre wohl die folgende Anweisung gewesen:

        hashCode= s.toLowerCase().hashCode();

Icon

Fazit

Gleiche Semantik für equals() und hashCode()

Für Operationen wie Suchen, Einfügen und Entfernen von Elementen verwenden Hash-Kollektionen zuerst die hashCode()-Methode, um das Bucket zu bestimmen. Dann traversieren sie durch das Bucket, um mittels equals() das Element zu finden. Somit folgt:

gp  Zur equals()-Semantik muss immer die von hashCode() passen.

Immutable
Hashcodes für Kollektionen

gp  Objekte, deren Hashcode sich ändert, während sie in der Kollektion sind, werden nicht wieder gefunden.

Die letzte Implikation kann entweder durch ein Feld für den Hashcode behoben werden, das für die Dauer in der Kollektion immutable bleibt (siehe o.a. Beispiel), oder durch ein Rehash.


Galileo Computing

14.3.3 HashMap  downtop

Generell erlaubt eine Map einen direkten Zugriff auf ein Objekt über einen assoziierten Schlüssel. Die Methoden zum Einfügen bzw. Lesen von Schlüssel/Wert-Einträgen sind die Methoden put() bzw. get().

HashMap:
put() und get()

Map hm= new HashMap();
hm.put("3-934358-01-2","JavaScript");
hm.put("Ch.Wenz","JavaScript");
hm.put("1-56592-455-x",null);  // in Hashtable nicht erlaubt!
hm.put("1-56592-455-x","JAVA Swing");
hm.put("0-201-54848-8","C++ Primer");
System.out.println(hm.get("1-56592-455-x")); 
// :: Java Swing

Collections-Sichten zur Map:
values() keySet() entrySet()

Eine Map kann durchaus wie eine Kollektion behandelt werden. Es stehen je nach Zweck drei alternative Methoden zur Verfügung:

   Collection values();  // liefert die Werte als Kollektion
   Set keySet();         // liefert die Schlüssel als Menge
   Set entrySet();       // liefert die Einträge als Menge

Da Werte im Gegensatz zu Schlüsseln und Einträgen mehrfach vorkommen können, kann die erste Methode kein Set liefern.

remove():
Map-Einträge

System.out.println(hm.values());
// :: [C++ Primer, JAVA Swing, JavaScript, JavaScript]
System.out.println(hm.keySet()); 
// :: [0-201-54848-8, 1-56592-455-x, Ch.Wenz, 3-934358-01-2]
System.out.println(hm.entrySet()); 
// :: [0-201-54848-8=C++ Primer, 1-56592-455-x=JAVA Swing,
        Ch.Wenz=JavaScript, 3-934358-01-2=JavaScript]
System.out.println(hm.remove("abc"));     // :: null
System.out.println(hm.remove("Ch.Wenz")); 
// :: JavaScript

Icon

Regeln zur Collection-Sicht auf eine Map

Map-Collection-Sicht

gp  Die Reihenfolge in den drei Kollektionen ist nicht festgelegt.
gp  Die drei Kollektionen zur Map sind keine Kopien, sondern liefern die Original-Objekte der Map.
hm.values().remove("JavaScript"); 
   // entfernt Originaleintrag
hm.keySet().remove("1-56592-455-x"); // 
dito
((Map.Entry)hm.entrySet().iterator().next()).setValue("?");
System.out.println(hm.entrySet());
         // :: [0-201-54848-8=?, 
3-934358-01-2=JavaScript]

Galileo Computing

14.3.4 Listen  downtop

Liste: Elementzugriffe wie bei Arrays

Die Elemente in Listen sind wie bei Arrays indiziert. Im Gegensatz zu einem Array können Listen aber dynamisch wachsen und die Elemente können an beliebigen Stellen eingefügt und entfernt werden.

Man kann bei Listen zwischen den konkreten Implementationen ArrayList und LinkedList wählen.

ArrayList

ArrayList:
die Standardwahl

Die Klasse ArrayList ist in der Regel die beste Wahl. Sie basiert intern auf Arrays und ist damit im Elementzugriff konstant schnell.

Wie bei der Hash-Tabelle kann im Konstruktor einer ArrayList die Anfangsgröße des internen Arrays gesetzt werden.

Der Default-Wert der Array-Größe ist 10. Reicht sie bei einem Neueintrag nicht aus, vergrößert sie sich um jeweils oldArraySize * 3 / 2 + 1, d.h. um ca. 50%.

Die interne Array-Größe ist damit wieder ein Tuning-Faktor.

Icon
Tuning der ArrayList-Größe

Denn bei jeder Vergrößerung der ArrayList muss eine neues Array angelegt werden, die Elemente mittels System.arraycopy() kopiert und das alte Array vom Garbage-Collector entsorgt werden.

gp  Die anfängliche Größe einer ArrayList sollte etwa der Anzahl der aufzunehmenden Elemente entsprechen.
gp  ArrayList ersetzt im neuen API die Legacy-Klasse Vector.

ArrayList:
die neue Vector-Klasse

Vector ist grundsätzlich synchronisiert, ArrayList mit Hilfe des Wrappers Collections.synchronizedList() nur bei Bedarf (siehe Synchronisierte Wrapper) und hat die besseren Methoden sowie konsistentere Benennungen.

LinkedList

Für häufiges Einfügen oder Löschen am Anfang der Liste bzw. Iterieren mit gleichzeitigem Löschen ist LinkedList die geeignete Implementation, sofern kaum Zugriffe über den Index erfolgen.

LinkedList:
Verkettung mit Vor- und
Nachteilen

Die LinkedList speichert jedes Element separat mit einer Referenz auf den Vorgänger und Nachfolger und es gibt kein Tuning.

gp  Im Gegensatz zu ArrayList ist deshalb die eigentliche Einfüge- und Lösch-Operation am Kopf oder Ende der Liste gleich schnell.
gp  Für jede Positionierung muss aber durch die Liste traversiert werden (im ungünstigsten Fall listSize/2-Operationen).

Zusätzliche Methoden der LinkedList

Die LinkedList ist aufgrund von zusätzlichen Kopf-/Ende-Operationen gegenüber einer List besser für eine Queue oder eine DeQueue geeignet, da sie gerade hier geeignete Operationen zur Verfügung stellt:

         addFirst()      addLast()
         getFirst()      getLast() 
         removeFirst()   removeLast()

Preis: Die Nutzung der Methoden wird damit bezahlt, dass die LinkList-Instanz nicht mehr generisch als List verwendet werden kann.

Das Henne-Ei-Problem: Queue/Dequeue

Probleme mit der Vererbung:
Queue/DeQueue

Will man auf Basis einer Liste eine Klassen-Hierarchie zu Queue und Dequeue implementieren, steht man schnell vor einem Problem:

    class Queue   extends LinkedList { ... }    
          ¨
    class Dequeue extends Queue      { ... }              ¦

oder vielleicht doch besser umgekehrt:

    class Dequeue extends LinkedList { ... }    
          Æ
    class Queue   extends Dequeue    { ... }              Ø 

Zu ¨ und Æ: Der Vorteil, alle passenden Methoden und deren Code einfach zu erben, geht mit dem gravierenden Nachteil einher, dass man auch alle unerlaubten Operationen übernimmt.

Also bleibt einem keine andere Wahl, als alle unerlaubten Operationen so zu überschreiben, dass sie z.B. eine IllegalMethodException auslösen. Dies gilt übrigens auch für Ø.

Und – voilà – der Code ist umfangreicher als der, den man ohne Vererbung zu schreiben hätte. Zusätzlich traktiert man den Anwender noch mit Ausnahmen.

Zu ¦: Bei dieser Vererbung können eigentlich alle Operationen von Queue in Dequeue übernommen werden, zu dem Preis, dass die Vererbung auf den Kopf gestellt wird.

Minimalistische Implementation von Queue und Priority-Queue

PriorityQueue
Is-A Queue

Bei eine Priority-Queue werden anhand einer Priorität Elemente hinter das letzte Element mit gleicher Priorität eingefügt. In diesem Fall greift die Vererbung:

    class PriorityQueue extends Queue { ... }

Bis auf die add()-Methode sind alle Operationen gleich. Die folgenden Klassen beschränken sich nur auf die notwendigen Methoden:

Queue:
Implementation auf Basis von Delegation

class Queue {
  protected List l;                          // Delegation
  public Queue () { l= new LinkedList(); }
  public Queue (List l) {                    // Flexibel!
    this.l= l!=null? l: new LinkedList();
  }
  void add (Object o) { 
    if (l instanceof LinkedList) ((LinkedList)l).addLast(o);
    else l.add(l.size(),o);
  }
  public Object remove() {
    if (l instanceof LinkedList)
      return ((LinkedList)l).removeFirst();
    else return l.remove(0);
  }
  public boolean isEmpty() { return l.isEmpty(); }
  public String toString() { return l.toString(); }
}

Priority-Objekte:
pro Interface-, contra Reflexions-Lösung

// Für PriorityQueue: Jedes Objekt braucht 
eine Priorität
interface IPriority {  int getPriority(); 
}

PriorityWrapper

// Eine Hülle für Objekte in der PriorityQueue 

class PriorityWrapper implements IPriority {
  Object o;
  int p;
  public PriorityWrapper (Object o, int priority) 
{
    p= priority; this.o= o;
  }
  public int getPriority() { return p; }
  public String toString() { return o.toString(); }
}

Nachfolgend braucht die Klasse PriorityQueue nur die Methode add() zu überschreiben:

PriorityQueue

class PriorityQueue extends Queue {
  final int DEFAULT_PRIORITY;
  public PriorityQueue () { this(0); }
  public PriorityQueue (int defaultPriority) {
    // Für eine PriorityQueue ist eine ArrayList besser
    super(new ArrayList());
    DEFAULT_PRIORITY= defaultPriority;
  }

Overriding von add(), Object-Check: IPriority oder Wrapper

  void add(Object o) 
{
    int p;
    try { p= ((IPriority)o).getPriority(); 
    // keine IPriority implementiert, dann Wrapper!
    } catch (ClassCastException e) {
      o= new PriorityWrapper(o,p= DEFAULT_PRIORITY); 
 
    }
    int i= l.size();
    while (0<i && ((IPriority)l.get(i-1)).getPriority()<p)
      i--;
    l.add(i,o);
  }
}

Abschließend ein kleiner Test zur Priority-Queue:

class Test {
  public static void main(String[] args) {
    PriorityQueue q= new PriorityQueue();
    q.add(new PriorityWrapper("1",1));
    q.add(new PriorityWrapper("2",2));
    q.add(new PriorityWrapper("3",2));
    q.add(new PriorityWrapper("4",1));
    q.add("5");                       // Default-Priorität 0
    q.add(new PriorityWrapper("6",2));
    System.out.println(q);          // :: 
[2, 3, 6, 1, 4, 5]
    System.out.println(q.remove()); // :: 
2
    System.out.println(q);          // :: 
[3, 6, 1, 4, 5]
 }
}

Galileo Computing

14.3.5 Comparable und Comparator  downtop

Um für Objekte eine Ordnung zu definieren, bedient sich Java der Interfaces Comparable bzw. Comparator (siehe auch Abb. 14.3).

Comparable

Comparable: natürliche Ordnung einer Klasse

gp  Hat eine Klasse eine so genannte natürliche Ordnung, nach der die Elemente sortiert werden können, implementiert die Klasse nach Java-Konvention das Interface Comparable:
     class ClassWithNaturalOrdering implements Comparable

Bekannte Beispiele sind die Klassen String, Date, BigDecimal und alle Wrapper-Klassen von primitiven Zahlentypen.

Zu der einzigen Methode des Interfaces Comparable

         int compareTo(Object o);

Kontrakt zu
compareTo()

gehört der folgende Kontrakt:

                 o1.compareTo(o2) 
< 0  Û 
o1 < o2
                 o1.compareTo(o2)== 
0 Û 
o1== o2
                 o1.compareTo(o2) 
> 0 Û 
o1 > o2

Bei Ungleichheit ist also nur das Vorzeichen für die Ordnung ausschlaggebend, nicht der Zahlenwert, und compareTo() und equals() sollten Gleichheit übereinstimmend definieren.

compareTo() und ClassCastException

Wie bei equals() muss die Implementierung natürlich zuerst die Objekt-Klassen auf Kompatibilität überprüfen, aber im Gegensatz zu equals() sollte sie bei Klassen-Unverträglichkeit mit einer ClassCastException reagieren (Konvention bei String etc.).

Comparator

Comparator:
Definition von zusätzlichen Ordnungskriterien

Hat eine Klasse

gp  keine natürliche Ordnung oder
gp  eine natürliche Ordnung, die man durch eine eigene ersetzen will,

muss man den sortierten Kollektionen wie TreeSet oder Methoden wie Collections.sort() eine Instanz einer Klasse übergeben, die das

interface Comparator { int compare(Object o1, Object o2); }

implementiert. Die Methode compare() hat denselben Kontrakt wie compareTo() zu erfüllen, liefert also die o.a. Werte bei Objektvergleich.


Galileo Computing

14.3.6 Bäume (Trees)  downtop

TreeSet/TreeMap: sortiertes Einfügen

Muss nach jedem Einfügen eines Elements die Menge oder Map wieder sortiert sein, so kann man direkt auf eine sortierte Implementation wie TreeSet oder TreeMap zurückgreifen.

Das Einfügen von Elementen in eine sortierte Kollektion ist gegenüber einer unsortierten eine kostspielige Operation, da bis zu log2 n Vergleiche notwendig sind.

Alternative:
nur Sortieren bei Bedarf

gp  Alternativ kann man für Listen bzw. Arrays mit Hilfe der Methode Collections.sort() bzw. Arrays.sort() nachträglich nur dann sortieren, wenn es notwendig ist.

Zusätzliche
Tree-Operationen

TreeSet bzw. TreeMap implementiert zwei Interfaces SortedSet bzw. SortedMap, die äquivalente Operationen anbieten (siehe Abb. 14.1 und 14.2):

gp  Zugriff auf den Comparator, sofern definiert.
gp  Zugriff auf das erste und letzte Element in der Sortierfolge, die durch Comparable oder Comparator definiert wurde.
gp  Rückgabe eines Bereichs der Menge bzw. Map mit definiertem Anfangs- und Endpunkt.

Beispiele

HashSet vs. TreeSet:
ein simpler Zeitvergleich

Zuerst ein Zeitvergleich zwischen HashSet und TreeSet:

long t;
Set s1= new HashSet(), s2= new TreeSet();
t= System.currentTimeMillis();
for (int i=0; i<100000; i++)
  s1.add(new Integer((int)(100000.0*Math.random())));
Arrays.sort(s1.toArray());   
System.out.println(System.currentTimeMillis()-t); // :: 
1328
t= System.currentTimeMillis();
for (int i=0; i<100000; i++)
  s2.add(new Integer((int)(100000.0*Math.random())));
System.out.println(System.currentTimeMillis()-t); // :: 
1531

Ergebnis: Selbst wenn eine HashSet nachträglich sortiert wird, ist dies noch schneller als direktes sortiertes Einfügen bei einer TreeSet.

Im folgenden Beispiel wird ein TreeSet einmal mit Teilen nach der Nummer sortiert, ein andermal nach der Bezeichnung sortiert gefüllt.

Teile-Klasse mit natürlicher Ordnung

class Teil implements Comparable {
  private int nr;                      // nur positiv!
  private String bez;
  public Teil(int nr, String bez) {
    this.nr= Math.abs(nr); this.bez=bez; 
 
  }
  public int    getNr()  { return nr;  }
  public String getBez() { return bez; }
  public void   setString(String s) { bez= s; }

compareTo()-
Implementation

  public int compareTo(Object t) {  // natürliche 
Ordnung
    return nr - ((Teil)t).nr;  // führt zu keinem Überlauf!
  }
  public String toString() { return nr+":"+bez; }
}
class Test {
  public static void main(String[] args) {
    Teil [] tarr= new Teil[] { new Teil(7,"7"), 
                               new Teil(13,"13"),
                               new Teil(20,"20"), 
                               new Teil(3,"3")    };
    System.out.println(new TreeSet(Arrays.asList(tarr))); 

                           // :: [3:3, 
7:7, 13:13, 20:20]

Zusätzliche lexikographische Ordnung für
die Teile

    Set s= new TreeSet(
      // anonyme Klasse, die nach Teilebezeichnung sortiert 
      new Comparator() {                             
       ¨
        public int compare(Object t1, Object t2) {
          return 
          (Teil)t1).getBez().compareTo(((Teil)t2).getBez());
        }
      });
    s.addAll(Arrays.asList(tarr));
    System.out.println(s); // :: [13:13, 
20:20, 3:3, 7:7]
  }
}

Zu ¨: Die natürliche Ordnung der ganzen Zahlen wurde durch eine Comparator-Instanz mit einer lexikographischen Ordnung ersetzt.


Galileo Computing

14.3.7 Bereichs-Operationen: Bäume und Listen  downtop

Icon
Kontrakt für Bereichs-Operationen

Das List-Interface hat nur eine subList()-Methode, die beiden Baumklassen bieten jeweils drei äquivalente Bereichs-Operationen sub<X>, head<X> und tail<X>. Es gelten folgende Kontrakt-Regeln:

1. Bei allen Bereichs-Operationen werden keine Kollektionen mit neuen Elementen erschaffen, sondern es wird nur ein Ausschnitt aus der Original-Kollektion zurückgeliefert (ohne Elemente zu klonen).
2. Werden Elemente zu einer Teilkollektion hinzugefügt oder von ihr entfernt, betrifft dies die Original-Kollektion.
3. Werden Elemente zu einer Liste hinzugefügt oder von ihr entfernt, ist eine vorher mit subList() erschaffene Teilliste ungültig und löst bei Benutzung eine Ausnahme aus (siehe u.a. Beispiel).
4. Werden Elemente zu einem Baum hinzugefügt oder von ihm entfernt, sind die vorher erschaffenenen Teilbäume weiterhin gültig und enthalten ebenfalls die Änderung, sofern sie den Bereich betreffen.

Beispiele

Der erste Test demonstriert die erste und zweite Regel:

class Teil implements Comparable { /* wie in Bäume (Trees) */ }

Bereich: ein nach oben halboffenes Intervall

class Test {
  public static void main(String[] args) {
    Teil [] tarr= new Teil[] { new Teil(7,"t0"), 
                               new Teil(13,"t1"),
                               new Teil(20,"t2"), 
                               new Teil(3,"t3")   };

subSet()
headSet()

    SortedSet orgs= new TreeSet(Arrays.asList(tarr));
    // die Bereichs-Operation schließt das 
    // untere Element ein und das obere Element aus
    SortedSet sub1= orgs.subSet(tarr[3],tarr[2]);
    SortedSet sub2= orgs.headSet(tarr[2]);
    System.out.println(sub1); // :: [3:t3, 7:t0, 13:t1]
    sub2.clear();
    System.out.println(sub1); // :: []
System.out.println(orgs); // :: [20:t2] } }

Ergebnis: Es gibt nur eine Originalliste mit verschiedenen Sichten.

Im folgenden Test wird die dritte und vierte Regel demonstriert:

class Teil implements Comparable { /* wie in Bäume (Trees) */ }
class Test {
  public static void main(String[] args) {
    Teil [] tarr= new Teil[] { new Teil(7,"t0"), 
                               new Teil(13,"t1"),
                               new Teil(20,"t2"), 
                               new Teil(3,"t3")   };
    SortedMap orgm= new TreeMap();
    for (int i=0; i<tarr.length; i++)
      orgm.put(new Integer(tarr[i].getNr()),tarr[i]);

tailMap()-Bereich

    SortedMap sub= orgm.tailMap(new Integer(7));
    System.out.println(sub); // :: {7=7:t0, 13=13:t1, 20=20:t2}
    // nach 4. Regel ok
    orgm.remove(new Integer(7));
    System.out.println(sub); // :: {13=13:t1, 
20=20:t2}
    // Arrays.asList(tarr) ist immutable, deshalb:
    List l= new ArrayList(Arrays.asList(tarr));
    // es gibt weiterhin nur vier Teile: geändert 
wird tarr[2]   
    ((Teil)l.get(2)).setString("T2");
    // diese Änderung reflektiert dann auch 
die Map
    System.out.println(orgm.get(new Integer(20))); // :: 
20:T2
    List subl= l.subList(1,3);
    System.out.println(subl); // :: [13:t1, 
20:t2]
    l.remove(0);

ConcurrentModificationException

    // 3. Regel wird verletzt, erzeugt Ausnahme:
    System.out.println(subl); 
    // :: java.util.ConcurrentModificationException
    // ::    at  ...
  }
}
gp  Die vier o.a. Bereichs-Regeln können nicht von den Interfaces überprüft werden. Dies gilt ebenfalls für die Bereichs-Operationen, die nach Konvention das untere Element enthalten und das obere ausschließen.

Galileo Computing

14.4 Iterator-Pattern  downtop

Icon

Iteratoren sind spezialisierte Adapter. Das Adapter-Muster wurde bereits in 9.12.3 vorgestellt und verwendet.

Iterator-Pattern

Das Iterator-Pattern separiert den sequenziellen Zugriff auf eine Kollektion vom Kollektions-Objekt selbst und verlagert es in ein Iterator-Objekt, das in der Regel von einem Iterator-Interface abgeleitet wird (Abb. 14.6).


Abbildung
Abbildung 14.6   Iterator-Pattern

Iteratoren als private Member-Klassen

Auch die im Collection-Framework enthaltenen Klassen folgen dem Muster und implementieren die Iteratoren als private Member-Klassen. Die Klasse HashMap enthält z.B. die Klasse HashIterator:

public class HashMap extends AbstractMap
       implements Map, Cloneable, java.io.Serializable {
  ...
  private class HashIterator implements Iterator 
{ ... }
}
gp  Da die Iterator-Member-Klasse private deklariert ist und ihre Objekte nur über Interface-Referenzen nach außen geliefert werden, bleibt die konkrete Iterator-Klasse opak.
gp  Diese Umsetzung des Interface-Prinzips ist recht wirkungsvoll, da die Iteratoren nicht mehr als Klassen-Objekt (z.B. HashIterator) angesprochen werden können, sondern nur noch als Iterator.

Das Iterator-Pattern sieht eigentlich keine Methoden zur Veränderung der Kollektionen vor.

Iterator: remove() zur Konvenienz

Hier weicht die Java-Umsetzung vom Muster ab, da im Iterator eine Methode remove() zur Konvenienz enthalten ist, die im veralteten Iterator Enumeration nicht enthalten war.

Ein wichtiger konzeptioneller Unterschied zu Indizes von Arrays ist die Art, wie Java-Iteratoren eine Kollektion durchlaufen:

Iterator:
kein Index!

gp  Ein Iterator positioniert sich nicht wie ein Index auf einem Element, sondern vor einem Element und liefert die Referenz beim Überspringen (siehe Abb. 14.7).

Wirkung der Methoden iterator(), hasNext() und next()


Abbildung
Abbildung 14.7   Iterieren einer Kollektion

Icon
Iteratoren-
Verhalten

Iteratoren verhalten sich bei Entfernen von Elementen:

1. Iteratoren (mit Ausnahme der Listen-Iteratoren) liefern die Elemente einer Kollektion in keiner fest vorgegebenen Reihenfolge.
2. Die Methode remove() entfernt das zuletzt mit next() übersprungene Element einer Kollektion.
3. Wird mehr als ein Iterator zur Kollektion erzeugt, werden durch einen Aufruf von remove() alle anderen Iteratoren ungültig, d.h., ihre weitere Benutzung führt zu einer ConcurrentModificationException.

Beispiele

Zur ersten Regel: Die Rückgabe der Elemente ist bei einer Set nicht definiert:

Set s= new HashSet(Arrays.asList(
                   new String[] {"a","b","c","d","e"}));
Iterator i= s.iterator();
while (i.hasNext())
  System.out.print(i.next()+" "); // :: 
b a e d c

Zur zweiten Regel: Ein Aufruf von remove() ohne vorhergehendes next() führt unweigerlich zu einer IllegalStateException:

Set s= new HashSet(Arrays.asList(
                     new String[] {"a","b","c","d","e"}));
Iterator i= s.iterator();
i.remove();             // ß Fehler, kein vorheriges next()
while (i.hasNext())
  i.remove();           // ß Fehler, dito

remove() bei mehreren
Iteratoren

Zur dritten Regel: Zwei Iteratoren und der Aufruf von remove():

Set s= new HashSet(Arrays.asList(
                     new String[] {"a","b","c","d","e"}));
Iterator i1= s.iterator(), i2= s.iterator();
i2.next(); i2.remove();         // i1 ist 
nun ungültig!
System.out.println(i1.next());  // Ausnahme!

Diese letzte Einschränkung macht remove() ziemlich unsicher.


Galileo Computing

14.4.1 Iteratoren zu Maps  downtop

Keine Map-
Iteratoren!

Da eine Map keine Kollektion ist, gibt es für eine Map nicht unmittelbar einen Iterator. Hier wählt man den Weg über eine der drei Kollektions-Sichten auf eine Map (siehe auch HashMap).

Beispiel

// class Teil implements Comparable { /* wie in 
Bäume (Trees) */ }
Teil[] tarr=new Teil[] {new Teil(7,"t0"),new Teil(13,"t1")
                       ,new Teil(20,"t2"),new Teil(3,"t3")};
SortedMap map= new TreeMap();
for (int i=0; i<tarr.length; i++)
  map.put(new Integer(tarr[i].getNr()),tarr[i]);
for (Iterator i= map.entrySet().iterator(); i.hasNext();)   ¨
  System.out.print(((Map.Entry)i.next()).getValue()+" ");   ¦
  // :: 3:t3 
7:t0 13:t1 20:t2

Zu ¨: In diesem Code-Fragment wird zu der Entry-Menge einer Map ein Iterator erzeugt.

Zu ¦: Die Objekte, die dann vom Iterator zurückgeliefert werden, sind Instanzen von Map.Entry, von denen mit getValue() die Objekte geholt werden können.


Galileo Computing

14.5 Fundamentale vs. Template-Methoden  downtop

Die Methoden in den Interfaces lassen sich in zwei Gruppen einteilen:

Fundamentale und generische Algorithmen

gp  fundamentale Operationen, die sich nicht mit Hilfe eines Algorithmus aus den anderen ableiten lassen,
gp  generische Algorithmen, die unabhängig von der Implementation nur mit Hilfe der fundamentalen Operationen geschrieben werden.

Die generischen Algorithmen führen zu Template-Code bzw. Methoden, die nur Interface-Referenzen und nicht Klassen enthalten. Sie sind damit allgemein gültig, sofern nur der Kontrakt eingehalten wird.

Als Beispiel ein Original-Code aus der Klasse AbstractCollection:

public boolean contains(Object o) {
  Iterator e = iterator();
  if (o==null) {
    while (e.hasNext()) 
      if (e.next()==null) return true;
  } else {
    while (e.hasNext())
      if (o.equals(e.next())) return true;
  }
  return false;
}

Galileo Computing

14.5.1 Das Template-Problem  downtop

Viele Templates bzw. generische Algorithmen, ob statisch oder nicht statisch, gehören eigentlich nicht in Klassen, sondern in die Interfaces, was aber in Java ausgeschlossen ist. Dieses Dilemma wird von den Designern wie folgt gelöst.

Abstract<X>-Klassen für generische Instanz-
Algorithmen

Generische Algorithmen

gp  als Instanz-Methoden sind in den abstrakten Klassen Abstract<X> enthalten.10 

Collections-, Arrays-Klasse für statische generische Algorithmen

gp  als statischen Methoden sind in Collections bzw. Arrays enthalten.

Im Plural der Namen ist angedeutet, dass beide Klassen eine Ansammlung von generischem Code für Kollektionen bzw. Arrays darstellen.

Deshalb sollte man sich beim Umgang mit Kollektionen an das folgende, nützliche Prinzip halten:

gp  Suche vor dem Codieren zuerst nach passenden Methoden in den Klassen Collections oder Arrays!

Denn der Hauptvorteil dieser Templates gegenüber eigenem Code liegt in den klar definierten Eigenschaften, z.B. bei den folgenden Sortier-Operationen und der Robustheit des sorgfältig getesteten Codes.


Galileo Computing

14.5.2 Sortieren vs. Shuffling  downtop

In Bäume (Trees) wurde bereits eine Set nachträglich durch Umwandlung in ein Array mit Ausführung von Arrays.sort() sortiert.

sort(): stabil mit Performance
O(n log2 n)

Mit zwei sort()-Methoden der Klasse Collections lassen sich dagegen Listen mit Comparable bzw. Comparator sortieren.

gp  Der Sortier-Algorithmus in sort() ist ein modifizierter Mergesort, der eine garantierte Performance11  von O(nlog2n) hat und stabil ist.

Stabilität

gp  Ein Sortier-Algorithmus heißt stabil, wenn er Elemente, die gleich12  sind, nicht umordnet.

Neben der optimalen Performance ist Stabilität dann wichtig, wenn eine Sortierung einer anderen folgt (siehe hierzu das u.a. Beispiel).

shuffle(): gut gemischt

Die beiden shuffle()-Methoden in Collections stellen inverse Operationen zum Sortieren dar. Sie erzeugen Listen, bei denen jede Permutation der Elemente als Liste mit gleicher Wahrscheinlichkeit auftreten kann, mit anderen Worten das Gegenteil von dem, was man unter Sortieren versteht.

Beispiel

Beispiel zum stabilen Sortieren:
Kundenname, Beträge

Die Faktura – Rechnungen inkl. Gutschriften – sollen, nachdem sie zuerst gemischt werden, nach Beträgen geordnet ausgegeben werden. Bei gleichen Beträgen sind die Kunden alphabetisch zu ordnen.

Es wird also zuerst nach Kundennamen sortiert und nachträglich nach Beträgen, sodass die alphabetische Reihenfolge bei gleichen Beträgen erhalten bleibt.

class KFaktura implements Comparable 
{
  private String kd;
  private double rBetr;
  public KFaktura (String kunde, double rchBetrag) 
{
    kd= kunde; rBetr= rchBetrag;
  }
  public String getKunde() { return kd; }
  public double getBetrag(){ return rBetr; }

compareTo():
Ordnung nach Namen

  public int compareTo(Object 
k) {
    // nicht sauber, aber einfach: kein Type-Check
    return kd.compareTo(((KFaktura)k).kd); 
  }
  public String toString() { return kd+":"+rBetr; }
  // Comparator als statische innere Klasse
  static class BetragComparator implements Comparator {

compare() in statische innere Klasse:
Ordnung nach Betrag

    public int compare(Object 
f1, Object f2) {
      // Zugriff auf private Felder (wieder ohne Type-Check)
      double r=((KFaktura)f1).rBetr-((KFaktura)f2).rBetr;
      return r>0.? 1:(r==0.? 0:-1);
    }
  }
}
class Test {
  public static void main(String[] args) {
    KFaktura[] fArr= new KFaktura[] {
    new KFaktura("ABC AG",430.0),new KFaktura("CB eG",-10.0),
    new KFaktura("Kunz KG",323.0),new KFaktura("Otto GmbH",430.0),
    new KFaktura("PR OHG",-10.0),new KFaktura("WWW KGaA",323.0) };

shuffle()
doppeltes sort()

    List l= new ArrayList(Arrays.asList(fArr));
    Collections.shuffle(l); System.out.println(l); 
    Collections.sort(l);    System.out.println(l); 
    Collections.sort(l,new KFaktura.BetragComparator());
    System.out.println(l);                                    ¬
  }
}

Erklärung: Neben der »natürlichen« können weitere Ordnungen in der Klasse als statische Comparator-Klassen definiert werden. Dies hat neben dem Vorteil der klaren Zugehörigkeit den des direkten Zugriffs.

Ausgabe, stabil sortiert!

Tabelle 14.2   Ausgabe zum Test
[PR OHG:-10.0, Otto GmbH:430.0, ABC AG:430.0, CB 
eG:-10.0, WWW KGaA:323.0, Kunz KG:323.0]
[ABC AG:430.0, CB eG:-10.0, Kunz KG:323.0, Otto 
GmbH:430.0, PR OHG:-10.0, WWW KGaA:323.0]
[CB eG:-10.0, PR OHG:-10.0, Kunz KG:323.0, WWW KGaA:323.0, 
ABC AG:430.0, Otto GmbH:430.0]

Wie in der Ausgabe zu Zeile ¨ zu sehen, bleibt bei gleichen Beträgen die alphabetische Ordnung nach Kundennamen erhalten, die durch das vorherige Sortieren erreicht wurde.


Galileo Computing

14.5.3 Generische Listen-Operationen  downtop

Die Klasse Collections enthält außer für Sortieren und Mischen noch weitere interessante Methoden, insbesondere für Listen.

Binäres Suchen mit binarySearch()

Binären Suchen

Die Methoden

   int binarySearch(List list, Object element);
   int binarySearch(List list, Object element, Comparator c)

suchen in einer aufsteigend geordneten Liste aufgrund der definierten natürlichen Ordnung bzw. der der Comparator-Instanz das angegebene Objekt.

Icon

Es gelten folgende Regeln:

Binäre Suche
in Listen

gp  Ist das Objekt in der Liste, wird der Index des Objekts in der Liste zurückgegeben.
gp  Ist ein Objekt mehrfach vorhanden, wird der Index irgendeines dieser Objekte zurückgegeben (d.h. nicht unbedingt des ersten).
gp  Ist das Objekt nicht in der Liste, wird –(insertionIndex+1) zurückgegeben, wobei mit insertionIndex der Einfügepunkt gemeint ist (an dem das Objekt stehen müsste).
gp  Ist die Liste nicht sortiert, ist das Ergebnis schlichtweg falsch.

Binäre
Performance

Die Performance liegt zwischen log2n und linear im worst case für Subklassen der AbstractSequentialList.

Beispiel

Beispiel: binäre Suche in einer ArrayList

List l= new ArrayList(Arrays.asList(
       new String[] { "should", "be", "sorted", "to", 
                      "find","this","object"}));
Collections.sort(l);
int pos= Collections.binarySearch(l,"Object");
System.out.println(pos);
l.add(pos>0?pos:-pos-1,"Object");
System.out.println(l);
System.out.println(Collections.binarySearch(l,"Object"));
System.out.println(Collections.binarySearch(l,"OBJECT", 

       String.CASE_INSENSITIVE_ORDER)); // String-Comparator
Tabelle 14.3   Ausgabe zum Code-Fragment
-1
[Object, be, find, object, should, sorted, this, to]
0
3

Kopieren, Füllen und Umkehren

Die Methoden

   void copy   (List destination, List source);
   void fill   (List list, Object element);
   void reverse(List l);

kopieren, füllen oder kehren eine Liste um.

Bei der Kopier-Operation muss die Liste destination mindestens die Länge von source haben, sonst erhält man eine IndexOutOfBoundsException. Ist sie länger, bleiben die überzähligen Elemente erhalten.

Immutable
Singleton- Kollektionen

Singleton-Kollektionen

Die Methoden

   Set  singleton    (Object element);
   List singletonList(Object element);
   Map  singletonMap (Object key, Object value);

liefern eine immutable Set, List oder Map, was ab und an nützlich ist.


Galileo Computing

14.6 Dekorieren von Kollektionen  downtop

Wie bereits durch das Decorator-Pattern (11.7) hinlänglich bekannt, fügt ein Wrapper eine zusätzliche Eigenschaft zu dem Objekt hinzu, das er einhüllt.

Dies kann allerdings auch auf ein Entfernen unerwünschter Eigenschaften hinauslaufen (siehe Immutable Wrapper).


Galileo Computing

14.6.1 Synchronisierte Wrapper  downtop

Synchronisierte Wrapper für jedes Interface

Kollektionen in ihrer Grundform sind nicht thread-sicher (siehe 9.5.2). Deshalb enthält die Klasse Collections sechs Methoden,

   Collection synchronizedCollection(Collection c);
   Set        synchronizedSet       (Set s);
   List       synchronizedList      (List list); 
   SortedSet  synchronizedSortedSet (SortedSet s);
   Map        synchronizedMap       (Map m);
   SortedMap  synchronizedSortedMap (SortedMap m);

die für jedes Interface im Framework einen thread-sicheren Wrapper zurückliefern.

Design-Schwäche des Decorator-Patterns

Design-Schwäche des Decorator-Patterns

Die erste Frage ist wohl, warum sechs und nicht zwei Methoden für Collection bzw. Map, da doch alle anderen von ihnen abgeleitet sind?

Die Anwort offenbart eine Design-Schwäche des Decorator-Patterns bzw. von Wrappern.

Dekoriert werden nur Methoden der Klasse bzw. des Interfaces, die dem Decorator a priori – zum Entwicklungs- bzw. Compile-Zeitpunkt – bekannt sind. Der Wrapper reagiert nicht auf neue Methoden der Subklassen bzw. Sub-Interfaces, denn er kennt sie gar nicht.

gp  Im Fall der Synchronisation werden also nur die Methoden des übergebenen Interfaces durch den Wrapper synchronisiert, neue Subklassen/Sub-Interface-Methoden nicht.

Art der Synchronisation

Lock auf Wrapper-Objekt in jeder einzelnen Methode

Die zurückgelieferten Kollektionen sind Instanzen von gleichnamigen synchronisierten statischen inneren Klassen von Collections, z.B.:

static class SynchronizedCollection 
implements Collection,
Serializable {}

Die Synchronisation erfolgt in jeder Methode der neuen Instanz der Synchronized<X>-Klasse, und zwar nicht auf der Original-Kollektion, sondern auf dem Wrapper-Objekt. Dies bedeutet:

Vorsicht bei Referenz auf
Original- Kollektion

gp  Die übergebenen Kollektionen können weiterhin parallel zu den neuen benutzt werden (natürlich unsynchronisiert!), sofern sie nur referenziert werden können.

Vorsicht bei
Iteratoren

gp  Iteratoren zu einem synchronisierten Wrapper sind nicht thread-sicher, d.h., jeder Iterationsschritt wird nur einzeln synchronisiert und nicht etwa die gesamte Iteration.

Icon

Dies ist bei einem Thread-Safe-Design unbedingt zu berücksichtigen.

Thread-Safe-Iterator-Template

Der zweite Punkt führt zu folgendem Idiom als Template-Code, wobei <X> für eines der sechs Interfaces steht.

    <X> syncC= Collections.synchronized<X>(c);
    ...
    // nur wenn <X> eine Map oder SortedMap ist
    // eine von den drei Kollektions-Sichten
    // Set s= syncC.keySet();  
    // Set s= syncC.values();
    // Set s= syncC.entrySet();
    ...
    synchronized(syncC) { 
      for (Iterator i= syncC.iterator(); i.hasNext();)  ¨
    //for (Iterator i= s.iterator(); i.hasNext();)      ¦
        System.out.println(i.next());
    }

Ist <X> eine der beiden Maps, muss erst eine Set s erzeugt werden und Zeile ¨ durch Zeile ¦ ersetzt werden. Wichtig ist, dass nicht auf s, sondern auf syncC synchronisiert wird.

Code-Fragment zum Template

Collection c= new HashSet();
...
Collection syncC= Collections.synchronizedCollection(c);
...
synchronized(syncC) {
  for (Iterator i= syncC.iterator(); i.hasNext();)
    operateOn(i.next());
}

Galileo Computing

14.6.2 Immutable Wrapper  downtop

Immutable Wrapper:
unveränderbare Kollektion

Alles, was zu den synchronisierten Wrappern angeführt wurde, gilt auch für die immutable Wrapper. Sie liefern eine Kollektion zurück, die nicht geändert werden kann und sind damit für alle Clients nützlich, die nur lesen dürfen.

Es gibt wieder sechs Wrapper-Methoden in Collections mit den bereits in Synchronisierte Wrapper beschriebenen Restriktionen:

   Collection unmodifiableCollection(Collection c);
   Set        unmodifiableSet       (Set s);
   List       unmodifiableList      (List list);
   SortedSet  unmodifiableSortedSet (SortedSet s);
   Map        unmodifiableMap       (Map m);
   SortedMap  unmodifiableSortedMap (SortedMap m);

Mutable Operationen lösen
Ausnahmen aus

Da immutable Wrapper konzeptionell keine Methoden aus dem Interface entfernen können, liefern alle nicht lesenden Methoden des Wrappers wie z.B. remove() eine UnsupportedOperationException.

Vorsicht bei Referenz auf
Original- Kollektion

gp  Will man den Clients wirklich nur lesenden Zugriff auf eine Kollektion gewähren, dürfen sie keine Referenz auf die Original-Kollektion erhalten.

Galileo Computing

14.7 Zusammenfassung  downtop

Das Collection-Framework bietet wegen seiner späten Geburt sehr flexible Möglichkeiten der Verwendung.

Aufgrund seiner interface-basierten Konzeption kann es einerseits für eigene Klassen-Hierarchien genutzt werden, andererseits bietet es aber auch durch ein begleitendes Klassen-Framework konkrete Implementationen für alle Interfaces.

Die Auswahl der konkreten Klasse geschieht anhand von Anforderungen, die sich neben den geforderten Eigenschaften auch an Performance-gesichtspunkten orientieren. Diese werden im Detail zusammen mit einem Entscheidungsbaum zur Auswahl einer konkreten Klasse vorgestellt.

Die einzelnen Implementationen werden besprochen und die wichtigsten Eigenschaften zusammen mit Regeln und anhand von Code-Fragmenten demonstriert.

Viele Kollektionen benötigen eine Ordnung. Anhand der Interfaces Comparable und Comparator werden natürliche und ergänzende Ordnungen einer Klasse besprochen.

Bei Sortierungen ist neben der Performance insbesondere die Stabilität wichtig, was an einem praktischen Beispiel demonstriert wird.

Generische Algorithmen in den Klassen Arrays und insbesondere Collections bilden zusammen mit den Dekoratoren für Synchronisation und unveränderbare Kollektionen den Abschluss des Kapitels.

An relevanten Stellen wird auf das unverzichtbare Kontrakt-Management hingewiesen, was leider in Form von Constrains in jede einzelne Klasse implementiert werden muss.

Die konzeptionelle Problematik, die damit verbunden ist, bildet die eigentliche Achillesferse des Collection-Frameworks, was allerdings an der Sprache Java selbst liegt.


Galileo Computing

14.8 Testfragen  toptop

Zu jeder Frage können jeweils eine oder mehrere Antworten bzw. Aussagen richtig sein.

A: Die Interface-Hierarchie des Collections-Frameworks beruht auf einfacher Interface-Vererbung.

B: Die Hierarchie des Collections-Frameworks hat nur ein Basis-Interface, von dem alle anderen als Sub-Interfaces abgeleitet werden.

C: Die Klasse Vector implementiert das Interface List.

D: Keine der mit dem Collections-Framework in Java 1.2 neu eingeführten Klassen ist synchronisiert und immutable.

E: Bei allen von Tree abgeleiteten Klassen werden die Elemente bzw. Einträge sortiert eingefügt.

F: Die Elemente bzw. Einträge in einer HashSet bzw. HashMap sind nicht sortiert.

G: Zu jedem Interface im Collections-Framework gibt es einen Iterator.

H: Zu allen Klassen und Interfaces der Collection-Hierarchie gibt es einen Iterator.

I: Bei ListIterator ist die Reihenfolge, in der die Elemente mit next() geliefert werden, nicht festgelegt.

J: Die Map hat drei Methoden, die alle ihre Einträge als Typ Set zurückliefern.

K: Die Map hat eine Methode, die ihre Einträge als Typ Collection zurückliefert.

L: Auf alle Referenzen vom Typ Collection kann gleichzeitig von mehreren Iteratoren zugegriffen werden, sofern sie nicht remove() ausführen.

M: Vor jeder Ausführung von remove() muss bei einem Iterator vorher ein next() ausgeführt werden.

N: Die Ausführung von remove() bei einem Iterator führt bei Ausführung irgendeiner Methode eines anderen Iteratoren derselben Kollektion zu einer Ausnahme.

    String s= "A";
    Collection c= new HashSet();
    c.add("A"); 
    c.add("A");                   ¨
    c.add(s);
    System.out.println(c);

A: Die Ausgabe ist: [AAA]

B: Die Ausgabe ist: [AA]

C: Die Ausgabe ist: [A]

D: Zeile ¨ erzeugt eine Ausnahme.

    Collection c= new HashSet();
    c.add("a"); c.add("b"); c.add("c");
    for (Iterator i= c.iterator(); i.hasNext();)     ¨
      System.out.print(i.next());

A: Die Ausgabe ist: [abc]

B: Die Ausgabe erfolgt nicht in der Reihenfolge des Einfügens der Elemente.

C: Die for-Schleife ist eine Endlosschleife.

D: Die Zeile ¨ erzeugt beim Kompilieren einen Fehler.

    Collection c= new HashSet();
    c.add("a"); c.add("b"); c.add("c");
    for (Iterator i= c.iterator(); i.hasNext();)
      i.remove();                                   ¨
    System.out.println(c);                         

A: Die Ausgabe ist: []

B: Die Ausgabe ist: null

C: Die for-Schleife ist eine Endlosschleife.

D: Die Zeile ¨ erzeugt beim Kompilieren einen Fehler.

E: Die Zeile ¨ erzeugt eine Ausnahme.

    Map m= new HashMap();
    m.put("1","Teil1"); m.put("2","Teil2");
    m.put("2","Teil3");                        ¨
    System.out.println(m);

A: Die Ausgabe ist: {2=Teil3, 1=Teil1}

B: Die Ausgabe ist: {2=Teil3, 2=Teil2, 1=Teil1}

C: Die Zeile ¨ erzeugt eine Ausnahme.

    Map m= new HashMap();
    m.put("1","Teil1"); m.put("2","Teil1"); 
    m.put("3","Teil1");
    Iterator i= m.values().iterator();     ¨
    while (i.hasNext()) System.out.print(i.next()+" ");

A: Die Ausgabe ist: Teil1

B: Die Ausgabe ist: Teil1 Teil1 Teil1

C: Zeile ¨ erzeugt bei der Ausführung eine Ausnahme.

    List l= new ArrayList(Arrays.asList(
                        new String[] {"A","B","C","D"}));
    Collections.shuffle(l);
    System.out.println(l);                             ¨
    Collections.sort(l, new Comparator() {
       public int compare(Object s1, Object s2) {
         return -((String)s1).compareTo((String)s2);
       }
    });
    System.out.println(l);                             ¦

A: Die Ausgabe von Zeile ¨ enthält eine Permutation der Buchstaben A, B, C und D.

B: Die Ausgabe von Zeile ¦ ist: [D, C, B, A]

C: Das Code-Fragment erzeugt beim Kompilieren einen Fehler.






1    Sieht man die potenziellen Möglichkeiten, wird die Sprachdefinition von Interfaces nach mehreren Jahren Java-Evolution langsam ärgerlich.

2    Dies bedeutet, dass compare() konsistent zu equals() sein sollte, d.h. dann Null liefert, wenn equals() den Wert true zurückgibt.

3    Dies ist nicht nur äußerst informell, sondern führt dazu, dass alle Methoden im Interface Set wegen der Kommentare noch einmal aufgeführt werden, obwohl Set überhaupt keine neuen Methoden gegenüber Collection definiert.

4    Legacy-Code, aus der Vergangenheit geerbter Code, erfreut sich durch rege Verwendung meist eines langen Lebens und wurde deshalb geschickt erweitert, um in das neue Framework zu passen. Er wird hier nicht weiter behandelt.

5    Diese Aussage gilt nicht für Legacy-Klassen wie Vector und Hashtable. synchronized ist bei einer Interface-Hierarchie auch schlecht möglich, da bei Interfaces nicht erlaubt.

6    Queue (Warteschlange): Eine Liste, bei der nur vom Kopf Elemente entfernt und am Ende der Liste hinzugefügt werden dürfen.

7    DeQueue: Eine Liste, bei der nur an beiden Seiten (Kopf und Ende) Elemente entfernt und hinzugefügt werden dürfen.

8    Sollten die einzig möglichen Einfüge/Lösch-Operationen in Queue übrigens add() bzw. remove() lauten, haben sie in Dequeue zweideutige Namen, da sie hier addLast() bzw. removeFirst() heißen müssten. Not amusing!

9    Bei n Einträgen und einer Implementation als binärer Baum.

10    Will man also Template-Code zu Instanz-Methoden nicht selbst schreiben, muss man seine Klassen von Abstract<X> ableiten (eine verdammt unschöne Lösung!).

11    Performance wird in der Big O-Notation angegeben. O(f(n)) bedeutet, dass ab einer großen Zahl n etwa f(n) Operationen notwendig sind, präziser:
g(n)= O(f(n)) Û es existiert ein Konstante k und ein n0 mit g(n) <= k f(n) für n >= n0.

12    Gleich bedeutet: compare()==0 bzw. compareTo()== 0.

  

Perl – Der Einstieg




Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken und speichern. Ansonsten unterliegt das <openbook> denselben Bestimmungen wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Veröffentlichung der Inhalte oder Teilen davon bedarf der ausdrücklichen schriftlichen Genehmigung von Galileo Press. Falls Sie Interesse daran haben sollten, die Inhalte auf Ihrer Website oder einer CD anzubieten, melden Sie sich bitte bei: stefan.krumbiegel@galileo-press.de


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de