• Keine Ergebnisse gefunden

5.3 Subtypen und Vererbung

N/A
N/A
Protected

Academic year: 2022

Aktie "5.3 Subtypen und Vererbung"

Copied!
106
0
0

Wird geladen.... (Jetzt Volltext ansehen)

Volltext

(1)

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

(2)

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

(3)

Figur

Ellipse Vieleck

Viereck Dreieck Kreis

Parallelogramm

Raute Rechteck

Quadrat Person

Student Angestellte

Wissenschaftl.

Angestellte

Verwaltungs- angestellte

(4)

• 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.

(5)

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.

(6)

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:

(7)

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

(8)

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 {

(9)

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.

(10)

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 );

(11)

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 );

} }

(12)

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

(13)

- 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

(14)

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.

(15)

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.

(16)

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

(17)

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 );

}

(18)

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

(19)

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();

}

(20)

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.>

}

(21)

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);

} }

(22)

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:

(23)

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

(24)

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:

(25)

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

(26)

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:

(27)

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.

(28)

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!

(29)

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] ;

(30)

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:

(31)

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 ;

(32)

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

(33)

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 );

} }

(34)

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:

(35)

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();

}

(36)

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

(37)

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");

(38)

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

(39)

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; } }

(40)

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)

(41)

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:

(42)

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.

(43)

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

(44)

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.

(45)

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.)

(46)

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

(47)

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:

(48)

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

(49)

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

(50)

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

(51)

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.

(52)

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 ) {

(53)

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.

(54)

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.

(55)

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;

} } ...

}

(56)

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:

(57)

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.

(58)

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:

(59)

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:

(60)

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;

(61)

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

(62)

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)

(63)

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

(64)

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)

(65)

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)

(66)

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

(67)

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 }

(68)

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.

(69)

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:

(70)

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:

(71)

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;

}

(72)

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:

(73)

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);

} }

(74)

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;

} }

(75)

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);

} } }

(76)

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.

(77)

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.

(78)

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:

(79)

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:

(80)

// 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:

(81)

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 );

...

(82)

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

(83)

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;

} }

Referenzen

ÄHNLICHE DOKUMENTE

Jede Klasse in Java ist von der Klasse Objekt abgeleitet, ohne dass wir dies explizit definieren müssen.. Wie wir in der Abbildung sehen, können wir in Java eine Klasse Person

§8.1.3: The optional extends clause in a class declaration specifies the direct superclass of the current class.. A class is said to be a direct subclass of the class

Deshalb gibt es einen Zugriffsbereich für Vererbung, der alle Subklassen einer Klasse umfasst. Programmelemente, die als geschützt deklariert

erbt: code, price, available, print ergänzt: author, title, Konstruktor überschreibt: print. Article code price available() print() Article(

n Mehrfachvererbung: Klasse kann von mehr als einer Superklasse abgeleitet sein (nicht im Sinne von Interfaces in Java).. n Late Binding: in Java immer; in C++ steuerbar

► Vererbung: Eine Klasse kann Merkmale von einer Oberklasse übernehmen..

Auf das Attribut besitzer soll lesend zugegriffen werden können. Schreibe dazu in der Klasse BALL eine sondierende Methode für besitzer und ändere den Quelltext von TEST

Auf das Attribut besitzer soll lesend zugegriffen werden können. Schreibe dazu in der Klasse BALL eine sondierende Methode für besitzer und ändere den Quelltext von TEST