Datenstrukturen Klassifikation von Strukturen Typen Laufzeitkomplexitäten Programmieren 2 - H.Neuendorf (81) Datenstruktur-Typen Implementierungs-Unabhängig Datenstrukturen = Grundkonzepte unterschiedlicher Semantik Technische Realisation auf Basis von Feldlisten (Arrays) oder Verketteten Referenzen Sammlung (Collection) Allg. Ansammlung von Objekten ohne Anforderungen an Elemente oder Zugriffsart Liste (List) Indizierte Reihung von Elementen mit Definition von Vorgängern und Nachfolgern Stapel (Stack) Chronologische Liste mit Zugriffsbeschränkung auf ein Ende - LIFO-Struktur Schlange (Queue) Chronologische Liste mit Zugriffsbeschränkung auf Anfang und Ende – FIFO-Struktur Menge (Set) Enthält keine Duplikate - Hinzufügen vorhandener Elemente verändert Menge nicht Keine Indizierung - speziell : Ordnung der Elemente (Sorted Set) Abbildung (Map / Key-Value-Store / Assoziation) Unidirektionale Assoziation zwischen Element-Paaren zweier Mengen Schlüssel → Wert Schlüssel bilden Set - nur Schlüssel sind eindeutig speziell : Geordnete Schlüssel (Sorted Map) Eine Map mit leeren Values ist ein Set Programmieren 2 - H.Neuendorf (82) Datenstruktur-Typen Laufzeitkomplexität Datenstruktur Einfügen Löschen Suchen Voraussetzung Feldliste (Array) O( n ) O( n ) O( n log(n) ) Sortierung Keine Sortierung Verkettete Liste O( 1 ) O( n ) O( n ) Sortierte verk. Liste O( n ) O( n ) O( n ) Sortierung Stack + Queue O( 1 ) O(1 ) O( n ) keine Set + Map O( n ) O( n ) O( n ) keine Sortierter Baum Hashtable O( log(n) ) O( log(n) ) O( 1 ) O( 1 ) O( log(n) ) O( 1 ) Einfügestelle beliebig Ausgeglichener Baum Sortierung Füllgrad < 60 % Gute Hashfunktion Zur Verwaltung von Objekten als Elemente in sortierten Strukturen müssen diese paarweise vergleichbar sein (equals + hashCode + Comparable) In diesem Fall zeigen Suchbäume die beste Performanz. Bei mangelnder Vergleichbarkeit (keine Ordnung definierbar) und ausreichend Speicher ist Hashtable die performanteste Struktur Programmieren 2 - H.Neuendorf (83) Datenstrukturen Dynamische Datenstrukturen Realisation durch Pointer = Objektreferenzen Programmieren 2 - H.Neuendorf (84) Dynamische Datenstrukturen Verketten von Objekten mittels Zeigern / Referenzen Grundlage dynamischer Datenstrukturen : Verkettete Objekte = Knoten Dynamisch : Knoten zur Laufzeit erzeugt + verkettet Datenstruktur kann dynamisch wachsen + schrumpfen Elemente können jederzeit instanziiert oder freigegeben werden Anzahl der Knoten nicht prinzipiell festgelegt ! (≠ Arrays!) Keine Speicherplatz-Verschwendung / Überläufe wie bei Arrays ! Baum : Verzweigte Struktur Liste : Lineare unverzweigte Struktur Jeder Knoten hat nur einen Nachfolger Jeder Knoten kann mehrere Nachfolger (childs) haben Jeder Knoten wird nur von einem Vorgänger referenziert (ein parent-Knoten) Objektreferenz = "Zeiger" auf Objekt Java : Objektvariable Programmieren 2 - H.Neuendorf (85) Wiederholung : Objekt-Referenzen class Mitarbeiter { public String name ; public double gehalt ; } class Verwaltung { public static void main( String[ ] args ) { (2048) m1 (####) Schmidt 5000.0 m2 (2048) m1 (2048) Mitarbeiter m1 = new Mitarbeiter( ) ; (2048) (2048) Schmidt 5000.0 m2 m1.name = "Schmidt"; m1.gehalt = 5000.0 ; Mitarbeiter m2 ; m2 = m1 ; ( null ) m1 ( null ) Schmidt 5000.0 m2 m1 = null ; m2 = null ; } Nicht mehr referenzierte Objekte werden durch Garbage Collection automatisch gelöscht } Programmieren 2 - H.Neuendorf (86) Knoten (Nodes) als Grundelemente der Verkettung Datenstruktur / -Typ Node (Knoten) mit (minimal) zwei Komponenten : val Datenkomponente / Wert, den der Knoten transportiert Kann von beliebigem Typ sein (int, Konto, Object, <T> …) next Verweis / Referenz auf Nachfolger Stets vom Typ der Knotenklasse selbst (Node) Selbstbezügliche Datenstruktur : Attribut next der Knotenklasse Node ist vom gleichen Typ wie tragende Klasse Node selbst : Jedes Element kann Referenz auf ein Nachfolger-Objekt halten ! Grundmuster der Listenbildung (5078) Realisierung mittels Referenzen (Objektvariablen) a (5078) val : 3 next : null Programmieren 2 - H.Neuendorf (87) Knoten (Nodes) als Grundelemente der Verkettung class Node { // Knotenklasse public int val ; // Datenelement public Node next ; // Objektreferenz ! Typ von val könnte auch Referenztyp sein : Mitarbeiter, Konto, Object, <T> … Erzeugen Knoten-Objekt Node a = null ; // leer public Node( int v ) { Node a = new Node( 3 ) ; val = v ; next = null ; (5078) } a } Extremfall leere Liste : Einstiegsreferenz ist null a (null) val : (5078) 3 next : null a 3 val next Programmieren 2 - H.Neuendorf (88) Verketten von Knoten via Objektreferenzen Prinzip der Listenbildung : An einen Knoten kann der nächste Knoten als Nachfolger gehängt werden Zugriff auf 1. Knoten Zugriff auf alle Nachfolge-Knoten Verketten zweier Node-Objekte : a Node a = new Node( 3 ) ; Zugriff auf alle Elemente b 3 val next 5 Node b = new Node( 5 ) ; a.next = b ; (5078) val 3 next (1024) (1024) val next a b 5 null (5078) b a 3 5 (1024) Realisierung Verknüpfung mittels Referenzen Es entsteht eine (lineare, einfach-verkettete) Liste Speziell: Ende der Liste gekennzeichnet durch Knoten mit next-Wert null ! Programmieren 2 - H.Neuendorf (89) Verketten von Knoten via Objektreferenzen class Node { // Knotenklasse public int val ; public Node next ; Selbstbezügliche Datenstruktur durch next : Zeiger zum Verketten von Node-Objekten = Referenz auf Nachfolger ! public Node( int v ) { val = v ; Inhalt = Referenz auf anderes Node-Objekt (Nachfolger) oder null (kein Nachfolger / Listenende) next = null ; } } a b 3 Liste aus zwei Knoten val next 5 Kann von vorne nach hinten durchlaufen werden : → Startknoten (head) ist Objekt a → Von a kommt man zu b mittels Attribut a.next Via Referenz auf Startknoten kann man sich durch gesamte Liste "hangeln" b a 3 5 Wenn erster Knoten verfügbar ist, so sind alle Knoten und deren Elemente verfügbar Programmieren 2 - H.Neuendorf (90) Array ↔ Verkettete Liste : Komplementäre Leistungen tail head 5 3 Kombination beider Welten in Hashtables 12 Array (Feldliste) : Verkettete Liste : Rascher direkter wahlfreier Random AccessZugriff durch Index O(1) Langsamerer Random Access-Zugriff aufgrund Durchwanderns der Liste O(n) Einfügen am Array-Ende schnell wegen direkten Zugriffs O(1) Einfügen am Ende bei tail-Pointer schnell O(1) Einfügen am Anfang bzw. bei Index i langsam, da n bzw. (n-i) Elemente verschoben werden müssen: O(n) Es wird nur ein Träger-Objekt = Array benötigt Größe fest vorgegeben - statisch Arrays sind effizienter bei häufigem nur lesenden Random Access Zugriff Einfügen relativ schnell, da keine Elemente verschoben und kopiert werden müssen. Modifikation bei jeder Größe relativ schnell. Durch doppelte Verkettung verbesserbar. Es sind stets zahlreiche Objekte involviert Nur soviel Speicher wie erforderlich belegt Verkettete Strukturen sind effizienter bei häufigem veränderndem Zugriff Programmieren 2 - H.Neuendorf (91) Datenstrukturen Dynamische Datenstrukturen Verkettete unsortierte Liste Operationen in iterativer Formulierung (Rekursive Formulierungen jedoch auch gut darstellbar … ) tail head 5 3 12 Knoten-Reihenfolge entspricht nicht Sortierreihenfolge ihrer Datenelemente Programmieren 2 - H.Neuendorf (92) Unsortierte Listen - Typische Operationen Methoden : boolean add( Object obj ) // fügt obj am Ende der Liste an und gibt true zurück boolean addFirst( Object obj ) // fügt obj am Anfang der Liste an und gibt true zurück int size( ) // Anzahl der Elemente in Liste Object get( int pos ) // liefert an Position pos gespeichertes Objekt zurück ohne Entfernen boolean contains( Object obj ) // liefert true, falls Liste spezifiziertes Objekt enthält boolean remove( Object obj ) // löscht erstes Vorkommen des spezifizierten Objekts gibt true zurück, falls Objekt gefunden und gelöscht wurde void clear( ) // leert Liste boolean isEmpty( ) // testet, ob Liste leer Anmerkung : Hinzufügen von Elementen in Liste mittels add( ) funktioniert immer. In bestimmten Datenstrukturen (zB Set) ist Hinzufügen aber nicht immer möglich, so dass Erfolg durch Rückgabewert (true / false) ausgedrückt wird. Um gleiche Semantik für möglichst viele Datenstrukturen zu erhalten, macht es Sinn, die Methoden wie angegeben zu spezifizieren. Das List-Interface / die Listen-Semantik kann auch auf Basis von Arrays implementiert werden ! Begrifflich trennen zwischen dem ADT und seiner technischen Konkretisierung ! Programmieren 2 - H.Neuendorf (93) Listen - Erwägungen Diskussion : Ist Methode get( int pos ) sinnvoll parametrisiert ? 1. Auffassung : (deskriptive Denkweise) Beim Verwalten von Objekten ist eher Methode get( Key k ) sinnvoll, die Referenz auf Objekt gemäß key = ID zurückliefert. Vor- oder Rückgabe von Indexpositionen ist eigentlich weniger sinnvoll, da internes technisches Detail der Datenstruktur - das sich jederzeit ändern kann Positionen sind nicht statisch, ändern sich - sind deshalb keine nützliche Information. 2. Gegenposition : (imperative Denkweise) Eine Liste ist definitionsgemäß eine indizierte Reihung. Somit ist Indexzugriff statthaft ! Programmieren 2 - H.Neuendorf (94) Listen - Erwägungen tail head 3 5 12 1. Doppelte Einträge Sind Einträge äquivalenter Elemente erlaubt ? Stack, Queue, Pool, UnsortedList → ja (tendenziell) SortedList, Set, Baum, Hashtable → nein 2. Abgriff von Einträgen Werden Objekte beim Zugriff typischerweise entfernt ? Stack, Queue, Pool → ja - ansonsten eher nicht 3. Rückgabewerte Welche Art von Rückgabewerten machen Sinn ? Die an der Position pos bzw. unter key gespeicherten Werte bzw. Referenzen Keine Rückgabe von Referenzen auf Node-Objekte → sind internes technisches Detail der Datenstruktur, das Verwender nicht kümmert Eher keine Rückgabe von Indexpositionen – ebenfalls technisches Detail Programmieren 2 - H.Neuendorf (95) Unsortierte Listen - Operationen Vereinfachte Darstellung tail head 5 3 mit Typ int und public Zugriffen 12 // Knotenklasse class Node { public int val ; Liste = Knoten + typische Operationen : public Node next ; Umsetzung mittels Verkettung : Klasse LinkedList , die Knoten-Objekte verwaltet public Node( int v ) { val = v ; next = null ; LinkedList - Node head, tail Node } } int val + boolean addFirst( int x ) Node next + boolean add( int x ) + boolean remove( int x ) // Listenklasse class LinkedList { + boolean contains( int x ) Es könnten beliebige Datentypen in Listen gespeichert werden. // … Attribute von Node nur deshalb public, um Implementierung zu vereinfachen. private Node head = null ; private Node tail = null ; // … } Programmieren 2 - H.Neuendorf (96) Einfügen List-Elemente → Am Listenende tail head 3 5 class LinkedList { // kein Zugriff auf Pointer! 12 private Node head = null ; private Node tail = null; Liste am Anfang leer head + tail mit null initialisiert public boolean add( int val ) { 1. Neuen Knoten p erzeugen und Wert darin speichern Node p = new Node( val ) ; 2. Mittels tail.next auf den neuen Knoten verweisen: if ( head == null ) tail.next = p ; { head = p ; } Dadurch ist dieser nun ans Ende der Liste gehängt else 3. Variable tail auf den neuen Knoten setzen, { tail.next = p ; } da dieser nun das neue Ende der Liste darstellt : tail = p ; return true ; tail = p ; } Fälle : a) Liste noch leer } (head == null) : neuer Knoten p ist einziger Knoten head = tail = p b) Liste nicht mehr leer (head != null) : neuer Knoten p ans Ende der Liste tail.next = p Initialzustand head tail add12); head (p) tail 12 Programmieren 2 - H.Neuendorf (97) Einfügen → Am Listenende Initialzustand head tail (p) 12 tail private Node tail = null ; public boolean add( int val ) { Dann : Node p = new Node(val) ; Neuen tail erzeugen, indem tail nun auf neuen Knoten p zeigt : tail = p ; (p) 3 12 if ( head == null ) { head = p ; } else add(3); head private Node head = null ; Alten tail verwenden, um auf neuen Knoten zu verweisen : tail.next = p ; add(12); head class LinkedList { Zuerst : { tail.next = p ; } tail = p tail = p ; return true ; tail.next = p } } add(45); head (p) 12 3 45 tail.next = p Wachstumsrichtung der Liste tail = p Objektvariable p ist lokale Variable der Methode add : Lebt nur während Ausführung der Methode - im Gegensatz zu globalen Variablen head + tail ! Programmieren 2 - H.Neuendorf (98) Einfügen → Am Listenanfang tail head 5 3 class LinkedList { private Node head = null ; 12 private Node tail = null ; public boolean addFirst( int val ) { 1. Neuen Knoten p erzeugen und Wert darin speichern Node p = new Node( val ) ; 2. Der neue Knoten hat als Nachfolger den bisherigen if( tail == null ) tail = p; p.next = head ; Listenanfang (head) head = p ; p.next = head ; return true ; 3. Der neue Kopf der Liste ist nun der neue Knoten : Knoten p wird zum neuen Listenanfang head = p ; Keine Unterscheidungen hinsichtlich Listenlänge (leer / nicht leer) erforderlich Bei unsortierten Listen ist es egal, wo man ein neues Element einfügt … } } Vorteil gegenüber Arrays : Einfügen von Elementen am Anfang erfordert kein Verschieben von Elementen ! Initialzustand head addFirst(12); head 12 Programmieren 2 - H.Neuendorf (99) Einfügen → Am Listenanfang class LinkedList { Initialzustand private Node head = null ; head tail private Node tail = null ; addFirst(12); head = p (p) 12 public boolean addFirst( int val ) { tail = p Node p = new Node( val ) ; if( tail == null ) tail = p; p.next = head ; head = p ; p.next = head = null head = p return true ; } addFirst(3); } (p) 3 12 Zuerst : Next-Zeiger des neuen Knotens p auf alten head zeigen lassen : p.next = head ; p.next = head head = p addFirst(45); Wachstumsrichtung der Liste (p) 45 Dann : 3 p.next = head 12 Neuen head erzeugen, indem head nun auf neuen Knoten p zeigt : head = p ; Programmieren 2 - H.Neuendorf (100) Suchen von Elementen tail head 3 5 12 class LinkedList { private Node head = null ; Suchen private Node tail = null ; 1. An den Anfang der Liste gehen : public boolean addFirst( int val ) { … } Hilfszeiger p = head ; 2. Knoten der Liste durchlaufen + prüfen, ob gesuchter Wert enthalten : public boolean contains( int val ) { p.val == val Node p = head ; Wenn nicht, zum nächsten Knoten übergehen : while( p != null && p.val != val ) { p = p.next ; p = p.next ; 3. Wenn p == null ist Listenende erreicht } return p != null ; Abbruchkriterium : Knoten mit next == null } Wert nicht gefunden } Umsetzung while-Schleife → Läuft solange gesuchter Wert noch nicht gefunden evtl. bis ans Ende der Liste ! Erstes vorkommendes Element wird gefunden – Mehrfacheinträge können nicht erfasst werden … Bei Objekten mit equals( ) arbeiten Programmieren 2 - H.Neuendorf (101) Löschen von Elementen Problem : head Klasse Node kennt nur Nachfolger 3 5 12 Beim Durchlauf zweiten Hilfszeiger prev mitführen, der auf Knoten vor p zeigt ! p head 3 5 Wie bekommt man Vorgängerknoten ? 12 "Umbiegen" / Übernrücken : prev.next = p.next ; prev p Sonderfall : Löschen 1. Zu löschenden Knoten suchen Hilfszeiger p darauf setzen 2. Next-Zeiger des Vorgängerknotens auf Nachfolger p ist erstes Element der Liste → keine Vorgänger ! (prev == null) Neuer Listenanfang ist Nachfolger von p Liste beginnt nun mit Nachfolger von p des zu löschenden Knotens setzen ("überbrücken") "Umbiegen" : head = p.next ; Zu löschender Knoten nur noch von p referenziert Zeiger p ist lokale Variable der Methode GC entsorgt nicht mehr referenzierten Knoten nach Methodenende ! Programmieren 2 - H.Neuendorf (102) Löschen von Elementen class LinkedList { private Node head ; private Node tail ; public boolean remove( int val ) { Node p = head ; Node prev = null ; // Hilfszeiger while( p != null && p.val != val ) { prev = p ; p = p.next ; } if ( p != null ) { if ( p == head ) { head = p.next ; } else { prev.next = p.next ; } if( p == tail ) { tail = prev ; } return true ; } else return false ; } } Abfrage darf nicht lauten : if ( p.val == val ) da p auch null sein könnte, wenn Element nicht in Liste enthalten Weiterführen von prev und p p == head : Lösch-Element am List-Anfang p == tail : Tail-Pointer umsetzen ! Wenn p == null ist zu löschendes Element nicht in Liste enthalten false zurückgeben ! Programmieren 2 - H.Neuendorf (103) Ausgabe Listenelemente → class LinkedList { private Node head ; private Node tail ; public String toString( ) { StringBuffer sb = new StringBuffer( ) ; toString( ) Ausgabe Liste : Werte werden in Stringbuffer geschrieben und am Ende in String umgewandelt. String wird zurückgegeben. Liste von Anfang bis Ende durchlaufen. sb.append( "( " ) ; Node p = head ; if( p == null ) return "Leer" ; Alternative Schleifen-Formulierung : for( Node p = head; p!=null; p=p.next ) { … } while( p != null ) { sb.append( p.val ) ; sb.append( " " ) ; p = p.next ; Weiterführen von p } sb.append( " )" ) ; return sb.toString( ) ; } } Übung : Wie lauten Methoden void clear( ) und boolean isEmpty( ) ? Programmieren 2 - H.Neuendorf (104) Doppelt verkettete Liste Erlaubt Traversieren in zwei Richtungen Node class DLinkedList { prev private Node head ; val private Node tail ; next public boolean add( int val ) { Node p = new Node( val ) ; class Node { if( head == null ){ tail = p ; // angespasst an DLL public int val ; head = p ; } else{ public Node next ; // Nachfolger-Referenz public Node prev ; // Vorgänger-Referenz public Node( int v ) { tail.next = p ; val = v ; p.prev = tail ; next = null ; } tail = p ; } } return true ; prev = null ; tail head } // weitere Methoden : Übung … } tail-Pointer macht dadurch erst Sinn ! warum ? Programmieren 2 - H.Neuendorf (105) Verkettete Liste - Rekursive Formulierungen class LinkedList { private Node head = null ; public boolean addFirst( int val ) { /*… */ } public int sizeRekursiv( ) { return sizeRek( head ) ; } private int sizeRek( Node p ) { if( p == null ) return 0 ; return 1 + sizeRek( p.next ) ; Typischer Aufbau : Public Zugriffsmethode ruft intern mit privaten Argumenten die private eigentliche rekursive Implementierung Bsp : head ist kein public Attribut ! Rekursionsschritt : Statt while-Iteration erfolgt rekursiver Aufruf mit p.next } public boolean containsRekursiv( int val ) { return containsRek( head, val ) ; } private boolean containsRek( Node p, int val ) { if( p == null ) return false ; Alle iterativen List-Methoden lassen sich problemlos rekursiv darstellen - denn eine Liste ist eine rekursive Struktur ! Alternative Definition der tail-Position erleichtert rekursive Formulierungen : tail = direkt hinter head if( p.val == val ) return true ; return containsRek( p.next, val ) ; } } Übungen : Rekursive Ausgabe aller Listelemente Rekursive Erstellung einer invertierten Liste Programmieren 2 - H.Neuendorf (106) Datenstrukturen Dynamische Datenstrukturen Verkettete sortierte Liste Programmieren 2 - H.Neuendorf (107) Sortierte Liste tail head 5 7 Knoten-Reihenfolge entspricht Sortierreihenfolge ihrer Elemente 12 Suchen Vorteil : class SortedList { Sortierung erlaubt sortierte Ausgabe der Werte ohne extra Sortieroperation private Node head ; private Node tail ; Schnelleres Aufsuchen von Elementen - Liste muss nicht bis Ende durchsucht werden : // public boolean contains( int val ) { Abbrechen der Suche, wenn man auf Element stößt, das "größer" ist als gesuchtes Element Node p = head ; while( p! = null && p.val < val ) p = p.next ; Aber : if ( p != null && p.val == val ) return true ; Für effizientes Suchen in großen Datenbeständen sind Binäre Suchbäume geeigneter Lineare Liste zum Durchsuchen nicht gut geeignet, da kein wahlfreier Zugriff Binary Search nicht effizient darstellbar ! else return false ; } } Programmieren 2 - H.Neuendorf (108) Lineares Suchen in verketteter Liste class LinkedList { private Node head ; class SortedList { private Node head ; private Node tail ; private Node tail ; public boolean contains( int val ) { Node p = head ; public boolean contains( int val ) { Node p = head ; while( p! = null && p.val < val ) { p = p.next ; } while( p != null && p.val != val ) { p = p.next ; } if( p != null ) return true ; if ( p != null && p.val == val ) return true ; else return false ; else return false ; } } Zeitbedarf : neg. Fall → n Vergleiche pos. Fall → ∅ n /2 Vergleiche Komplexität O(n) Wächst linear mit Zahl der Elemente } } Zeitbedarf : verändert sich kaum neg. Fall → ∅ n / 2 Vergleiche kann früher abgebrochen werden pos. Fall → ∅ n /2 Vergleiche Komplexität bleibt O(n) Wächst linear mit Zahl der Elemente Programmieren 2 - H.Neuendorf (109) Sortierte Liste - Einfügen class SortedList { private Node head ; tail head 7 5 prev q public boolean insert( int val ) { 12 Node p = head; Node prev = null; Node q = new Node( val ) ; p if( p==null ) { // leere Liste head = q ; Neues Element an richtiger Position einfügen - nicht einfach am Anfang / Ende wie bei unsortierter Liste prev = p ; // Keine doppelten Einträge : if( p!=null && p.val==val ) return false ; q.next = p ; if( p==head ) head = q ; Neuen Knoten q einfügen zwischen p und prev : else q.next = p ; q wird zum neuen Kopf // am Anfang prev.next = q ; if( prev==tail ) tail = q ; Sonderfälle : p == head p = p.next ; } Zeiger p auf Knoten setzen, dessen Wert größer ist als einzufügendes Element + Zeiger prev auf Vorgänger von p. q wird am Ende eingehängt return true ; while( p!=null && p.val<val ) { Zum Einfügen die Einfügeposition suchen : p == null tail = q ; } Erfordert nur Zeigeroperationen - kein Verschieben von Elementen nötig. Doppelt-Eintragungen unterbunden ! prev.next = q ; und private Node tail ; // am Ende return true ; } } Programmieren 2 - H.Neuendorf (110) Sortierte Liste - Einfügen insert( 6 ) head 7 5 prev Lokale Variablen : tail p 6 q, p, prev Globale Variablen : head tail 12 tail head 7 5 q 1. Node q = new Node(6) ; prev q 12 p 6 q.next = p ; 2. 3. head tail prev.next = q ; 7 5 prev 6 12 p q Programmieren 2 - H.Neuendorf (111) Sortierte Liste - Einfügen Sonderfälle Sonderfälle : public boolean insert( int val ) { Node p = head; Node prev = null; Node q = new Node(val); 1. p ist der erste Knoten der Liste : p == head if( p==null ) { // leere Liste Es gibt keinen Vorgänger prev head = q ; tail = q ; return true ; q wird neuer Listenkopf: head = q ; } while( p!=null && p.val<val ) { 2. Leere Liste : head = null p == null prev = p ; q wird neuer Listenkopf : head = q ; p = p.next ; } q hat noch keinen Nachfolger : q.next = null if( p!=null && p.val==val ) return false; q.next = p ; 3. q muß ans Ende der Liste : if( p==head ) head = q ; Schleife läuft bis ans Ende der Liste p hat dort den Wert null else prev zeigt auf den letzten Knoten if( prev==tail ) tail = q; q kommt ans Ende : return true; prev.next = q ; prev.next = q ; } q.next = p ; // = null } Programmieren 2 - H.Neuendorf (112) Sonderfall Einfügen am Anfang p == head tail insert( 4 ) head 7 5 prev p 12 Lokale Variablen : q, p, prev Globale Variablen : head tail 4 tail head q 7 5 12 1. Node q = new Node(4) ; prev p 4 2. q.next = p ; q tail 7 5 prev p 12 4 3. head q if ( p == head ) head = q ; Programmieren 2 - H.Neuendorf (113) Sonderfall Einfügen am Ende p == null tail insert( 14 ) head 7 5 Lokale Variablen: q, p, prev 12 Globale Variablen: head tail p 1. Node q = new Node(14) ; prev 14 q tail head 7 5 12 p prev head 5 7 3. prev.next = q ; 2. q.next = p ; 14 q 12 p prev tail 14 // tail = q ; q Programmieren 2 - H.Neuendorf (114) Datenstrukturen Dynamische Datenstrukturen Design-Prinzipien & Varianten Generische Formulierung Programmieren 2 - H.Neuendorf (115) Strategy-Interfaces Strategy- Methoden : Aktionsobjekte als Parameter interface Action { // Strategy-IF public abstract Object act( Node h ) ; } class LinkedList { // Liste ganzer Zahlen private Node head = null ; private Node tail = null ; public boolean add( int val ) { // … } public Object work( Action obj ) { return obj.act( head ) ; } } Ohne LinkedList zu verändern kann man zusätzliche Funktionalität injizieren … Dadurch besitzt Klasse Schnittstelle, an die beliebige Funktionalität PlugIn-artig übergeben werden kann class Sum implements Action { public Object act( Node h ) { Node p = h ; int summe = 0 ; while( p != null ) { summe = summe + p.val ; p = p.next ; } return new Integer( summe ) ; } } class Output implements Action { public Object act( Node h ) { Node p = h ; String str = null ; while( p != null ) { str = str + p.val + " " ; p = p.next ; } return str ; } } Programmieren 2 - H.Neuendorf (116) Strategy-Interfaces class UList { public static void main( String[ ] args ) { LinkedList MyList = new LinkedList( ) ; MyList.add( 17 ) ; MyList.add( 3 ) ; MyList.add( 6 ) ; Sum su = new Sum( ) ; Integer summe = (Intreger) MyList.work( su ) ; Output out = new Output( ) ; String output = (String) MyList.work( out ) ; } } class Sum implements Action { public Object act( Node h ) { Node p = h ; int summe = 0 ; while( p != null ) { summe = summe + p.val ; p = p.next ; } return new Integer( summe ) ; } } class Output implements Action { public Object act( Node h ) { Node p = h ; String str = null ; while( p != null ) { str = str + p.val + " " ; p = p.next ; } return str ; } } Verschiedene Aktionen ("Strategien") erfordern verschiedene Aktionsobjekte mit Methode korrekter Signatur, die über "PlugIn"Methode der Ziel-Klasse (work() von LinkedList) aufgerufen wird. Implementierende Klassen sind typischerweise zustandslos (ohne Attribute), besitzen nur entsprechende Aktionsmethode. Referenzen auf Aktionsobjekte als Ersatz für in Java nicht vorhandene Funktions- Pointer Programmieren 2 - H.Neuendorf (117) Listen - Strukturvarianten Spezifikation des Abstrakten Datentyps List : public interface List { // interface List<T> public boolean add( Object obj ) ; public boolean addFirst( Object obj ) ; public int size( ) ; public Object get( int pos ) ; public boolean contains( Object obj ) ; public boolean remove( Object obj ) ; public void clear( ) ; public boolean isEmpty( ) ; } Eleganter : Definition von Node als private Klasse innerhalb von LinkedList Konzept der Inneren Klasse Somit Node von außen nicht sichtbar ! Implementierung von List : class LinkedList implements List { private Node head ; public boolean add( Object obj ) { /* …*/ } // … } class Node { public Node( Object obj ) { val = obj ; next = null ; } public Object val ; public Node next ; } class LinkedList implements List { private Node head ; public boolean add( Object obj ) { /* …*/ } // … private class Node { Node( Object obj ) { val = obj ; next = null ; } Object val ; Node next ; } } Programmieren 2 - H.Neuendorf (118) LinkedList via Object class Node { Elemente vom Typ Object : Kann beliebige Objekte aufnehmen public Object val ; Speichern einfacher Datentypen durch Hüllklassen public Node next ; public Node( Object obj ) { val = obj ; // … Fortsetzung : next = null ; public boolean contains( Object obj ) { } Node p = head ; } for( Node p = head; p!=null; p=p.next) { if( p.equals( obj ) ) return true ; class LinkedList { private Node head = null ; } private Node tail = null ; return false ; public boolean addFirst( Object obj ) { if( obj==null ) return false ; } } Node p = new Node( obj ) ; if ( head == null ) { head = p ; } else { tail.next = p ; } tail = p ; return true ; } // … Keine null-Werte als Datenelemente akzeptiert ! List MyList = new LinkedList( ) ; MyList.addFirst( "Test" ) ; MyList.addFirst( new Long(27) ) ; Programmieren 2 - H.Neuendorf (119) Speichern spezieller Objekte // Nutzdatenklasse class Mitarbeiter { private String name ; public String getName( ) { return name ; } public Mitarbeiter( String n ) { name = n ; } } // Knotenklasse = Behälter für Mitarbeiter-Objekte class Node { private Mitarbeiter val ; public Node next ; public Node( Mitarbeiter obj ) { val = obj ; next = null ; } } class LinkedList { private Node head, tail ; public boolean addFirst( Mitarbeiter obj ) { if( obj==null ) return false ; Node p = new Node( obj ); if( tail == null ) tail = p; p.next = head ; head = p ; Keine null-Werte return true ; als Datenelemente! } } In Node Elemente vom Typ Mitarbeiter speicherbar // Alternativ : Verkettungsfähige Mitarbeiterklasse // Verfügt über passendes next-Attribut class Mitarbeiter { private String name ; public Mitarbeiter next ; public String getName( ) { return name ; } public Mitarbeiter( String n ){ name = n ; } } class Linkedist { private Mitarbeiter head, tail ; public boolean addFirst( Mitarbeiter obj ) { if( obj == null ) return false ; Mitarbeiter p = obj ; if( tail == null ) tail = p ; p.next = head ; head = p ; return true ; } } Wie sehen die entstehenden Objekt-Strukturen aus ? Programmieren 2 - H.Neuendorf (120) Speichern von Objekten in Listen - Referenzbild a) Mit separater Knotenklasse Node : Müller Haller Gross Mitarbeiter head List val next Node Jede Klasse kann durch entsprechendes next Attribut verkettungsfähig gemacht werden Vorteil : b) Mit verkettungsfähiger Mitarbeiter-Klasse : Keine separate Knoten-Klasse nötig Nachteil : Klasse muss angepasst werden head Müller next Mitarbeiter Haller Gross List Beim Verwalten von Objekten ist Methode get( ) sinnvoll, die Referenz auf das Objekt gemäß Schlüsselwert (ID) = key zurückliefert Programmieren 2 - H.Neuendorf (121) Generische Liste mittels Generics class Node<T> { Typ-Variable <T> public T val ; public Node<T> next ; val = obj ; Man vermeidet den total generischen, völlig unspezifizierten Typ Object und gibt Verwender trotzdem Möglichkeit zur flexiblen Typ-Vorgabe mit Typsicherheit : next = null ; Compiler prüft bei konkreter Verwendung auf korrekten Typ ! public Node( T obj ) { } } class LinkedList<T> { class ListUser { public static void main( String[] args) { private Node<T> head = null ; private Node<T> tail = null ; LinkedList<String> MyList = new LinkedList<String>( ) ; public boolean addFirst( T obj ) { MyList.add( "dies geht" ) ; if( obj == null ) return false ; Node<T> p = new Node<T>( obj ) ; MyList.add( "und auch" ) ; if ( head == null ) { head = p ; } MyList.add( new Long(27) ) ; // Compilerfehler !! else { tail.next = p ; } tail = p ; return true ; } } } } Programmieren 2 - H.Neuendorf (122) Datenstrukturen Dynamische Datenstrukturen Stack und Queue auf Basis verketteter Listen Verschiedene Varianten … Listen sind allgemeinere Datenstrukturen als Stack und Queue Somit können die spezielleren Datenstrukturen Stack und Queue durch Listen dargestellt werden Programmieren 2 - H.Neuendorf (123) Stack als verkettete Liste Nachteil Array-Implementierung : Fest vorgegebene Größe kann überlaufen ! Vorteil List-Implementierung : Beliebig viele Elemente hinzufügbar - bis Heap erschöpft Darstellung als verkettete Liste : Werte könnten auch Referenzen sein ... Zeiger top zeigt auf obersten Knoten next-Zeiger liefern darunterliegende Elemente Operation push( ) : Einfügen eines neuen Knotens am Anfang der Liste Operation pop( ) : Entfernen eines Knotens vom Anfang der Liste top - Node top + int pop( ) val next Stack + void push( int x ) Node Node int val Node next Schnittstelle der Stack-Klasse ist die selbe wie bei Realisation mit Array Außensicht + Verwendbarkeit unverändert. (Node als private innere Klasse!) Interne Umsetzung ist völlig anders - ohne dass Verwender dies wahrnehmen Konstanz der Methodenschnittstellen + ihrer wahrnehmbaren Funktionalität ↔ Verbergen der internen Abläufe + Datenstrukturen Information Hiding ! Abstrakte Datenstrukturen Programmieren 2 - H.Neuendorf (124) Stack als verkettete Liste class Stack { private Node top = null ; Einfügen push( ) : 1. Neuen Knoten p mit Wert erzeugen public void push( int x ) { 2. Nachfolger des neuen Knotens ist bisheriger top : Zugriff nur über öffentliche Schnittstelle! Kein direkter Zugriff auf top-Pointer ! Node p = new Node( x ) ; p.next = top ; p.next = top ; 3. Neuer top ist nun der neue Knoten p : top = p ; } top = p ; public int pop( ) { if ( top == null ) { /* Exception */ } Entfernen pop( ) : else { 1. Wenn Stack leer ist ( top == null ) Exception werfen Node p = top ; 2. Wenn Stack nicht leer top-Element entfernen : top = top.next ; (p) top return p.val ; 2.1. Lokalen Hilfszeiger p auf top setzen: Node p = top ; } 2.1. Nachfolger des bisherigen top-Elements wird top : } top = top.next ; public boolean isEmpty( ) { 2.2. Wert des bisherigen top-Elements zurückgeben return top == null ; return p.val ; 3. Altes top-Objekt nun nicht mehr referenziert top } } von GC nach Methodenende entsorgt Programmieren 2 - H.Neuendorf (125) Stack als verkettete Liste : top 1. 5 push( ) push( 5 ) class Stack { private Node top = null ; Node p = new Node( 5 ) ; public void push( int x ) { p.next = top; // = null ! p Node p = new Node( x ) ; p.next = top ; top top = p ; 2. 5 } top = p ; p // … } 8 p top push( 8 ) 1. Node p = new Node( 8 ) ; p.next = top; top 8 2. top = p ; p 5 5 Programmieren 2 - H.Neuendorf (126) Stack als verkettete Liste : top pop( ) pop( ) 3 p 8 class Stack { 1. Node p = top ; private Node top = null ; … public int pop( ) { if ( top == null ) { /* Exception */ } else { Node p = top ; top = top.next ; return p.val ; } } // … return p.val ; 5 Methodenende : Garbage Collection 3 } top (p) 8 5 Referenz p ist lokale Variable der Methode pop( ) 2. top = top.next ; Nach Methodenende verschwindet p und somit die letzte Referenz auf Element Garbage Collection entfernt Element ! Programmieren 2 - H.Neuendorf (127) Queue als verkettete Liste Nachteil Array-Implementierung : Fest vorgegebene Größe Vorteil List-Implementierung : Beliebig viele Elemente hinzufügbar - bis Heap erschöpft Darstellung als verkettete Liste : kann überlaufen head tail Zeiger head zeigt auf Anfang der Schlange Zeiger tail zeigt auf Ende der Schlange Verkettung läuft von Anfang Richtung Ende Operation put( ) : Anhängen neues Element ans Ende Operation get( ) : Entfernen des ältesten Elements vom Anfang Sonderfall : Schlange ist leer es kann nichts entnommen werden Queue - Node head, tail + void put ( int x ) + int get ( ) Node int val Node next tail-Pointer ermöglicht bei put( )Operation direkten Zugriff auf das letzte Element … Programmieren 2 - H.Neuendorf (128) Queue als verkettete Liste class Queue { private Node head = null ; private Node tail = null ; Einfügen put( ) : 1. Neuen einzufügenden Knoten p erzeugen 2. Leere Schlange (head == p) : public void put( int x ) { Node p = new Node( x ) ; if ( head == null ) head = p ; else tail.next = p ; tail = p ; } public int get( ) { if ( head == null ) { /* Exception */ } else { Node p = head ; head = head.next ; if ( head == null ) tail = null ; return p.val ; } } neuer Knoten p wird neuer head : head = p ; Nichtleere Schlange - neuen Knoten p ans Ende : tail.next = p ; 3. Nach Anhängen ist neuer Knoten p das neue Ende : tail = p ; Entfernen get( ) : 1. Wenn Schlange leer ( head == null ) Exception werfen 2. Hilfszeiger p auf head setzen - soll entfernt werden : Node p = head ; Rückgabe von p.val 3. Neuer Kopf ist der Nachfolger des bisherigen Kopfes: head = head.next ; 4. Wenn letztes Element der Schlange entfernt wird, dann auch tail = null ; head head + tail nun beide null public boolean isEmpty( ) { tail return head == null ; } } Programmieren 2 - H.Neuendorf (129) head put( 9 ) Queue als verkettete Liste tail class Queue { private Node head = null ; private Node tail = null ; 1. Node p = new Node( 9 ) ; 9 public void put( int x ) { p head Node p = new Node( x ) ; tail if ( head == null) head = p ; else tail.next = p ; tail = p ; } 2. tail.next = p ; // ... 9 } p Sonderfall : Leere Queue head - head ist noch null - head zeigt nach Erzeugen von tail 3. tail = p ; 9 (p) neuem Knoten p auf diesen head = p ; Methodenende: Lokale Variable p gelöscht Programmieren 2 - H.Neuendorf (130) get( ) head Queue als verkettete Liste tail class Queue { private Node head = null ; private Node tail = null ; 1. Node p = head ; p head 2. p GC ! (p) public int get( ) { if ( head == null ) { /* Exception */ } else { Node p = head ; head = head.next ; if ( head == null ) tail = null; return p.val ; } } // ... tail head = head.next ; … return p.val ; head tail } Methodenende : Lokale Variable p gelöscht Altes head-Objekt nicht mehr referenziert Garbage Collection ! Programmieren 2 - H.Neuendorf (131) head tail Queue als verkettete Liste get( ) Sonderfall : class Queue { Queue enthält nur noch ein Element p head private Node head = null ; private Node tail = null ; 1. Node p = head ; tail public int get( ) { if ( head == null ) { /* Exception */ } else { Node p = head ; head = head.next ; if ( head == null ) tail = null ; return p.val ; } } // … 2. head = head.next ; // = null ! … return p.val ; p head GC ! tail 3. if( head == null ) tail = null ; Methodenende : Lokale Variable p gelöscht Objekt nicht mehr referenziert (p) } Garbage Collection ! Es bleibt leere Queue zurück - head + tail sind wieder null ! Programmieren 2 - H.Neuendorf (132) Generischer Stack + Knotenklasse class Node<E> { private Node<E> next ; private E element ; Typ der generischen Knotenklasse Node ist nicht mehr einfach "Node", sondern nun : public Node( E element ) { this.element = element ; Node<E> // sprich : "Node of E" } public void setNext( Node<E> next ) { this.next = next ; } public Node<E> getNext( ) { return next ; Diese Typbezeichnung muss somit durchgängig verwendet werden – wie zB beim Attribut next und bei der Methode setNext( ) ! Ansonsten raw-type Warnung vom Compiler } public E getElement( ) { return element ; } public void setElement( E element) { this.element = element ; } } Namenskonvention - Typvariablen tragen kurze Namen, meist nur ein Buchstabe : E für Datenelement K für ein Schlüsselobjekt V für ein Wertobjekt T für einen allgemeinen Typ … Programmieren 2 - H.Neuendorf (133) Generischer Stack + Knotenklasse class Stack<E> { Auch Klasse Stack ist generisch. private Node<E> top ; Sie gibt den bei der Type-Invocation für E zugewiesenen Typ an die Objekte der generischen Klasse Node weiter ! public void push( E item ) { Node<E> p = new Node<E>( item ) ; p.setNext( top ) ; top = p ; } public E pop( ) { class TestStack { public static void main( String[ ] args) { if( top == null ) throw new RuntimeException( "Empty!" ) ; Node<E> p = top ; Stack<String> s = new Stack<String>( ) ; top = top.getNext(); s.push( "Hallo" ) ; return p.getElement() ; s.push( "Ihr" ) ; } String test = s.pop( ) ; public boolean isEmpty( ){ // ... Stack<Double> d = new Stack<Double>( ) ; return top == null ; // ... } } } } Programmieren 2 - H.Neuendorf (134) Generischer Stack + Innere Knotenklasse class Stack<E> { private Node top ; // ... Fortsetzung private class Node { // inner class private Node next ; public void push( E item ) { private E element ; Node p = new Node( item ) ; public Node( E element ) { p.setNext( top ) ; this.element = element ; top = p ; } } public void setNext( Node next ) { public E pop( ) { this.next = next ; if( top == null ) throw new RuntimeException( "Empty!" ) ; } Node p = top ; public Node getNext( ) { return next ; top = top.getNext( ) ; } - NodeKlasse übernimmt Typ ! return p.getElement( ) ; Die // okinnere Node istEnun nicht mehr generisch, sondern } direkt deneigenen Typ ETypder Funktioniertübernimmt nicht, wenn innere Klasse // .... Parameter E hätte, der äußeren verdeckt: äußeren KlasseTyp-Parameter Stack Die innere Klasse Node übernimmt den Typ E. Sie selbst ist nun nicht mehr generisch, sondern hängt von dem Typ ab, der ihr von der äußeren Klasse Stack vorgegeben wird. Typ-Variablen einer äußeren Klasse sind in einer inneren Klasse sichtbar + verwendbar. public E getElement( ) { return element ; } public void setElement( E element) { this.element = element ; } } } // Ende der äußeren Klasse Stack Programmieren 2 - H.Neuendorf (135) Stack mittels interner LinkedList class Stack<E> { private List<E> list ; public Stack( ) { list = new LinkedList<E>( ) ; } public void push( E item ) { list.addFirst( E ) ; } public E pop( ) { if( list.isEmpty( ) ) throw new RuntimeException( "Empty!" ) ; E element = list.get( 0 ) ; list.remove( element ) ; return element ; Die innere Klasse Node ist nun nicht mehr generisch, sondern public E peek( ) { übernimmt direkt den Typ E der äußeren Klasse Stack if( list.isEmpty( ) ) throw new RuntimeException( "Empty!" ) ; Klasse Stack kümmert sich nicht selbst um den Aufbau einer verketteten Liste … … sondern verwendet ein Attribut vom Typ List … … mit desen Methoden die Stack-Semantik (ADT) umgesetzt wird. Verwender merkt keinen Unterschied ! } return list.get( 0 ) : } public boolean empty( ) { return list.isEmpty( ) ; } Stack<String> s = new Stack<String>( ) ; s.push( "Hallo" ) ; s.push( "Ihr" ) ; String test = s.pop( ) ; // ... } Programmieren 2 - H.Neuendorf (136) Datenabstraktion ADT Grundprinzip = Information Hiding ↔ Ziel = Reduktion von Komplexität : Implementierungsdetails verbergen → Zugriff auf komplexe Strukturen nur über öffentliche Methoden = einfache Schnittstelle ohne technische interne Implementierungsdetails offenzulegen ! Vorteil: Interne Implementierungsdetails können jederzeit geändert werden … … solange Schnittstelle konstant bleibt Datenabstraktion : "Wie" der Implementierung wird versteckt Nur das "Was" der Datenhaltung exponiert in öffentlicher Schnittstelle Verwender / Clients werden nicht invalidiert Nicht Implementierung ist vorgegeben, sondern die semantischen Operationen auf Datenelementen. Somit kann Implementierung jederzeit geändert und neuen Erfordernissen angepasst werden – ohne dass verwendende Programme verändert werden müssten. Stack-Klasse mit konventioneller Schnittstelle pop( ) push( … ) Implementierung via Array oder Verketteter Liste Implementierungsdetail - von Außen nicht sichtbar Austauschbar ! Programmieren 2 - H.Neuendorf (137) Datenstrukturen Java-Standard-Interfaces im Datenstruktur-Umfeld Ordnungs-Pattern : Comparable & Comparator Iterator-Pattern : Iterable & Iterator Einige Inhalte + Abbildungen wurdem dem Skript meines Kollegen Prof. Dr. Deck entnommen Programmieren 2 - H.Neuendorf (138) Ordnungspattern : IF java.lang.Comparable java.util.Comparator Vergleichsmethoden : compareTo( ) und compare( ) implementieren totale Ordnungsrelation mit gleicher Semantik a.compareTo( b ) compare( a, b ) <0 =0 >0 bedeutet: a<b a gleichgroß b a>b Absolutwerte <0 oder >0 egal Keine weitere Semantik einbauen ! Totale Ordnung : Alle Elemente einer Menge können aufgrund einer Größer-GleichRelation linear angeordnet werden Allerdings unterschiedliche Signatur der Vergleichsmethoden : public interface Comparable { public interface Comparator { public int compare( Object o1, Object o2 ) ; public int compareTo( Object o ) ; } Implementierung von Comparable bei "natürlicher" Ordnung als Teil der Klassenlogik public boolean equals( Object o ) ; } Implementierung von Comparator für weitere Ordnungskriterien z.B: durch separate Comparator-Klassen : Definieren weitere Kriterien … Davon kann es beliebig viele geben ! Programmieren 2 - H.Neuendorf (139) Ornungspattern : IF java.lang.Comparable java.util.Comparator class Mitarbeiter implements Comparable { public int persNr ; // eindeutige Ordnung public String name ; public int alter ; public Mitarbeiter( int nr, String n ) { /* … */ } Implementierung von Comparable bei "natürlicher" Ordnung als Teil der Klassenlogik - hier : persNr als Haupt-Kriterium public int compareTo( Object o ) { Mitarbeiter m = (Mitarbeiter) o ; int cmp = this.persNr – m.persNr ; Vorsicht : if( cmp == 0 ) return 0 ; Differenz zweier negativer Werte kann Überlauf bewirken if( cmp > 0 ) return +1; else return -1 ; // alternativ: // return this.persNr – m.persNr ; } // konsistente equals() + hashCode() -Implementierungen Zwei Objekte, für die equals true liefert sollten auch bei Vergleich mit compareTo 0 liefern ! // Bezug auf selbe Attribute ! } Programmieren 2 - H.Neuendorf (140) Ordnungspattern : IF java.lang.Comparable java.util.Comparator import java.util.* ; import java.util.*; class Mitarbeiter implements Comparable { class Sorted { public static void main( String[] args ) { // … Mitarbeiter[ ] m = new Mitarbeiter[4] ; } m[0] = new Mitarbeiter( 11, "Anna" ) ; m[1] = new Mitarbeiter( 5, "Ursula" ) ; // Weiteres Sortierkriterium: Name m[2] = new Mitarbeiter( 7, "Wolf" ) ; class NameComparator implements Comparator { m[3] = new Mitarbeiter( 13, "Bert" ) ; public int compare( Object p, Object q ) { // Sortieren nach compareTo() : String s1 = ( (Mitarbeiter)p ).name ; Arrays.sort( m ) ; String s2 = ( (Mitarbeiter)q ).name ; System.out.println( Arrays.toString( m ) ) ; return s1.compareTo( s2 ) ; } // Sortieren nach NameComparator : } Arrays.sort( m, new NameComparator( ) ) ; System.out.println( Arrays.toString( m ) ) ; } // Weitere Komparator-Klassen für weitere Attribute … // Auch als Innere Klasse darstellbar } Programmieren 2 - H.Neuendorf (141) Ordnungspattern : Generische Darstellung import java.util.* ; class Mitarbeiter implements Comparable<Mitarbeiter> { public int persNr ; // eindeutige Ordnung public String name ; public Mitarbeiter( int nr, String n ) { /* … */ } public int compareTo( Mitarbeiter m ) { Ab Java5 generisch definiert für Referenztyp T : interface Comparable<T> interface Comparator<T> return this.persNr – m.persNr ; } } Wegfall aller Downcasts // Weiteres Sortierkriterium: Name class NameComparator implements Comparator<Mitarbeiter> { public int compare( Mitarbeiter p, Mitarbeiter q ) { String s1 = p.name ; String s2 = q.name ; return s1.compareTo( s2 ) ; } } Programmieren 2 - H.Neuendorf (142) Iterator-Pattern Ziel : Sequentieller Zugriff auf alle Elemente einer Datensammlung (Kollektion) Einheitliche Behandlung ohne Kenntnis + Preisgabe der Datenstruktur ! Verwender bekommt keinen direkten Zugriff auf zugrundeliegende Datenstruktur ! Kapselung der technischen strukturabhängigen Umsetzung hinter Interface = java.util.Iterator Verwaltung eines internen Zeigers auf aktuelle Position in zugrunde liegender Datenstruktur Mehrere Iteratoren können gleichzeitig auf der selben Kollektion arbeiten Iteratoren lassen sich für alle Datenstrukturen bauen – nicht an Listenkonzept gebunden ! Klassen mit Iterator sind iterierbar – können IF java.lang.Iterable implementieren Abbruch-Kriterium – gibt es noch weitere Elemente? Object next( ) Referenz auf nächstes Element Anwendungs-Idiom : // ref auf Datenstruktur boolean hasNext( ) while( ref.hasNext( ) ) { Object o = ref.next( ) ; Wird von Implementierungen des JavaCollection-Frameworks unterstützt … } Programmieren 2 - H.Neuendorf (143) Iterator – Abgrenzung zu konkreten Iterationen Bisherige Iterationsmethoden setzten Kenntnis der inneren Datenstruktur voraus + greifen direkt darauf zu - so dass private Details exponiert werden Bedienen sich spezieller, stets anders-namiger und -artiger Methoden : Iteration über Array : for( int i=0; i<arr.length; i++ ) { Object o = arr[ i ] ; // Zugriff } Iteration über String : for( int i=0; i<str.length( ); i++ ) { char c = str.charAt( i ) ; // Zugriff } Iteration über Verkettete Liste : for( Node p=head; p!=null; p=p.next ) { Object o = p.o ; // Zugriff } Programmieren 2 - H.Neuendorf (144) Iterator – Verallgemeinerung Ohne Kenntnis der internen Datenstruktur kann durch vordefinierte Semantik + Methoden universell gearbeitet werden - sofern Datenstruktur Iterator-Konzept unterstützt Implementieren von Interfaces : interface Iterator { interface Iterable { // java.lang Iterator iterator( ) ; // java.util } boolean hasNext( ) ; // Noch weitere Elemente in Kollektion ? Object next( ) ; // Liefert aktuelles Element + setzt internen Zeiger auf nächstes Element void remove( ) ; // Löscht das durch letzten next()-Aufruf (!) erhaltene Element // muss nicht unterstützt werden - optional ! } Ausnahmen (unchecked) // Generics : java.util.NoSuchElementException Wenn bei next()-Aufruf keine Elemente mehr vorhanden java.util.ConcurrentModificationException next()-Aufruf muss vorausgegangen sein - sonst Exception ! interface Iterator<T> { boolean hasNext( ) ; // Fast-Fail-Semantik Wenn Datenstruktur durch Fremdeinwirkung verändert wurde (s.u.) T next( ) ; java.lang.UnsupportedOperationException Falls remove( ) ohne Funktionalität implementiert wird java.lang.IllegalStateException Aufruf von remove() ohne jeweils direkt vorheriges next() void remove( ) ; } Programmieren 2 - H.Neuendorf (145) Iterator – Besonderheit von next( ) interface Iterator { boolean hasNext( ) ; Object next( ) ; // Liefert aktuelles Element + setzt internen Zeiger auf nächstes Element void remove( ) ; } Jeder Aufruf von next( ) liefert ein weiteres Element der Datensammlung Es wird jedoch keine bestimmte Ordnung der Elemente vorausgesetzt. Somit liefert next( ) die Elemente nicht in einer irgendwie garantierten Reihenfolge ! Einzige Garantie : Jedes Element wird durch next() genau einmal geliefert Alle Elemente des Datenbehälters sind durch Iterator abgreifbar // Beispiel : Entfernen aller Elemente Iterator it = collect.iterator ; while( it.hasNext( ) ){ it.next( ) ; it.remove( ) ; } Es sind auch weitere Iterations-Methoden denkbar und umsetzbar, wie previous(), hasPrevious() … Sind aber in Interface Iterator nicht vorgesehen ! Dafür im Interface java.uiti.ListIterator<T> extends Iterator<T> Programmieren 2 - H.Neuendorf (146) Iterator – mit separater Iterator-Klasse class LinkedList implements Iterable { // ist iterabel ! private Node head ; private Node tail ; class Node { public boolean add( Object obj ) { /* … */ } public Object val ; // … public Node next ; public java.util.Iterator iterator( ) { // IF Iterable public Node( Object obj ) { return new ListIterator( head ) ; val = obj ; next = null ; } } } class ListIterator implements java.util.Iterator { private Node cursor ; } public ListIterator( Node n ){ cursor = n ; } public boolean hasNext( ) { return cursor != null ; } public void remove( ) { throw new UnsupportedOperationException( ) ; } Nach einem kompletten Durchgang ist ein gewöhnlicher Iterator "verbraucht" public Object next( ) { if( !hasNext( ) ) throw new java.util.NoSuchElementException( ) ; Object o = cursor.val ; cursor = cursor.next ; return o ; // oder : Clonen! Nachteil : Verwender hat es mit mehreren separaten Klassen zu tun … } } Programmieren 2 - H.Neuendorf (147) Iterator – Verwendung import java.util.Iterator ; class IterTest { public static void main( String[] args ) { LinkedList MyList = new LinkedList( ) ; Auf einer Datenstruktur könnten mehrere Iteratoren gleichzeitig störungsfrei arbeiten. Allerdings nur, wenn Methode remove( ) nicht unterstützt wird → Umgang damit s.u.. MyList.add( "das" ) ; Testen : Iterator muss auch mit noch leerer Datenstruktur funktionieren, ohne Abbrüche (NullpointerException) zu produzieren ! Iterator it = MyList.iterator( ) ; Methode next( ) liefert Typ Object . while( it.hasNext( ) ) { Zwingt Verwender zum Casten ! MyList.add( "dies" ) ; String s = (String) it.next( ) ; } Ist bei Verwendung der generischen Variante nicht mehr erforderlich (s.u.) ! for( Object s : MyList ) { IO.writeln( (String)s ) ; } } Alle iterablen Strukturen können mit der seit Java5 verfügbaren erweiterten for-Schleife (forEach-Loop) komplett iteriert werden ! } Programmieren 2 - H.Neuendorf (148) Iterator – Klasse selbst als Iterator class LinkedList implements Iterable, java.util.Iterator { private Node head ; private Node tail ; private Node cursor ; public boolean add( Object obj ) { /* … */ } // … public java.util.Iterator iterator( ) { // IF Iterable durch Klasse selbst impl. cursor = head ; return this ; Vorteil : Der technische Iterator der verketteten Liste ist nach Außen hin nicht mehr als separate Klasse sichtbar. Seine Implementierung ist nun ein technisches Detail der Internas der Klasse LinkedList. } public boolean hasNext( ) { return cursor != null ; } public void remove( ) { throw new UnsupportedOperationException( ) ; } public Object next( ) { if( !hasNext( ) ) throw new java.util.NoSuchElementException( ) ; Object o = cursor.val ; // oder : Clonen! cursor = cursor.next ; return o ; } Nachteil : Nun sind nicht mehr mehrere Iteratoren gleichzeitig für dieselbe Kollektion darstellbar ! Kommen sich über gemeinsame Referenz-Variable cursor ins Gehege … Clonen der gesamten Struktur in Methode iterator( ) wäre ziemlich aufwendig … } Programmieren 2 - H.Neuendorf (149) Iterator – Erlaubt einfache Definition weiterer Methoden class LinkedList implements Iterable, java.util.Iterator { private Node head ; private Node tail ; private Node cursor ; public boolean add( Object obj ) { /* … */ } // … public java.util.Iterator iterator( ) { cursor = head ; return this ; } public boolean hasNext( ) { /* … */ } public void remove( ) { /* … */ } public Object next( ) { /* … */ } public int size( ) { int n = 0 ; for( Object o : this ) n++ ; return n ; } public boolean contains( Object val ) { for( Object o : this ) if( o.equals( val ) ) return true ; return false ; } public String toString( ) { StringBuffer sb = new StringBuffer( ) ; for( Object o : this ) sb.append( o ) ; return sb.toString() ; } } Programmieren 2 - H.Neuendorf (150) Iterator – mit innerer Klasse class LinkedList implements Iterable { private Node head ; private Node tail ; public boolean add( Object obj ) { /* … */ } // … private class ListIterator implements java.util.Iterator { private Node cursor = head ; public boolean hasNext( ) { return cursor != null ; } Vorteil : Der technische Iterator der verketteten Liste ist nach Außen hin nicht mehr als separate Klasse sichtbar. Seine Implementierung ist nun ein technisches Detail der Internas der Klasse LinkedList. public void remove( ) { throw new UnsupportedOperationException( ) ; } public Object next( ) { if( !hasNext() ) throw new java.util.NoSuchElementException( ) ; Object o = cursor.val ; // oder : Clonen! cursor = cursor.next ; Als separate innere Klasse gekapselt, so dass Klasse intern "aufgeräumter" (besser strukturiert) ist return o ; } } public java.util.Iterator iterator( ) { // IF Iterable return new ListIterator( ) ; Es sind wieder mehrere Iteratoren möglich, die störungsfrei gleichzeitig auf derselben Liste arbeiten ! } private class Node { /* … */ } } Programmieren 2 - H.Neuendorf (151) Iterator – mit innerer Klasse - Generics class Node<T> { public T val ; public Node<T> next ; public Node( T obj ) { val = obj ; next = null ; } } class LinkedList<T> implements Iterable<T> { private Node<T> head ; private Node<T> tail ; public boolean add( T obj ) { /* … */ } // … private class ListIterator implements java.util.Iterator<T> { private Node<T> cursor = head ; public boolean hasNext( ) { return cursor != null ; } public void remove( ) { throw … ; } public T next( ) { import java.util.Iterator ; class IterTest { // Verwender public static void main( String[ ] args ) { // Rückgabetyp T !! LinkedList<String> MyList = new LinkedList<String>( ) ; MyList.add( "dies geht" ) ; if( !hasNext( ) ) throw … ; T ref = cursor.val ; cursor = cursor.next ; return ref ; Iterator<String> it = MyList.iterator( ) ; while( it.hasNext( ) ) { // Typ passt nun ohne Cast : String s = it.next( ) ; // !! } } } public java.util.Iterator<T> iterator( ) { return new ListIterator( ) ; } } } } Programmieren 2 - H.Neuendorf (152) Iterator – mit korrekter Fast-Fail-Technik Prinzip : Verwalten eines separaten Modifikations-Counters innerhalb der Datenstruktur und innerhalb jedes ihrer Iteratoren Beim Erzeugen des Iterators wird dessen Modifikations-Counter auf den Wert des Modifikations-Counters der zugehörigen Datenstruktur gesetzt Wenn Datenstruktur ihren Bestand durch eigene Methoden verändert (z.B. add(), remove() ) dann zählt sie nur ihren eigenen Mod-Counter hoch Wenn Iterator durch remove()-Method den Datenbestand verändert, dann zählt er eigenen Mod-Counter und Mod-Counter der Datenstruktur hoch ! Bevor Iterator eine seiner Methoden durchführt, vergleicht er seinen ModCounter-Stand mit dem Mod-Counter-Stand der Datenstruktur Wenn die Stände identisch sind führt der Iterator seine Aktion aus Wenn die Stände unterschiedlich sind wirft der Iterator eine ConcurrentModificationException Dieses Verhalten wird nicht durch das IF Iterator vorgeschrieben, aber von den Klassen des Collections Frameworks praktiziert Bei CopyOnWrite-Datenstrukturen ist dies nicht nötig. Jede Änderung erzeugt Kopie, Iterator arbeitet auf unveränderlichem Bestand. Programmieren 2 - H.Neuendorf (153) Iterator – mit korrekter Fail-Fast-Technik class LinkedList implements Iterable { private int modCount = 0 ; private Node head ; private Node tail ; public boolean add( Object obj ) { /* … */ modCount++ ; } // in allen verändernden Methoden !! private class ListIterator implements java.util.Iterator { private int myCount = 0 ; private Node cursor = head ; public ListIterator( ){ myCount = modCount ; } public boolean hasNext( ) { if( myCount != modCount ) throw new java.util.ConcurrentModificationException( ) ; return cursor != null ; } public void remove( ) { if( myCount != modCount ) throw new java.util.ConcurrentModificationException( ) ; // Implementierung … myCount++ ; modCount++ ; // synchrones Verändern } public Object next( ) { if( myCount != modCount ) throw new java.util.ConcurrentModificationException( ) ; // Implementierung … } } public java.util.Iterator iterator( ) { return new ListIterator( ) ; } private class Node { /* … */ } } Programmieren 2 - H.Neuendorf (154)