Datenstrukturen Schwerpunkte Bedeutung von Datenstrukturen Dynamische Datenstrukturen mit Java Umgang mit Objektreferenzen als "Pointer" Vorbereitung zum Collection Framework Programmieren 2 - H.Neuendorf (66) Datenstrukturen Zusammenfassung "gleichartiger" Daten in strukturierter Weise : Technische Datenverwaltung + erlaubte semantische Operationen Typischerweise wird wahlfreier Zugriff eingeschränkt - um Semantik zu realisieren Bereits behandelte "Datenstrukturen" : 1. Klassen → Attribute enthalten Daten + Methoden regeln Zugriff Sehr allgemein - damit beliebige konkrete Datenstrukturen darstellbar 2. Arrays → Indizierte Anordnung : Datenzugriff via Index Wahlfreier Zugriff = Elemente in beliebiger Reihenfolge zugreifbar aber : Einfügen zwischen belegte Plätze muss implementiert werden Anzahl der Elemente steht fest - nicht veränderbar (statisch) ! ⇒ Dynamische Datenstrukturen = Container, die sich dynamisch der wachsenden oder verringernden Datenmenge anpassen ! Programmieren 2 - H.Neuendorf (67) Datenstrukturen Stack und Queue zur Einführung Realisation auf Basis von Arrays Prinzip Abstrakter Datentypen Beispiel: Abstrakter Datentyp Stack Objekt-Pool Programmieren 2 - H.Neuendorf (68) Stack ( Keller, Stapel ) Stapel von Daten (Objekten), die "übereinander liegen" : LIFO = Last In First Out - Zuletzt abgelegtes Element wird als erstes wieder entnommen Typische Operationen: Stapel s, Element x : Ablegen neuer Objekte oben auf Stapel push Zurückliefern + Entfernen des obersten Stapelobjekts pop Zurückliefern oberstes Stapelobjekt ohne Entfernen peek s.push( x ) ; // Element abgelegt x = s.pop( ) ; // Rückgabe des entfernten obersten Elements KEIN wahlfreier Zugriff - nur oberstes Element jeweils ansprechbar x s.push(x); y x s.push(y); Einfügen und Entnehmen am selben Ende = "oben" x z = s.pop(); z = s.pop(); // z == y // z == x Entnahmereihenfolge kehrt Einfügereihenfolge um: "Aktuellstes" Element immer oben Informatik → Methodenstack / Rekursions-Zwischenergebnisse auf Stack ( kein Basisfall ⇒ stack overflow! ) Programmieren 2 - H.Neuendorf (69) Stack-Implementierung via Array push : Anfügen des Elements "am Ende" des Arrays pop : an jeweils oberster Position Abholen des Elements "vom Ende" des Arrays Ausnahmesituationen : Kein Platz mehr für neue Elemente ⇒ Keine Elemente mehr im Array Stack - int [ ] s - int pos + void push( int x ) ⇒ overflow underflow Durch Hilfsmethoden wie isFull() isEmpty() … kann man Prüfung auf RuntimeExceptions vermeiden Bedeutung pos : Anzahl momentan im Stack gespeicherte Datenelemente Zugleich Index des nächsten freien Platzes, auf den das nächste einzufügende Datenelement zu setzen ist = aktuelle Einfüge-Position + int pop( ) Belegung beginnt bei Indexposition 0 // … Wirkung von Array s pos = pos - 1 ; nächster freier Platz hat Index pos -1; dortiger Wert wird überschrieben Statt int könnte man beliebige Objekte im Stack ablegen durch entsprechenden Typ des Arrays Bsp: Mitarbeiter[ ] s push ... pop s[3] s[2] s[1] s[0] Programmieren 2 - H.Neuendorf (70) Stack-Impl. via Array class Stack { Stack hat "Gedächtnis" - merkt sich zuletzt besetzten Platz private int[ ] s ; // privater Datenbehälter private int pos = 0 ; // Anzahl gespeicherter Elemente public Stack( int size ) { s = new int[ size ] ; Array gewünschter Größe anlegen } public void push( int x ) { Zugriff nur über öffentliche Schnittstelle! boolean overflow = ( pos >= s.length ) ; Kein direkter Zugriff auf Daten ! if ( overflow ) { /* Ausnahmebehandlung */ } s[ pos ] = x ; pos++ ; // s[ pos++ ] = x ; } Prüfung, ob Array voll ist public int pop( ) { Wenn noch Platz, dann Datenelement einfügen boolean underflow = ( pos == 0 ) ; Zähler hochzählen pos-- ; if ( underflow ) { /* Ausnahmebehandlung */ } return s[ pos ] ; } public int peek( ) { return s[ pos-1] ; } Prüfung, ob Array leer ist public boolean isEmpty( ) { return pos == 0 ; } Wenn nicht leer, dann Datenelement zurückliefern Zähler vermindern public boolean isFull( ) { return pos >= s.length ; } public void clear( ) { pos = 0 ; } } Programmieren 2 - H.Neuendorf (71) Queue ( Schlange, Puffer ) Daten-Röhre : An einem Ende rein, am andern raus ... FIFO = First In First Out - Zuerst abgelegtes Element als erstes wieder entnommen Typische Operationen : Anfügen neuer Objekte am Queue-Ende Queue q, Element x : put Zurückliefern + Entfernen vorderstes Objekt get Zurückliefern vorderstes Objekt ohne Entfernen peek q.put( x ) ; // Element abgelegt x = q.get( ) ; // Rückgabe + Entfernen vorderstes Element KEIN wahlfreier Zugriff - nur vorderstes Element ansprechbar ! x xy x q.put(x); y x q.put(y); q.put(z); z y z e = q.get(); // e == x x y z Head Tail Entnahmestelle Einfügestelle Einfügen und Entfernen an verschiedenen Enden ! Entnahmereihenfolge identisch zu Einfügereihenfolge z e = q.get(); // e == y Warteschlangen dienen der Entkopplung. Zwischengeschalteter Puffer zwischen Erzeuger (Einfügen am tail) und Verbraucher (Entnahme am head) von Daten, Nachrichten etc. Keine direkte Interaktion. Voraussetzung ist korrekte Synchronisation. Informatik / Modellierung : Warteschlangen, Pipes, Message- / Task-Queues … Programmieren 2 - H.Neuendorf (72) Queue-Implementierung via Array put : Anfügen Element am Array-Ende get : Abholen "ältestes" Elements vom am "längsten belegten" Array-Platz Index head : erstes = "ältestes" Element ⇒ Entnahme alter Elemente aus q[ head ] Index tail : Einfügen neuer Elemente in q[ tail ] nächster freier Platz Ausnahmesituationen : Queue - int[ ] q ⇒ Kein Platz mehr für neue Elemente ⇒ overflow Keine Elemente mehr im Array ⇒ underflow 0 1 2 3 4 5 6 7 4 5 6 7 q tail head - int head, tail 0 - int size 1 2 3 q + void put( int x ) + int get( ) head tail Vereinfachung: Nach Entnahme wird Inhalt nicht nach links verschoben ! Stattdessen Index head erhöht ⇒ belegter Teil wandert nach rechts ⇒ Belegter Teil erreicht Arrayende - obgleich Array nicht voll ist Nutzung leerer Plätze am Anfang : Zyklisches Array … Programmieren 2 - H.Neuendorf (73) Queue-Implementierung via Array 0 1 2 3 4 5 6 7 q Zyklisches Array (Ringpuffer) : Indices head und tail laufen "im Kreis" tail head 0 1 2 3 4 5 6 7 Darstellbar durch Berechnung des Index tail nach Einfügen : tail = ( tail + 1 ) % 8 ; q allgemein : modulo size head tail Berechnung Index head nach Entnahme : head = ( head + 1 ) % size ; 7 0 Erkennen Ausnahmesituationen 6 a) Leere Schlange 1 underflow : head == tail ; b) Volle Schlange overflow : tail ist "rundherum gelaufen" - also wiederum head == tail. Zur Unterscheidung von leerer Schlange wird deshalb boolsche Variable full verwaltet : Wenn head==tail in put( ) , dann ist Queue voll wenn head==tail in get( ) , dann ist Queue leer Programmieren 2 - H.Neuendorf (74) Queue via Array Queue - int[ ] q - int head, tail - int size - boolean full + void put( int x ) + int get( ) Zugriff nur über öffentliche Schnittstelle! Kein direkter Zugriff auf Daten! Array fester Größe ⇒ Umständliche Verwaltung der Belegung als zyklisches Array class Queue { private int[ ] q ; // privater Datenbehälter private int head ; // Index erstes Element private int tail ; // Index nächster freier Platz = Einfügeposition private int size ; // Queue-Größe private boolean full ; // Kontrolle Füllstand public Queue( int n ) { q = new int[ n ] ; size = n ; tail = 0 ; head = 0 ; full = false ; } public void put( int x ) { if ( isFull( ) ) throw new RuntimeException( "Full" ) ; q[ tail ] = x ; tail = (tail + 1) % size ; full = ( head == tail ) ; } public int get( ) { if ( isEmpty( ) ) throw new RuntimeException( "Empty" ) ; int x = q[ head ] ; head = (head + 1) % size ; full = false; return x ; } public boolean isEmpty( ) { return head == tail && full == false ; } public boolean isFull( ) { return full ; } public void clear( ) { head = tail = 0 ; full = false ; } } Programmieren 2 - H.Neuendorf (75) Abstrakte Datentypen ADT Abstrakter Datentyp von Implementierung unabhängige Spezifikation einer Datenstruktur unterschiedliche Implementierungen sind möglich Details der Implementierung interessieren Anwender nicht – sind weggekapselt universelle, von Implementierung unabhängige Verwendung "abstrakt" - da nach außen nur öffentliche Methoden bekannt - nicht aber konkrete Impl.-Details Realisierung von ADTs in Java Spezifikation (der Methoden) der Datenstruktur durch Interface verschiedene implementierende Klassen Unabhängigkeit von Implementierungen durch Verwendung des Interface-Typs beim Aufrufer Instanziiert werden jedoch natürlich stets konkrete Implementierungen Beispiel Abstrakte Datentypen 'Stack' + 'Queue' → intern array-basiert oder list-basiert realisierbar Programmieren 2 - H.Neuendorf (76) Abstrakte Datentypen Bsp : Datentyp Stack Spezifikation (der Methoden) des Datentyps durch Interface public interface Stack { public void push( Object obj ) ; public Object pop( ) ; public Object peek( ) ; public void clear( ) ; public boolean isEmpty( ) ; // … } Wichtige Detail-Entscheidung bei Dokumentation des Interfaces + seiner Implementierung (zB auch im Collection Framework) : Werden null-Werte als Datenelemente akzeptiert – und führen dann zur Erhöhung der internen size ? Können null-Werte von Methoden zurückgeliefert werden ? Zurückliefern von null sollte eher bedeuten, dass gesuchtes Elemente nicht vorhanden ist ! Bereitstellen (mindestens) einer Implementierung public class ArrStack implements Stack { private Object[ ] s ; private int pos = 0 ; public void push( Object obj ) { // Implementierung … } /* … */ } Anwendung : Methodenaufruf via Interface-Typ – sorgt für semantische Klarheit Stack s = new ArrStack( ) ; s.push( "Hallo" ) ; Programmieren 2 - H.Neuendorf (77) Object Pool of reusable objects mittels Array / Stack Metapher : Pfandflaschen / Carpool Wiederverwendung von Objektinstanzen, deren Lebenszyklus überwacht wird - weil deren Erzeugung aufwendig ist oder deren Anzahl via Poolgröße limitiert / gesteuert werden soll Instanzen nur zeitweise verwendet + danach zur Verwendung durch andere Clients zurück gestellt Gebrauchtes Objekt wird bei Wiederaufnahme in Pool gesäubert, indem Zustand bereinigt wird (Zurücksetzen in Ursprungszustand) Pool-Überwachung ermöglicht effizienten Umgang mit Ressourcen + Aussage über Systemlast Mögliche Probleme : In Multithreaded-Umgebungen ist Pool ein bremsender Synchronisationspunkt Leerer + zu voller Pool bremst Abläufe aus ⇒ Größe eventuell dynamisch anpassen Clients müssen Objekte stets zurückgeben - auch bei Auftreten von Exceptions Clients dürfen Referenz nach Rückgabe nicht mehr verwenden - durch gültige IDs checkbar Clients dürfen Referenz nicht weitergeben Clients müssen mit leerem Pool umgehen ⇒ Timeouts definieren ⇒ Stabile + sichere Realisation schwierig - nur möglich, wenn Clients unter eigener Kontrolle Programmieren 2 - H.Neuendorf (78) Object Pool - via Stack class ObjectPool { private Stack pool ; public ObjectPool( int size ) { class ReusableObject { pool = new Stack( size ) ; private int zustand = 0 ; while( pool.notFull( ) ) { public int operation( ) { pool.push( new ReusableObject( ) ) ; } zustand++ ; } return zustand ; public ReusableObject acquire( ) { } if( pool.isEmpty( ) ) return null ; public void clean( ){ zustand = 0 ; } ReusableObject r = pool.pop( ) ; } return r ; } public void returnInstance( ReusableObject r ) { Voraussetzung ist Stack-Klasse für Objekte vom Typ ReusableObject : if( pool.isFull( ) ) return ; if( r != null ) { Klasse Stack verwaltet intern ein Array vom Typ ReusableObject r.clean( ) ; pool.push( r ) ; Oder : Generics mit entsprechendem Interface IF : <T extends IF> } } } Programmieren 2 - H.Neuendorf (79) Object Pool - Client // Client ist unabhängig von Implementierungsdetails class Client { private ReusableObject myInstance ; // Nutzung : class PoolTest { private ObjectPool myPool ; public static void main( String[] args ) { public Client( ObjectPool pool ) { myPool = pool ; } ObjectPool pool = new ObjectPool( 5 ) ; public void setInstance( ) { Client c1 = new Client( pool ) ; if( myInstance == null ) c1.setInstance( ) ; myInstance = myPool.acquire( ) ; c1.work( ) ; } c1.pushBack( ) ; public void pushBack( ) { // … myPool.returnInstance( myInstance ) ; } myInstance = null ; } } public void work( ) { if( myInstance != null ) myInstance.operation( ) ; } } Programmieren 2 - H.Neuendorf (80)