5.3 Subtypen und Vererbung
Klassifikation ist eine zentrale Grundlage der
objektorientierten Modellierung und Programmierung.
Begriffsklärung: (Klassifikation)
Klassifizieren ist eine allgemeine Technik, mit der
Wissen über Begriffe, Dinge und deren Eigenschaften hierarchisch strukturiert wird. Das Ergebnis nennen wir eine Klassifikation.
Dieser Abschnitt erläutert die Konzepte der Subtypbildung und Vererbung.
5.3.1 Klassifizieren von Objekten Überblick:
• Klassifizieren von Objekten
• Subtypen und Schnittstellentypen
• Vererbung
Beispiele: (Klassifikationen)
Wirbeltiere
Fische Lurche Reptilien Säugetiere Vögel
Wale Primaten Paarhufer
_ ist_ein _ (Vogel ist ein Wirbeltier)
Recht
Öffentliches Recht
Privatrecht Kirchenrecht
Bürgerliches Recht
Handels- recht
Urheber- recht
Figur
Ellipse Vieleck
Viereck Dreieck Kreis
Parallelogramm
Raute Rechteck
Quadrat Person
Student Angestellte
Wissenschaftl.
Angestellte
Verwaltungs- angestellte
• Beobachtungen zu Klassifikationen:
- Sie können sich auf Objekte oder Gebiete beziehen.
- Sie können baumartig oder DAG-artig sein.
- Objektklassifikationen begründen ist-ein-Beziehungen.
- Es gibt abstrakte Klassen (ohne „eigene“ Objekte) und nicht abstrakte Klassen
• Üblicherweise stehen die allgemeineren Begriffe oben, die spezielleren unten.
Bemerkung:
Ziel:
Anwendung der Klassifikationstechnik auf Objekte in der Software-Entwicklung.
Klassifikation in der Softwaretechnik:
Objekte lassen sich nach ihren Eigenschaften klassifizieren:
- Alle Objekte mit ähnlichen Eigenschaften werden zu einer Klasse zusammen gefasst.
Klassifikation beruht auf:
- Schnittstellen der Klassen/Objekte - Verhalten/Eigenschaften der Objekte Genauer:
1. Syntaktisch: Subklassenobjekte haben im Allg.
größere Schnittstellen als Superklassenobjekte (Auswirkung auf Programmiersprache)
2. Semantisch: Subklassenobjekte bieten mindestens die Eigenschaften der Superklassenobjekte.
Zentraler Aspekt der OO-Programmentwicklung:
Entwurf und Realisierung von Klassen- bzw.
Typhierarchien.
Abstraktion/Generalisierung
... das Heraussondern des unter einem bestimmten Gesichtspunkt Wesentlichen vom Unwesentlichen.
[Meyers großes Taschenlexikon]
Begriffsklärung: (Abstraktion)
Abstraktion geht also vom Speziellen zum Allgemeinen.
Ansatz:
• Für unterschiedliche Objekte bzw. Typen mit gemeinsamen Eigenschaften soll Software entwickelt werden.
Beispiele:
- Komponenten von Fenstersystemen (Menues, Schaltflächen, Textfelder, ...)
- Ein-/Ausgabeschnittstellen (Dateien, Netze, ...)
• Erarbeite einen abstrakteren Typ, der die gemein- samen Eigenschaften zusammenfasst und eine
entsprechende Schnittstelle bereitstellt (Verkleinern der Schnittstelle).
• Programme, die sich auf die Schnittstelle des abstrakteren Typs abstützen, arbeiten für alle Objekte mit spezielleren Schnittstellen.
Beispiel: (Gemeinsame Eigenschaften)
Wir betrachten zwei Klassen mit gemeinsamen Eigenschaften:
class Student { String name;
int matNr;
...
void drucken() {
System.out.println( name );
System.out.println( matNr );
...
} }
class Professor { String name;
int telNr;
...
void drucken() {
System.out.println( name );
System.out.println( telNr );
...
} }
Anforderung:
Alle Personendaten sollen gedruckt werden.
Abstraktion:
- Entwickle einen Typ Person, der die Nachricht drucken versteht.
- Formuliere das Drucken der Personendaten auf Basis des Typs Person
Person[] p = new Person[4];
p[0] = new Student(...);
p[1] = new Professor(...);
...
for( i=0; i<p.length; i++ ) { p[i].drucken();
}
dynamisches Binden
ungleiche Typen
Deklaration des Typs Person in Java:
interface Person { void drucken();
}
Anpassen der Typen Student und Professor:
class Student implements Person { ... // wie oben
}
class Professor implements Person {
Spezialisierung
Spezialisierung bedeutet hier das Hinzufügen speziellerer Eigenschaften zu einem Gegenstand oder das Verfeinern eines Begriffs durch Einführen weiterer Merkmale (z.B. berufliche Spezialisierung).
Begriffsklärung: (Spezialisierung)
Spezialisierung geht also vom Allgemeinen zum Speziellen.
Ansatz:
• Existierende Objekte bzw. Typen sollen zusätzliche Anforderungen erfüllen.
Beispiele:
- spezielle Komponenten für eine graphische Bedienoberfläche
- Anpassung eines Buchführungssystems an die speziellen Anforderungen einer Firma
• Erweitere die existierenden Typen (zusätzliche Attribute & Methoden, Anpassen von Methoden).
Im Allg. vergrößern sich dabei die Schnittstellen.
• Existierende Programme für die allgemeineren Typen arbeiten auch mit den spezielleren Typen.
Programmtechnische Mittel zur Spezialisierung:
- Hinzufügen von Attributen - Hinzufügen von Methoden
- Anpassen, Erweitern bzw. Implementieren von Supertyp-Methoden:
Überschreiben
Anwenden überschriebener Methoden
Beispiel: (Spezialisierung)
Wir spezialisieren die Klasse Frame des AWT:
package memoframe;
import java.awt.* ;
class MemoFrame extends Frame { private Color letzterHintergrund;
public void einstellenLetztenHintergrund() { setBackground( letzterHintergrund );
}
public void setBackground( Color c ) { letzterHintergrund = getBackground();
super.setBackground( c );
package memoframe;
public class TestMemoFrame {
public static void main(String[] args) { MemoFrame f = new MemoFrame();
f.setLocation( 200, 200 );
f.setSize( 300, 200 );
f.setVisible( true );
f.setBackground( Color.red );
f.update( f.getGraphics() );
try{ Thread.sleep(4000); } catch( Exception e ){}
f.setBackground( Color.green );
f.update( f.getGraphics() );
try{ Thread.sleep(4000); } catch( Exception e ){}
f.einstellenLetztenHintergrund();
f.update( f.getGraphics() );
try{ Thread.sleep(4000); } catch( Exception e ){}
System.exit( 0 );
} }
Bemerkung:
• Eine genaue Kenntnis der zu spezialisierenden Klasse ist meist nicht nötig. Die ererbten
Eigenschaften kann man über die Methoden ansprechen.
• Überschreibende Methoden können die über- schriebene Methode nutzen.
• Zwei Aspekte werden demonstriert:
Subtypbeziehung:
Ein MemoFrame-Objekt ist ein Frame-Objekt.
Vererbung:
Ein MemoFrame-Objekt erbt den größten Teil seiner Implementierung von der Klasse Frame.
Zusammenfassung zu 5.3.1
- Jedes Objekt hat Schnittstelle aus Attributen und Methoden.
- Objekte werden entsprechend ihrer Schnittstelle klassifiziert.
- Allgemeinere Objekte haben kleinere Schnittstelle
- Abstraktion/Generalisierung erlaubt es, Typen zu deklarieren, die die relevante Gemeinsamkeiten anderer Typen ausdrücken.
- Spezialisierung erlaubt es, Typen zu deklarieren, die die Funktionalität existierender Typen erweitern.
- Entwurf geeigneter Klassenhierarchien ist ein zentraler Aspekt des objektorientierten Entwurfs bzw. der objektorientierten Programmierung.
Dabei sind Abstraktion und Spezialisierung sinnvoll zu kombinieren.
5.3.2 Subtypen und Schnittstellentypen
Übersicht::
- Klassifikationen und Typisierung - Schnittstellentypen in Java
- Subtypbildung in Java
- Dynamische Methodenauswahl - Weitere Aspekte der Subtypbildung - Programmieren mit Schnittstellentypen
Typ beschreibt Eigenschaften von Werten bzw. Objekten. Annahme bisher:
Kein Objekt bzw. Wert gehört zu mehr als einem (nicht-parametrisierten) Typ.
Klassifikationen und Typisierung
Ansatz:
- Realisiere jede Klasse/jeden Begriff einer Klassifikation im Programm durch einen Typ.
- Führe eine partielle Ordnung (vgl. Folie 268) auf Typen ein, so dass
speziellere Typen gemäß der Ordnung kleiner als ihre allgemeineren Typen sind und
alle Objekte speziellerer Typen auch zu den allgemeineren gehören.
Wenn S T gilt, d.h. wenn S ein Subtyp von T ist, dann gehören alle Objekte von S auch zu T.
Wenn S T und S T, heißt S ein echter Subtyp von T, in Zeichen S < T.
Wenn S T, dann heißt T ein Supertyp von S, und wir schreiben auch T S.
Beispiel: (Subtypbeziehungen)
In Java gibt es einen allgemeinsten Referenztyp, genannt Object. Es gilt:
String Object ,
MemoFrame Object , MemoFrame Frame , int[] Object ,
Student Person, Person Object
Prinzip der Substituierbarkeit:
Sei S T; dann ist an allen Programmstellen, an denen ein Objekt vom Typ T zulässig ist, auch ein Objekt vom Typ S zulässig.
Konsequenzen:
• Subtypobjekte müssen alle Eigenschaften des Supertyps aufweisen.
• Eine Ausdruck von einem Subtyp kann an Stellen verwendet werden, an denen in Sprachen ohne
Subtypen nur ein Ausdruck von einem allgemeineren Typ zulässig wäre.
Beispiel: (Substituierbarkeit)
Folgende Anweisungen sind typkorrektes Java:
Bemerkung:
• Vereinfachend betrachtet, kann man Typen
als die Menge ihrer Objekte bzw. Werte auffassen.
Bezeichne M(S) die Menge der Objekte vom Typ S.
Für Typen S und T gilt:
S T impliziert M(S) M(T)
• In Java wird die Subtyprelation im Wesentlichen zusammen mit den Typdeklarationen definiert.
Object ov = "Ein String ist auch ein Object";
Person[] p = new Person[4];
p[0] = new Student(...);
Schnittstellentypen in Java
In Java gibt es zwei Arten von benutzerdefinierten Typen:
- Klassentypen
Eine Klasse deklariert einen Typ und beschreibt Objekte diesen Typs, d.h. u.a. deren öffentliche Schnittstelle und Implementierung.
Eine Schnittstelle deklariert einen Typ T und beschreibt die öffentliche Schnittstelle, die alle Objekte von T haben.
Mögliche Implementierungen für Objekte von T liefern die echten Subtypen von T.
Insbesondere lassen sich zu einem Schnittstellentyp T keine Objekte erzeugen, die nur zu T gehören.
Syntax der Schnittstellendeklaration:
<Modifikatiorenlist> interface <Schnittstellenname>
[ extends <Liste von Schnittstellennamen> ] {
<Liste von Konstantendekl. und Methodensignaturen>
}
Beispiel: (Schnittstellendeklaration)
interface Person { String getName();
int getGeburtsdatum();
void drucken();
boolean hat_geburtstag( int datum );
}
interface Farbe { byte gelb = 0;
byte gruen = 1;
byte blau = 2;
}
Die Deklaration eines Typs T legt fest, welche direkten Supertypen T hat.
Bei einer Schnittstellendeklaration T gilt Folgendes:
- Gibt es keine extends-Klausel, ist Object der einzige Supertyp.
- Andernfalls sind die in der extends-Klausel genannten Schnittstellentypen die direkten Supertypen.
interface Druckbar { void drucken();
}
Subtypbildung in Java
Beispiel: (Subtyprelation bei Schnittstellen)
interface Angestellte
extends Person, Druckbar { String getName();
int getGeburtsdatum();
int getEinstellungsdatum();
String getGehaltsklasse();
void drucken();
boolean hat_geburtstag( int datum );
}
Eine Schnittstellendeklaration erweitert also die Schnittstelle eines oder mehrerer anderer Typen.
Methodensignaturen aus den Supertypen brauchen nicht nochmals aufgeführt werden (Signaturvererbung):
1. Die Typen Person, Druckbar und Farbe haben nur Object als Supertypen.
2. Der Typ Angestellte hat Person und Druckbar als direkte Supertypen:
interface Angestellte
extends Person, Druckbar { int getEinstellungsdatum();
String getGehaltsklasse();
}
Eine Klassendeklaration T deklariert genau eine direkte Superklasse, die auch eine Supertyp ist, und ggf. mehrere weitere Supertypen:
- Gibt es keine extends-Klausel, ist Object die direkte Superklasse.
- Andernfalls ist die in der extends-Klausel genannte Klasse die direkte Superklasse.
- Alle in der implements-Klausel genannten Schnittstellentypen sind Supertypen.
Eine Klasse erweitert die Superklasse (siehe 5.3.3).
Sie implementiert die Schnittstellentypen, die in Syntax der Klassendeklartion:
<Modifikatiorenlist> class <Klassenname>
[ extends <Klassenname> ]
[ implements <Liste von Schnittstellennamen> ] {
<Liste von Attribut-, Konstruktor-, Methodendekl.>
}
Beispiel: (Implementieren von Schnittstellen)
class Student implements Person, Druckbar { private String name;
private int geburtsdatum; // Form JJJJMMTT private int matrikelnr;
private int semester;
public Student(String n,int g,int m,int s){
name = n; geburtsdatum = g;
matrikelnr = m; semester = s;
}
public String getName() { return name; } public int getGeburtsdatum() {
return geburtsdatum;
}
public int getMatrikelnr() { return matrikelnr;
}
public int getSemester() { return semester;}
public void drucken() {
System.out.println("Name:"+ name);
System.out.println("Gdatum:"+ geburtsdatum);
System.out.println("Matnr:" + matrikelnr );
System.out.println("Semzahl:"+ semester );
}
public boolean hat_geburtstag ( int datum ) { return (geburtsdatum%10000)==(datum%10000);
} }
Typen:
• elementare Datentypen: int, char, byte, ....
• Schnittstellentypen
• Klassentypen
• Feldtypen
Referenztypen
Subtyp-Ordnung:
Deklaration: interface S extends T1, T2, ...
impliziert S <T1, S <T2, ...
Deklaration: class S extends T implements T1, T2, ...
impliziert: S < T, S < T1, S < T2, ...
S < T impliziert: S[] < T[]
und davon die reflexive, transitive Hülle.
Zusammenfassung: Typen & Subtyp-Ordnung:
Realisierung von Klassifikationen:
Die Klassen bzw. Begriffe in einer Klassifikation können im Programm durch Schnittstellen- oder Klassentypen realisiert werden.
Wir betrachten die Klassifikation bestehend aus:
Person, Druckbar, Student, Angestellte, WissAngestellte und VerwAngestellte.
1. Variante:
Nur die Blätter der Klassifikation (Student, Wiss-
Angestellte, VerwAngestellte) werden durch Klassen realisert, alle anderen durch Schnittstellen.
Person
Student Angestellte
WissAngestellte VerwAngestellte Druckbar
Object ist Subtyp
ist Subklasse
Dazu die entsprechenden Typdeklarationen:
interface Person { ... } interface Druckbar { ... } interface Angestellte
extends Person,Druckbar { ... } class Student
implements Person, Druckbar {... } class WissAngestellte
implements Angestellte { ... } class VerwAngestellte
implements Angestellte { ... }
class Person { ... }
interface Druckbar { ... } class Student extends Person
implements Druckbar { ... } class Angestellte extends Person
implements Druckbar { ... } class WissAngestellte
extends Angestellte { ... }
2. Variante:
Außer des Typs Druckbar realisieren wir alle Typen durch Klassen:
Das Klassendiagramm zur 2. Variante:
Diskussion:
Verwendung von Schnittstellen in Java:
- nur wenig über den Typ bekannt
- keine Festlegung von Implementierungsteilen
- als Supertyp von Klassen mit mehreren Supertypen Verwendung von Klassen in Java, wenn
- Objekte von dem Typ erzeugt werden sollen;
- Vererbung an Subtypen ermöglicht werden soll.
Person
Student Angestellte
WissAngestellte VerwAngestellte Druckbar
Object ist Subtyp
ist Subklasse
Die Auswertung von Ausdrücken vom (statischen) Typ T kann Ergebnisse haben, die von einem
Subtyp sind.
Damit stellt sich die Frage, wie Methodenaufrufe auszuwerten sind. Hier sind die charakteristischen Beispiele:
Dynamische Methodenauswahl
Beispiel: (Methodenaufruf)
Frame f = new MemoFrame();
...
f.setBackground( Color.red );
Welche Methode soll ausgeführt werden:
1. Auswahl zwischen Methode der Super- und Subklasse:
static void alle_drucken (Druckbar[] df) { int i;
for( i =0; i<df.length; i++) { df[i].drucken();
2. Auswahl zwischen Methode verschiedener Subklassen:
Begriffsklärung: (dynamische Meth.auswahl)
<ZielAusdr>.<methodenName>( <AktParam1>,...);
wird wie folgt bestimmt:
1. Werte <ZielAusdr> aus; Ergebnis ist das Zielobjekt.
2. Werte die aktuellen Parameter <AktParam1>, ... aus.
3. Führe die Methode mit Namen <methodenName>
des Zielobjekts mit den aktuellen Parametern aus.
Dieses Verfahren nennt man dynamische Methoden- auswahl oder dynamisches Binden (engl.
dynamic method binding).
Die auszuführende Methode zu einem Methoden- aufruf:
Bemerkung:
Die Unterstützung von Subtypen und dynamischer
Methodenauswahl ist entscheidend für die verbesserte Wiederverwendbarkeit und Erweiterbarkeit, die durch Objektorientierung erreicht wird.
Zusätzlich werden diese Aspekte auch durch Vererbung unterstützt.
Beispiel: (Erweiterbarkeit)
Wir gehen von einem Programm aus mit der Methode:
static void alle_drucken( Druckbar[] df ) { int i;
for( i =0; i<df.length; i++) { df[i].drucken();
} }
Druckbar ist dabei Supertyp von Student, Angestellte, WissAngestellte und VerwAngestellte.
Das Programm soll erweitert werden, um auch
Professoren und studentische Hilfskräfte behandeln zu können. Es reicht, zwei Klassen hinzuzufügen:
class Professor
implements Person, Druckbar {... } class StudHilfskraft
extends Student { ... }
Eine Änderung des ursprünglichen Programms ist NICHT nötig!
Weitere Aspekte der Subtypbildung
Dieser Unterabschnitt behandelt detailliertere Aspekte zu
- der Subtypordnung
- Typtest und Typkonvertierungen - Polymorphie
Aspekte der Subtypordnung
Zyklenfreiheit:
Die Subtyprelation darf keine Zyklen enthalten (sonst wäre sie keine Ordnung). Folgendes Fragment ist also in Java nicht zulässig:
interface C extends A { ... } interface B extends C { ... } interface A extends B { ... }
Subtyprelation bei Feldern:
Jeder Feldtyp mit Komponenten vom Typ S ist ein Subtyp von Object: S[] Object . D.h. folgende Zuweisung ist zulässig:
Object ov = new String[3] ;
Ist S T, dann ist S[] T[] .
D.h. folgende Zuweisung ist zulässig:
Person[] pv = new Student[3] ;
Diese Festlegung der Subtypbeziehung
zwischen Feldtypen ist in vielen Fällen praktisch.
Problem:
Statische Typsicherheit ist nicht mehr gegeben:
String[] strfeld = new String[2];
Object[] objfeld = strfeld;
objfeld[0] = new Object(); //Laufzeitfehler // ArrayStoreException int strl = strfeld[0].length();
Speicherzustand nach der zweiten Zeile:
strfeld:
objfeld:
:String[]
2
length:
0:
1:
•
•
Subtypen und elementare Datentypen:
Zwischen den elementaren Datentypen und den Referenztypen gibt es keine Subtypbeziehung:
int Object , int boolean , double int d.h. die folgenden Zuweisungen sind unzulässig:
Object ov = 7;
boolean bv = 9;
int iv = 3.4;
Wie in ML gibt es auch in Java die Möglichkeit, Werte eines elementaren Datentyps in Werte eines anderen Datentyps zu konvertieren (siehe unten).
Der Zusammenhang zwischen elementaren Datentypen und Referenztypen wird in Java über sogenannte Wrapper-Klassen erzielt.
Ein Wrapper-Objekt für den elementaren Datentyp D besitzt ein Attribut zur Speicherung von Werten des Typs D.
Anwendung von Wrapper-Klassen:
Integer ist die Wrapper-Klasse für den Typ int:
Integer iv = new Integer(7);
Object ov = iv;
int n = iv.intValue() + 23 ;
Character
Number
BigInteger Comparable
Object
Javas Wrapper-Klassen sind im Paket java.lang definiert. Folgendes Diagramm zeigt die Subtyp- beziehungen:
Byte Short
Integer
Boolean
Long Float Double
public interface Comparable {
public int compareTo(Object o);
}
public class Main {
static boolean issorted( Comparable[] cf ) { int i;
if( cf.length<2 ) return true;
for( i=0; i<cf.length-1; i++) {
if( cf[i].compareTo(cf[i+1]) > 0 ) { return false;
} }
return true;
}
public static void main( String[] args ) { boolean b;
Character[] cfv = new Character[4];
cfv[0] = new Character('\'');
cfv[1] = new Character('Q');
cfv[2] = new Character('a');
cfv[3] = new Character('b');
b = issorted(cfv);
System.out.println("cfv sortiert: " + b );
} }
Typtest und Typkonvertierungen:
Werte eines elementaren Datentyps lassen sich mittels sogenannter Casts in Werte anderer
Datentypen konvertieren:
double dv = 3333533335.3333333;
// dv == 3.3335333353333335E9 float fv = (float) dv;
// fv == 3.33353344E9 long lv = (long) fv;
// lv == 3333533440L int iv = (int) lv;
// iv == -961433856 short sv = (short) iv;
// sv == -20736
byte bv = (byte) sv;
// bv == 0
Typkonvertierungen von Datentypen mit kleinerem Wertebereich in solche mit größerem Wertebereich werden automatisch durchgeführt:
3.4 + 7
ist äquivalent zu:
Bei Referenztypen prüft ein Cast, ob das geprüfte Objekt zu dem entsprechenden Typ gehört:
- falls ja, wird die Ausführung fortgesetzt;
- falls nein, wird eine ClassCastException ausgelöst.
Number nv = new Integer(7);
Object ov = (Object) nv; // upcast Number nv1 = (Object) nv; //
Integer iwv = nv; //
Integer iwv1 = (Integer) nv; // downcast Float fwv = (Float) nv; //
Comparable c = (Comparable) nv; //
String sv = (String) nv; //
Beispiel: (Konvertieren von Referenztypen)
Java bietet außerdem den Operator instanceof zum Typtesten an:
Comparable c;
if( nv instanceof Comparable ) { c = (Comparable) nv;
} else {
throw new ClassCastException();
}
Polymorphie:
In Kapitel 3 (Folie 207) hatten wir Polymorphie wie folgt erklärt:
Ein Typsystem heißt polymorph, wenn es Werte bzw.
Objekte gibt, die zu mehreren Typen gehören.
Begriffsklärung: (Subtyp-Polymorphie)
Die Form der Polymorphie in Typsystemen mit Subtypen heißt Subtyp-Polymorphie.
Bemerkung:
Casts sollten soweit möglich vermieden werden.
Beispiel: (inhomogene Listen)
LinkedList ls = new LinkedList();
ls.addLast("letztes Element");
((String) ls.getLast()).indexOf("Elem");
// liefert 8 ls.addLast( new Float() );
// kein Uebersetzungsfehler
Vergleich von Subtyp- und parametrischer Polymorphie:
- Subtyp-Polymorphie:
ermöglicht inhomogene Datenstrukturen;
benötigt keine Instanzierung von Typparametern;
ist sehr flexibel in Kombination mit dynamischer Methodenauswahl.
- parametrische Polymorphie:
vermeidet Laufzeitprüfungen bei homogenen Datenstrukturen (effizienter);
bietet mehr statische Prüfbarkeit (keine Ausnahmen zur Laufzeit).
Beispiel: (Parametrische Listen)
LinkedList<String> ls =
new LinkedList<String>();
ls.addLast("letztes Element");
ls.getLast()).indexOf("Elem"); // liefert 8 ls.addLast( new Float() );
// Übersetzungsfehler!
ls.getLast().indexOf("Elem");
Programmieren mit Schnittstellentypen
Wir demonstrieren die Anwendung von Schnittstellen- typen an zwei charakteristischen Beispiel:
1. Implementierungen einer abstrakten Datenstruktur mit unterschiedlichen Laufzeit- und Speichplatz- eigenschaften:
- Der Anwender der Datenstruktur wählt die Eigenschaften bei der Erzeugung aus.
- Ansonsten benutzt die Anwendung nur die Methoden der Schnittstelle.
Drei Implementierungen für Dictionary 2. Der Anwender eines Objekts kennt nur den
Schnittstellentyp des Objekts, aber nicht dessen Implementierung:
Beobachtermuster
Drei Implementierungen von Dictionary:
In 3.2.2 wurden natürliche Suchbäume zur Realisierung der abstrakten Datenstruktur DICTIONARY betrachtet.
Hier behandeln wir drei andere Suchverfahren:
- A. Binäre Suche in Feldern
Dabei basiert die Verwaltung von Datensätzen auf drei Grundoperationen:
- Einfügen eines Datensatzes in eine Menge von Datensätzen;
- Suchen eines Datensatzes mit Schlüssel k;
- Löschen eines Datensatzes mit Schlüssel k.
In vereinfachter Anlehnung an java.util.Dictionary legen wir folgende Schnittstelle zugrunde:
interface Dictionary { Object get( int key );
void put( int key, Object value );
void remove( int key );
}
Ziel ist es, Datenstrukturen zu finden, bei denen der Aufwand für obige Operationen gering ist.
Entsprechend der Signatur von put gehen wir im Folgenden davon aus, dass ein Datensatz aus einem Schlüssel und einer Referenz vom Typ Object besteht.
class DataSet { int key;
Object data;
DataSet(int k,Object d){ key=k; data=d; } }
Wir betrachten drei Implementierungen des Schnittstellentypes Dictionary und
präsentieren jeweils - die Datenstruktur
- die drei grundlegenden Operationen
- eine einfache Komplexitätsabschätzung.
A. Binäre Suche in Feldern
Lineare Datenstrukturen (Listen, Felder) mit einem Zugriff über den Komponentenindex erlauben
das Auffinden eines Datensatzes durch binäre Suche.
(Hier betrachten wir eine Realisierung mit Feldern ähnlich wie AList aus der Übung.)
Datenstruktur:
Ein Dictionary wird repräsentiert durch ein Objekt mit:
- einer Referenz auf das Feld mit den Datensätzen - der Größenangabe des Feldes (capacity)
- der Anzahl der gespeicherten Datensätze (size)
public class ArrayDictionary
implements Dictionary {
private DataSet[] elems;
private int capacity;
private int size;
public ArrayDictionary() { elems = new DataSet[8];
capacity = 8;
size = 0;
} ...
}
Die Operationen gewährleisten folgende Invariante:
- Die Datensätze sind aufsteigend sortiert.
- Die Schlüssel sind eindeutig.
private int searchIndex( int key ) { /* liefert Index ix von Datensatz mit
Schlüssel k, wobei gilt:
- k == key, wenn so ein Eintrag vorhanden - k ist nächst größere Schlüssel als key,
zu dem Eintrag vorhanden size, sonst
*/ ...
}
Zum Einfügen, Suchen und Löschen benötigt man den Index, an dem die Operation ausgeführt werden soll:
Heraussuchen:
Löschen:
public Object get( int key ) { int ix = searchIndex( key );
if( ix == size || elems[ix].key != key ){
return null;
} else {
return elems[ix].data;
} }
public void remove( int key ) { int ix = searchIndex( key );
if( ix!=size && elems[ix].key == key ){
/* Datensatz löschen */
for( int i = ix+1; i<size; i++ ) { elems[i-1] = elems[i];
}
size--;
} }
Bemerkung:
Bei den Operationen ist eine schnelle Suche wichtig.
public void put( int key, Object value ) { int ix = searchIndex( key );
if( ix == size || elems[ix].key > key ) { /* neuen Datensatz eintragen */
size++;
if( size > capacity ) { DataSet[] newElems =
new DataSet[2*capacity];
for( int i = 0; i<ix; i++ ) { newElems[i] = elems[i];
}
for( int i = ix+1; i<size; i++ ) { newElems[i] = elems[i-1];
}
newElems[ix] = new DataSet(key,value);
elems = newElems;
capacity = 2*capacity;
} else {
for( int i = size-1; i>=ix+1; i-- ) { elems[i] = elems[i-1];
}
elems[ix] = new DataSet( key, value );
}
} else { // elems[ix].key == key elems[ix].data = value;
} }
Einfügen
private int searchIndex( int key ) {
if( size==0 || elems[size-1].key < key ){
return size;
} else {
int ug = 0;
int og = size-1;
/* key <= elems[og].key */
while( ug<=og-2 ) {
int mid = ug + (og-ug)/2;
if( key < elems[mid].key ) { og = mid;
} else { ug = mid;
} }
if( elems[ug].key < key ) { return og;
} else {
return ug;
Suchen
Das Arbeiten mit sortierten Feldern ermöglicht binäre Suche:
- Durch Vergleich mit Schlüssel des Datensatzes in der Feldmitte kann bestimmt werden, ob der gesuchte Satz in der unteren oder oberen Hälfte des Feldes liegt.
- Suche in der bestimmten Hälfte weiter.
Diskussion:
Binäres Suchen verursacht logarithmischen Aufwand: O(log N). Ebenso das Herausholen eines Eintrags aus dem ArrayDictionary.
Einfügen und Löschen benötigen in der gezeigten Variante linearen Aufwand: O(N).
Vorteile:
- einfach und speichersparend zu realisieren - schnelles Heraussuchen von Einträgen
Nachteile:
- Einfügen und Löschen sind vergleichsweise langsam.
B. Balancierte Suchbäume
In 3.2.2 haben wir natürliche binäre Suchbäume betrachtet. Sofern binäre Suchbäume hinreichend gut ausgeglichen (balanciert) sind, ist der Aufwand aller drei Grundoperationen logarithmisch.
Ziel ist es, bei den modifizierenden Operationen den Baum wenn nötig wieder auszubalancieren.
(Wir betrachten hier nur das ausbalancieren nach Einfüge-Operationen.)
Durch zusätzliche Anforderungen bzgl. einer
Verteilung der Blätter und Höhen in Unterbäumen kann man ein Degenerieren verhindern; Aspekte:
- Vorteil: geringer Aufwand für Grundoperationen kann zugesichert werden.
- Nachteil: Strukturinvariante muss erhalten werden.
- Kosten der Strukturerhaltung?
Beispiel: (Strukturerhaltung)
45
57 22
52 42
17
Einfügen von 10 unter Erhaltung von Fast- Vollständigkeit
45 57 22
52 42
17
10
Wegen der Balancierungseigenschaft mussten alle Knoten vertauscht werden.
Begriffsklärung: (AVL-Baum)
Ein binärer Suchbaum heißt AVL-ausgeglichen, höhenbalanciert und ein AVL-Baum, wenn
für jeden Knoten K gilt:
Die Höhe des linken Unterbaums von K
unterscheidet sich von der Höhe des rechten Unterbaums höchstens um 1.
Adelson-Velskij und Landis schlugen folgende Balancierungseigenschaft vor:
Vorgehen:
• Gestaltsanalyse von AVL-Bäumen
• Rotationen auf Suchbäumen
• Datenstruktur für AVL-Bäume
• Heraussuchen
• Balancieren nach Einfügen
• Diskussion
Gestaltsanalyse:
Frage: Hat jeder AVL-Baum logarithmische Höhe?
(Dies ist die Voraussetzung, alle Grundoperationen mit logarithmischen Aufwand realisieren zu können.)
Lemma:
Für die Höhe eines AVL-Baums mit N Knoten gilt:
h 2 * log (N +1) + 1
Beweis:
siehe Vorlesung
Eine Rotation ist ein lokale Reorganisation eines Suchbaums, bei der die Suchbaumeigenschaft erhalten bleibt, die Höhen der Unterbäume aber ggf. verändert werden.
Rotation nach rechts (nach links entsprechend):
Rotationen auf Suchbäumen:
Y X
A B
C
Y X
A
B C
Es gilt:
- alle Schlüssel aus A sind echt kleiner als X - alle Schlüssel aus B sind echt größer als X
und echt kleiner als Y
- alle Schlüssel aus C sind echt größer als Y - X < Y
Doppelrotation links-rechts (rechts-links entsprechend):
Z Y
A
D
X
X
B C
Z
C D
Y
A B
Auch bei der Doppelrotation bleibt die Suchbaum- Eigenschaft erhalten.
Datenstruktur für AVL Bäume:
Zur Realisierung von AVL-Bäumen gehen wir von der Implementierung der natürlichen Suchbäume aus:
- Die Baumknoten bekommen ein zusätzliches Attribut bf (balance factor), in dem die Höhendifferenz von linkem und rechtem Unterbaum gespeichert wird.
- Die Operationen zum Einfügen und Löschen müssen
Ein Dictionary wird repräsentiert durch ein Objekt, das eine Referenz auf einen Binärbaum enthält:
• Der leere Binärbaum wird durch die null-Referenz repräsentiert (leeres Dictionary).
• Jeder Knoten eines nichtleeren Binärbaums wird durch ein Objekt vom Typ AVLNode repräsentiert mit Instanzvariablen für:
- den Schlüssel - die Daten
- die Referenz auf das linke Kind - die Referenz auf das rechte Kind - die Höhendifferenz bf
• Die Baumknoten sind gekapselt und können von außen nur indirekt über die Grundoperationen manipuliert werden.
Datenstruktur-Invarianten:
- Schlüssel kommen nicht doppelt vor.
- Die Binärbäume sind Suchbäume.
- Die Höhendifferenz ist korrekt belegt.
class AVLTreeDictionary implements Dictionary {
private AVLNode root;
private static class AVLNode { private int key;
private Object data;
private AVLNode left, right;
private int bf;
private AVLNode( int k, Object d ) { key = k;
data = d;
} }
public AVLTreeDictionary() {
root = null; // leeres Dictionary }
public Object get( int key ) {...}
private AVLNode searchNode( AVLNode current, int key) {...}
public void put(int key, Object value){...}
private AVLNode insertNode( AVLNode current, int key, Object v ){...}
private AVLNode rotate(AVLNode current){...}
public void remove( int key ) {
public Object get( int key ) {
AVLNode tn = searchNode(root,key);
if( tn == null ) { return null;
} else {
return tn.data;
} }
private AVLNode searchNode(
AVLNode current, int key) {
if( current!=null && key != current.key ) { if( current.key > key ) {
return searchNode( current.left, key );
} else { // current.key < key
return searchNode( current.right, key );
} }
return current;
}
Heraussuchen:
- Ist der Wurzelschlüssel gleich dem gesuchten Schlüssel, terminiert das Verfahren.
- Ist der Wurzelschlüssel größer als der gesuchte Schlüssel, suche im linken Unterbaum weiter.
- Ist der Wurzelschlüssel kleiner als der gesuchte Schlüssel, suche im rechten Unterbaum weiter.
Einfügen:
Entwicklung des Algorithmus in 4 Schritten:
1. Einfügen ohne Aktualisieren von bf und Rotation 2. Aktualisieren von bf, aber ohne Rotation
3. Aktualisieren von bf mit Aufruf der Rotation 4. Rotation mit Aktualisieren von bf an den
rotierten Knoten
1. Einfügen ohne Aktualisieren von bf und Rotation : - Neue Knoten werden immer als Blätter eingefügt.
- Die Position des Blattes wird durch den Schlüssel des neuen Knotens festgelegt.
- Beim Aufbau eines Baumes ergibt der erste Knoten die Wurzel.
- Ein Knoten wird in den linken Unterbaum der Wurzel eingefügt, wenn sein Schlüssel kleiner ist als der Schlüssel der Wurzel; in den rechten, wenn er größer. Dieses Verfahren wird rekursiv fortgesetzt, bis die Einfügeposition bestimmt ist.
45
57 22
65 52
42 17
49
Beispiel:
Einfügen von 33:
33
class AVLTreeDictionary implements Dictionary {
...
private AVLNode root;
...
public void put( int key, Object value ){
if( root == null ) {
root = new AVLNode(key, value);
} else {
AVLNode res = insertNode(root,key,value);
if( res!=null ) root = res;
} } ...
}
void insertNode(
AVLNode current, int key, Object v ) { // pre: current != null
//
if( key < current.key ) {
if ( current.left == null ) {
current.left = new AVLNode(key,v);
} else {
insertNode( current.left, key, v);
}
} else if( key > current.key ) { if ( current.right == null ) {
current.right = new AVLNode(key,v);
} else {
insertNode( current.right, key, v);
}
} else { // key == current.key current.data = v;
} }
Rekursives Aufsuchen der Einfügestelle und Einfügen:
2. Aktualisieren von bf, aber ohne Rotation
Algorithmisches Vorgehen:
- Einfügen wie oben.
- Aktualisieren von bf soweit nötig: Der Höhendifferenz bf kann sich nur bei Knoten ändern, die auf dem Pfad von der Wurzel zum eingefügten Knoten liegen.
Nur an diesen Knoten kann die AVL-Eigenschaft verletzt werden.
- Bestimmen des kritischen Knotens KK; das ist der nächste Elternknoten zum eingefügten Knoten mit
| bf | = 2.
- Rotiere bei KK, Rotationstyp ergibt sich aus Pfad von eingefügtem Knoten zu KK.
Wir werden zeigen, dass durch die Rotation der Unterbaum mit Wurzel KK die gleiche Höhe erhält, die er vor dem Einfügen hatte. Die Balancierungs- faktoren an Knoten oberhalb von KK brauchen also nicht aktualisiert zu werden.
private boolean insertNode
( AVLNode current, int key, Object v ){
/* pre: current != null
ens: result==true, wenn
h(current) > old(h(current)) result==false, sonst
*/
if( key < current.key ) {
if( current.left == null ) {
current.left = new AVLNode(key,v);
current.bf++;
// |current.bf| < 2 return (current.bf>0);
} else {
if( insertNode(current.left,key,v) ){
current.bf++;
return (current.bf>0);
} else {
return false;
} }
} else if( key > current.key ) {
... // symmetrisch auf rechter Seite } else { // key == current.key
current.data = v;
return false;
}
Einfügen mit Aktualisieren von bf:
Bemerkung:
• Die obige Fassung veranschaulicht die Vorgehens- weise, auf dem „Rückweg“ von einem rekursiven Abstieg Operationen auszuführen.
• Die obige Fassung wird so nicht benötigt, da
bf nur bis zu kritischen Knoten zu aktualisieren ist.
Ist der kritische Knoten gefunden, wird rotiert und damit die Aktualisierungen oberhalb unnötig.
Problem: Die Rotation macht es nötig, den Elternknoten des kritischen Knotens zu modifizieren.
Idee: Statt true/false liefert die Einfüge-Operation:
- null, wenn sich die Höhe geändert hat.
- Die Referenz auf den möglicherweise rotierten
Unterbaum, wenn sich die Höhe nicht geändert hat;
deshalb ist ggf. root zu modifizieren:
public void put( int key, Object val ) { if( root == null ) {
root = new AVLNode(key, value);
} else {
AVLNode res = insertNode(root,key,val);
if( res!=null ) { root = res;
} } }
3. Einfügen mit Aktualisieren von bf und Rotation:
private AVLNode insertNode
( AVLNode current, int key, Object v ) {
if( key < current.key ) {
if( current.left == null ) {
current.left = new AVLNode(key,v);
current.bf++;
// |current.bf| < 2
return (current.bf>0) ? null : current;
} else {
AVLNode res =
insertNode(current.left,key,v);
if( res == null ) { current.bf++;
if( current.bf < 2 ) {
return (current.bf>0)?null:current;
} else {
return rotate( current );
}
} else {
current.left = res;
return current;
} }
} else if( key > current.key ) {
... // symmetrisch auf rechter Seite } else { // key == current.key
current.data = v;
return current;
4. Rotation mit Aktualisieren von bf:
Y X
A B
C vor Einfügen:
h h
h+1 h
h+2
0
1
Wir betrachten die Situation, dass bf im kritischen Knoten KK auf 2 gestiegen ist, also links eingefügt wurde. Die Unterbäume von KK bezeichnen wir mit li, li.li, li.re etc.
Da h(li) = h(re)+2, kann der Wurzelknoten von li nicht der neu eingefügte Knoten KN sein.
Es gibt vier unterschiedliche Fälle:
- KN ist in li.li eingefügt (rechts-Rotation)
- KN ist in li.re eingefügt; bf ist abhängig davon, ob KN neue Wurzel von li.re (links-rechts-Rotation) KN in li.re.li eingefügt (links-rechts-Rotation) KN in li.re.re eingefügt (links-rechts-Rotation) Fall: links-links
nach Einfügen:
Y X
A B
C
h+1 h
h+2 h
h+3
1
2
Y X
A
B C
h+1
h h
h+2
h+1
0
0
nach Rotation:
(der Fall rechts-rechts geht analog)
h h
h h+1
h+2
0
1
Fall: links-rechts:
Beachte:
Die Höhe nach der Rotation ist gleich der Höhe vor dem Einfügen. Damit wird die AVL-
Eigenschaft der Baumteile oberhalb des kritischen Knotens nicht beeinflusst.
vor Einfügen:
Z Y
A
D X
B C
h-1 h-1
0
0
1
0
Z 1
Y
nach Einfügen links-rechts:
0 0
1
0 0 nach Rotation:
X
Z Y
0 1
2
-1
Z 2
Y
X 0
0
(der Fall rechts-links geht analog)
nach Einfügen links-rechts-links:
h h-1 h
h+1 -1
h
h+2
0 h+1 nach Rotation:
X
Z
C D
Y
A B
h+1 h
h h+2
h+3
-1
Z 2
Y
A
D X
B C
h h-1
1
0
(der Fall rechts-links-rechts geht analog)
nach Einfügen
links-rechts-rechts:
h-1 h h
h+1 0
h
h+2
1 h+1 nach Rotation:
X
Z
C D
Y
A B
h+1 h
h h+2
h+3
-1
Z 2
Y
A
D X
B C
h h-1
-1
0
private AVLNode rotate( AVLNode current ) {
// pre: current != null && |current.bf| == 2 //
if( current.bf == 2 ) {
AVLNode cleft = current.left;
if( cleft.bf == 1 ) { // Variante LL current.left = cleft.right;
current.bf = 0;
cleft.right = current;
cleft.bf = 0;
return cleft;
} else { // LR-Varianten
AVLNode clright = cleft.right;
current.left = clright.right;
cleft.right = clright.left;
clright.left = cleft;
clright.right = current;
if( clright.bf == 1 ) { // LR(a) current.bf = -1;
cleft.bf = 0;
} else if( clright.bf == -1 ) { // LR(b) current.bf = 0;
cleft.bf = 1;
} else { // degenerierter Fall current.bf = 0;
cleft.bf = 0;
}
clright.bf = 0;
return clright;
}
} else { // current.bf == -2 ) ... // symmmetrisch fuer rechts }
Diskussion:
Beim Einfügen eines Knotens hat der rebalan- cierte Unterbaum stets die gleiche Höhe wie vor dem Einfügen:
- Der restliche Baum wird nicht beeinflusst.
- Höchstens eine Rotation wird benötigt.
Beim Löschen können ungünstigsten Falls so viele Rotationen erforderlich sein, wie es Knoten auf
dem Pfad von der Löschposition bis zur Wurzel gibt.
Da der Aufwand für eine Rotation aber konstant ist, ergeben sich maximal O(log N) Operationen.
C. Hashing/Streuspeicherung
Anstatt durch schrittweises Vergleichen von Schlüsseln auf einen Datensatz zuzugreifen, versucht man bei Hash- oder Streuspeicher- verfahren aus dem Schlüssel die
Positionsinformation des Datensatzes (z.B.
den Feldindex) zu berechnen.
Begriffsklärung: (Hashfunktion, -tabelle)
Seien
- S die Menge der möglichen Schlüsselwerte (Schlüsselraum) und
- A die Menge von Adressen in einer Hashtabelle (im Folgenden ist A immer die Indexmenge 0 .. m-1 eines Feldes).
Eine Hashfunktion h: S A ordnet jedem Schlüssel eine Adresse in der Hashtabelle zu.
Als Hashtabelle (HT) der Größe m bezeichnen wir einen Speicherbereich, auf den über die Adressen aus A mit konstantem Aufwand (unabhängig von m) zugegriffen werden kann.
Enthält S weniger Elemente als A, kann h injektiv sein:
Für alle s,t in S: s t => h(s) h(t)
d.h. die Hashfunktion ordnet jedem Schlüssel eine eineindeutige Adresse zu. Dann ist perfektes Hashing möglich.
Andernfalls können Kollisionen auftreten:
Begriffsklärung: (Kollision, Synonym)
Zwei Schlüssel s,t kollidieren bezüglich einer Hashfunktion h, wenn h(s) = h(t).
Die Schlüssel s und t nennt man dann Synonyme.
Die Menge der Synonyme bezüglich einer Adresse a aus A heißt die Kollisionsklasse von a.
Ist schon ein Datensatz mit Schlüssel s in der
Hashtabelle gespeichert, nennt man einen Datensatz mit einem Synonym von s einen Überläufer.
Eine Hashfunktion soll
- sich einfach und effizient berechnen lassen (konstanter Aufwand bzgl. S)
- zu einer möglichst gleichmäßigen Belegung der Hashtabelle führen
- möglichst wenige Kollisionen verursachen Anforderungen an Hashfunktionen:
Hashverfahren unterscheiden sich
• durch die Hashfunktion
• durch die Kollisionsauflösung:
- extern: Überläufer werden in Datenstrukturen außerhalb der Hashtabelle gespeichert.
- offen: Überläufer werden an noch offenen
Positionen der Hashtabelle gespeichert.
• durch die Wahl der Größe von der Hashtabelle:
- statisch: Die Größe wird bei der Erzeugung festgelegt und bleibt unverändert.
- dynamisch: Die Größe kann angepasst werden.
Wir betrachten im Folgenden eine Realisierung einer statischen Hashtabelle mit externer Kollisionsauf- lösung durch ein Dictionary mit binärer Suche.
Klassifikation von Hashverfahren:
Hashfunktion:
Entscheidend ist, dass die Hashfunktion die Schlüssel gut streut. Verbreitetes Verfahren:
- Wähle eine Primzahl als Hashtabellen-Größe.
- Wähle den ganzzahligen Divisionsrest als Hashwert:
private int hash( int key ) {
return Math.abs(key) % hashtable.length;
}
Datenstruktur:
Bemerkung:
Schlecht wäre beispielsweise eine Wahl von m = 2 als Tabellengröße bei dem Divisionsrest-Verfahren, da bei Binärdarstellung der Schlüssel dann nur die letzten i Bits relevant sind.
i
class HashDictionary implements Dictionary { int[] hashtable;
Object[] datatable;
private Dictionary[] overflowtable;
public HashDictionary( int tabsize ) { /* tabsize sollte eine Primzahl sein */
hashtable = new int[tabsize];
datatable = new Object[tabsize];
overflowtable = new Dictionary[tabsize];
}
private int hash( int key ) { ... } public Object get( int key ) { ... }
Wir realisieren eine Hashtabelle als Implementierung der Schnittstelle Dictionary:
Sei s ein Schlüssel, h(s) sein Hashwert.
Der Datenstruktur HashDictionary liegen folgende Invarianten zugrunde:
- Die Hashtabelle enthält den Datensatz zu s, wenn
hashtable[ h(s) ] == key
datatable[ h(s) ] != null
Die Daten liefert dann datatable[ h(s) ] .
- Alle eingetragenen Elemente der Kollisionsklasse zu h(s) befinden sich
in der Hashtabelle mit Index h(s) oder
in overflow[h(s)].
Wegen Löschens ist es möglich, dass die
Hashtabelle zu h(s) keinen Eintrag hat, sich trotzdem aber Einträge in der Überlauftabelle zu h(s) befinden!
Löschen:
public void remove( int key ) { int hix = hash(key);
if( hashtable[hix] == key
&& datatable[hix] != null ) { datatable[hix] = null;
} else if( overflowtable[hix] != null ) { overflowtable[hix].remove(key);
} }
Suchen:
public Object get( int key ) { int hix = hash(key);
if( hashtable[hix] == key
&& datatable[hix] != null ) { return datatable[hix];
} else if( overflowtable[hix] == null ) { return null;
} else {
Object v = overflowtable[hix].get(key);
if( datatable[hix] == null ) { datatable[hix] = v;
overflowtable[hix].remove(key);
}
return v;
} }
Einfügen:
public void put ( int key, Object value ) { if( value != null ) {
int hix = hash(key);
if( datatable[hix] == null ) { hashtable[hix] = key;
datatable[hix] = value;
} else if( hashtable[hix] == key ) { datatable[hix] = value;
} else {
if( overflowtable[hix] == null ) { overflowtable[hix] =
new BintreeDictionary();
}
overflowtable[hix].put(key,value);
} } }
Bemerkung:
Diskussion:
Die Komplexität der Operationen einer Hashtabelle hängt ab von:
- der Hashfunktion und dem Füllungsgrad der Tabelle - dem Verfahren zur Kollisionsauflösung
Bei guter Hashfunktion und kleinem Füllungsgrad kommt man im Mittel mit konstantem Aufwand aus.
Hashverfahren sind ein Beispiel dafür, dass man sich nicht immer für Algorithmen mit asymptotisch gutem Verhalten interessiert.
Anwendung von Hashverfahren:
Hashverfahren werden auch verwendet um Schlüssel- räume zu vereinfachen. Wir betrachten hier
Anwendungen mit Zeichenreichen als Schlüssel.
Zeichenreihen als Schlüssel:
- Vorteile: bzgl. Anwendung der sich direkt ergebende Schlüsseltyp; nicht längenbeschränkt.
Beispiel: (Strings als Schlüssel)
In Bezeichnerumgebungen (vgl. SE 1, Folie 106) und Deklarationstabellen von Übersetzern sind die Schlüssel in natürlicherweise Zeichenreihen.
Durch Hashing wird jedem Bezeichner eine natürliche Zahl als Schlüssel zugeordnet. Unter diesem Zahl- schlüssel wird die Deklarationsinformation verwaltet.
Die Hashfunktion wird dabei benutzt, um komplexere Schlüssel in einfachere, meist ganzzahlige Schlüssel abzubilden.
Da Injektivität meist nur annäherungsweise erreicht werden kann, braucht man Kollisionsauflösung. Meist verwendet man dazu offene Kollisionsauflösung.
Bemerkung:
Die praktische Bedeutung von Hashverfahren zur
Schlüsselvereinfachung wird auch durch die Methode
/** Returns a hash code value for the object.
* This method is supported for the benefit
* of hashtables such as those provided by
* java.util.Hashtable ... */
public native int hashCode();
in der Java Klasse Object verdeutlicht.
Beobachtermuster:
Als Beispiel einer Klasse, die Objekte nur über deren Schnittstelle anspricht, betrachten wir eine Anwendung des Beobachtermusters.
Aktie
name: String kurswert: int
Beobachter
void steigen(Aktie a) void fallen(Aktie a)
* *
Boersianer1 Boersianer2
Bei Realisierung der Klasse Aktie ist nur bekannt, dass die Beobachter über das Steigen und Fallen des Aktienkurses informiert werden wollen.
Wie Beobachter auf Änderungen reagieren, ist nicht bekannt. Die Klassen können also getrennt entwickelt werden.
Bemerkung:
public class Aktie { private String name;
private int kursWert;
private ArrayList beobachterListe;
Aktie( String n, int anfangsWert ){
name = n;
kursWert = anfangsWert;
beobachterListe = new ArrayList();
}
public String getName(){
return name;
}
public int getKursWert(){
return kursWert;
}
// Fortsetzung nächste Folie interface Beobachter {
void steigen( Aktie a );
void fallen( Aktie a );
} // Fortsetzung nächste Folie
Da über die Beobachter nichts bekannt ist, realisiert man sie sinnvollerweise durch einen Schnittstellentyp:
Die Assoziation zwischen Aktien und Beobachtern implementieren wir durch eine Liste in der Klasse Aktie, die alle Beobachter der Klasse enthält:
// Fortsetzung von voriger Folie
void setKursWert( int neuerWert ){
int alterWert = kursWert;
kursWert = neuerWert>0 ? neuerWert : 1 ; ListIterator it =
beobachterListe.listIterator();
if( kursWert > alterWert ) { while( it.hasNext() ){
Beobachter b = (Beobachter)it.next();
b.steigen( this );
}
} else {
while( it.hasNext() ){
Beobachter b = (Beobachter)it.next();
b.fallen( this );
} } }
public
void anmeldenBeobachter( Beobachter b ) { beobachterListe.add( b );
}
Beim Setzen des Kurswertes werden auch die Beobachter benachrichtigt:
Zur Illustration von Beobachterimplementierungen betrachten wir einen Boersianer der
- von der beobachteten Aktien kauft, wenn deren Kurs unter 300 Euro fällt und er noch keine besitzt, - verkauft, wenn der Kurs über 400 Euro steigt.
public class Boersianer1 implements Beobachter{
private boolean besitzt = false;
void fallen( Aktie a ) {
if( a.getKursWert() < 300 && !besitzt ) { System.out.println("Kauf "+a.getName());
besitzt = true;
} }
void steigen( Aktie a ) {
if( a.getKursWert() > 400 && besitzt ) { System.out.print("Verkauf "+a.getName());
System.out.println();
besitzt = false;
} } }
Anwendungsfragment:
...
Aktie vw = new Aktie("VW", 354);
Beobachter peter = new Boersianer1();
vw.anmeldeBeobachter( peter );
...
5.3.3 Vererbung
Begriffsklärung: (Vererbung)
Vererbung (engl. inheritance) im engeren Sinne bedeutet, dass eine Klasse Programmteile von einer anderen übernimmt.
Die erbende Klasse heißt Subklasse, die vererbende Klasse heißt Superklasse.
In Java sind die ererbten Programmteile Attribute, Methoden und geschachtelte Klassen, nicht vererbt werden Klassenattribute, Klassenmethoden und Konstruktoren.
In Java ist die Subklasse immer ein Subtyp des Typs der Superklasse.
Vererbung unterstützt Spezialisierung durch:
- Hinzufügen von Attributen (Zustandserweiterung) - Hinzufügen von Methoden (Erweiterung der
Funktionalität)
- Anpassen, Erweitern bzw. Reimplementieren
Beispiel: (Vererbung)
class Person { String name;
int gebdatum; /* Form JJJJMMTT */
void drucken() {
System.out.println("Name: "+ this.name);
System.out.println("Gebdatum: "+gebdatum);
}
boolean hat_geburtstag ( int datum ) { return (gebdatum%10000)==(datum%10000);
}
Person( String n, int gd ) { name = n;
geburtsdatum = gd;
} }
class Student extends Person { int matrikelnr;
int semester;
void drucken() { super.drucken();
System.out.println("Matnr: "+ matrikelnr);
System.out.println("Semzahl: "+ semester);
}
Student(String n,int gd,int mnr,int sem) { super( n, gd );
matrikelnr = mnr;
semester = sem;
} }