5.3 Subtypen und Vererbung

Werbung
5.3 Subtypen und Vererbung
Dieser Abschnitt erläutert die Konzepte der
Subtypbildung und Vererbung.
Überblick:
• Klassifizieren von Objekten
• Subtypen und Schnittstellentypen
• Vererbung
5.3.1 Klassifizieren von Objekten
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.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
104
Beispiele: (Klassifikationen)
Wirbeltiere
Fische
Lurche
Vögel
Reptilien Säugetiere
Wale
Primaten
Paarhufer
_ ist_ein _ (Vogel ist ein Wirbeltier)
Recht
Öffentliches
Recht
Privatrecht
Bürgerliches
Recht
08.01.09
Handelsrecht
Kirchenrecht
Urheberrecht
© A. Poetzsch-Heffter, TU Kaiserslautern
105
Figur
Ellipse
Vieleck
Kreis
Viereck
Dreieck
Parallelogramm
Raute
Rechteck
Quadrat
Person
Student
Angestellte
Wissenschaftl.
Angestellte
08.01.09
Verwaltungsangestellte
© A. Poetzsch-Heffter, TU Kaiserslautern
106
Bemerkung:
• 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.
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.
- Die Klassen werden hierarchisch geordnet.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
107
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
Begriffsklärung: (Abstraktion)
... das Heraussondern des unter einem bestimmten
Gesichtspunkt Wesentlichen vom Unwesentlichen.
[Meyers großes Taschenlexikon]
Abstraktion geht also vom Speziellen zum
Allgemeinen.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
108
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 gemeinsamen 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:
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
109
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
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
110
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 {
... // wie oben
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
111
Spezialisierung
Begriffsklärung: (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).
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.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
112
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 );
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
113
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 );
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
114
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 überschriebene 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
als speziellere Objekte.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
115
- 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
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
116
Klassifikationen und Typisierung
Typ beschreibt Eigenschaften von Werten
bzw. Objekten. Annahme bisher:
Kein Objekt bzw. Wert gehört zu mehr als
einem (nicht-parametrisierten) Typ.
Ansatz:
- Realisiere jede Klasse/jeden Begriff einer
Klassifikation im Programm durch einen Typ.
- Führe eine partielle Ordnung ≤ (vgl. Folie 3.196)
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.
Wenn S <T und es kein U mit S < U < T gibt, dann
heißt S ein direkter Subtyp von T.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
117
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.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
118
Beispiel: (Substituierbarkeit)
Folgende Anweisungen sind typkorrektes Java:
Object ov
= "Ein String ist auch ein Object";
Person[] p =
new Person[4];
p[0]
new Student(...);
=
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.
Schnittstellentypen in Java
In Java gibt es zwei Arten von benutzerdefinierten
Typen:
- Klassentypen
- Schnittstellentypen
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
119
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 );
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
120
interface Druckbar {
void drucken();
}
interface Farbe {
byte gelb = 0;
byte gruen = 1;
byte blau = 2;
}
Subtypbildung in Java
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.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
121
Beispiel: (Subtyprelation bei Schnittstellen)
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 {
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):
interface Angestellte
extends Person, Druckbar {
int getEinstellungsdatum();
String getGehaltsklasse();
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
122
Syntax der Klassendeklartion:
<Modifikatiorenlist> class <Klassenname>
[ extends <Klassenname> ]
[ implements <Liste von Schnittstellennamen> ]
{
<Liste von Attribut-, Konstruktor-, Methodendekl.>
}
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
der implements-Klausel angegeben sind.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
123
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);
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
124
Zusammenfassung: Typen & Subtyp-Ordnung:
Typen:
• elementare Datentypen: int, char, byte, ....
• Schnittstellentypen
• Klassentypen
Referenztypen
• Feldtypen
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.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
125
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, WissAngestellte, VerwAngestellte) werden durch Klassen
realisert, alle anderen durch Schnittstellen.
Object
Druckbar
Person
Student
Angestellte
WissAngestellte
08.01.09
ist Subtyp
ist Subklasse
VerwAngestellte
© A. Poetzsch-Heffter, TU Kaiserslautern
126
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 { ... }
2. Variante:
Außer des Typs Druckbar realisieren wir alle Typen
durch Klassen:
class Person
{ ... }
interface Druckbar { ... }
class Student extends Person
implements Druckbar { ... }
class Angestellte extends Person
implements Druckbar { ... }
class WissAngestellte
extends Angestellte { ... }
class VerwAngestellte
extends Angestellte { ... }
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
127
Das Klassendiagramm zur 2. Variante:
ist Subtyp
ist Subklasse
Object
Druckbar
Person
Student
Angestellte
WissAngestellte
VerwAngestellte
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.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
128
Dynamische Methodenauswahl
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:
Beispiel: (Methodenaufruf)
Welche Methode soll ausgeführt werden:
1. Auswahl zwischen Methode der Super- und
Subklasse:
Frame f = new MemoFrame();
...
f.setBackground( Color.red );
2. Auswahl zwischen Methode verschiedener
Subklassen:
static void alle_drucken (Druckbar[] df) {
int i;
for( i =0; i<df.length; i++) {
df[i].drucken();
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
129
Begriffsklärung: (dynamische Meth.auswahl)
Die auszuführende Methode zu einem Methodenaufruf:
<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 Methodenauswahl oder dynamisches Binden (engl. dynamic
method binding).
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.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
130
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!
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
131
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] ;
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
132
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[]
length:
0:
1:
08.01.09
2
•
•
© A. Poetzsch-Heffter, TU Kaiserslautern
133
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:
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
08.01.09
= iv.intValue() + 23 ;
© A. Poetzsch-Heffter, TU Kaiserslautern
134
Javas Wrapper-Klassen sind im Paket java.lang
definiert. Folgendes Diagramm zeigt die Subtypbeziehungen:
Object
Comparable
Boolean
Number
Character
Byte
Short
Long
Float
Double
Integer
BigInteger
BigDecimal
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
135
Boxing, Unboxing, Autoboxing
Die Umwandlung von Werten der elementaren Datentypen in Objekte der Wrapper-Klassen nennt man
Boxing, die umgekehrte Umwandlung Unboxing.
Wo nötig führt Java 6 Boxing und Unboxing automatisch durch (Autoboxing).
Beispiel: (Autoboxing)
Das folgende Programmfragment mit Autoboxing
List<Integer> ints = new ArrayList<Integer();
ints.add(1);
int i = ints.get(0);
ist eine Abkürzung von:
List<Integer> ints = new ArrayList<Integer();
ints.add(new Integer(1));
int i = ints.get(0).intValue();
Bemerkungen:
1. Autoboxing erlaubt es, Werte elementarer Datentypen einfacher zusammen mit parameterischen
Typen zu benutzen; kostet aber Zeit und Speicher.
2. Vorsicht: Objekte, die den gleichen Wert einpacken, müssen nicht gleich sein.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
136
Beispiel: (Comparable)
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 );
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
137
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)
// bv == 0
sv;
Typkonvertierungen von Datentypen mit kleinerem
Wertebereich in solche mit größerem Wertebereich
werden automatisch durchgeführt:
3.4 + 7
ist äquivalent zu:
3.4 + (double) 7
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
138
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.
Beispiel: (Konvertieren von Referenztypen)
Number
nv
= new Integer(7);
Object
ov
= (Object) nv;
Number
nv1 = (Object) nv;
//
Integer
iwv = nv;
//
// upcast
Integer iwv1 = (Integer) nv; // downcast
Float
fwv = (Float) nv;
//
Comparable c = (Comparable) nv; //
String
sv
= (String) nv;
//
Java bietet außerdem den Operator instanceof
zum Typtesten an:
Comparable c;
if( nv instanceof Comparable ) {
c = (Comparable) nv;
} else {
throw new ClassCastException();
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
139
Bemerkung:
Casts sollten soweit möglich vermieden werden.
Polymorphie:
In Kapitel 3 (Folie 3.133) 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.
Beispiel: (inhomogene Listen)
LinkedList ls = new LinkedList();
ls.addLast("letztes Element");
((String) ls.getLast()).indexOf("Elem");
// liefert 8
ls.addLast( new Float(3.3) );
// kein Uebersetzungsfehler
((String) ls.getLast()).indexOf("Elem");
// Laufzeitfehler
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
140
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(3.3) );
// Übersetzungsfehler!
ls.getLast().indexOf("Elem");
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
141
Programmieren mit Schnittstellentypen
Wir demonstrieren die Anwendung von Schnittstellentypen an zwei charakteristischen Beispiel:
1. Implementierungen einer abstrakten Datenstruktur
mit unterschiedlichen Laufzeit- und Speichplatzeigenschaften:
- 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
- B. Balancierte Suchbäume
- C. Hashing/Streuspeicherung
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
142
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; }
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
143
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)
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
144
Die Operationen gewährleisten folgende Invariante:
- Die Datensätze sind aufsteigend sortiert.
- Die Schlüssel sind eindeutig.
public class ArrayDictionary
implements Dictionary
{
private DataSet[] elems;
private int
capacity;
private int
size;
public ArrayDictionary() {
elems = new DataSet[8];
capacity
= 8;
size
= 0;
}
...
}
Zum Einfügen, Suchen und Löschen benötigt man den
Index, an dem die Operation ausgeführt werden soll:
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
*/
...
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
145
Heraussuchen:
public Object get( int key ) {
int ix = searchIndex( key );
if( ix == size || elems[ix].key != key ){
return null;
} else {
return elems[ix].data;
}
}
Löschen:
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.
Deshalb konzentrieren sich die algorithmischen
Untersuchungen auf diese Operation.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
146
Einfügen
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;
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
147
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.
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;
}
} }
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
148
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.)
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
149
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)
Einfügen von 10 unter
Erhaltung von FastVollständigkeit
45
22
17
08.01.09
57
42
52
© A. Poetzsch-Heffter, TU Kaiserslautern
150
42
17
10
52
22
45
57
Wegen der Balancierungseigenschaft mussten
alle Knoten vertauscht werden.
Adelson-Velskij und Landis schlugen folgende
Balancierungseigenschaft vor:
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.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
151
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
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
152
Rotationen auf Suchbäumen:
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):
Y
X
X
Y
C
A
B
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
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
153
Doppelrotation links-rechts (rechts-links entsprechend):
Z
X
Y
Y
D
Z
X
A
A
B
B
C
D
C
Auch bei der Doppelrotation bleibt die SuchbaumEigenschaft 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
angepasst werden (ggf. wird dazu auch die Datenstruktur erweitert).
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
154
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.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
155
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 ) {
throw RuntimeException("not available");
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
156
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.
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;
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
157
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.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
158
Beispiel:
Einfügen von 33:
45
22
17
57
42
33
52
65
49
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;
}
}
...
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
159
Rekursives Aufsuchen der Einfügestelle und Einfügen:
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;
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
160
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 Balancierungsfaktoren an Knoten oberhalb von KK brauchen also
nicht aktualisiert zu werden.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
161
Einfügen mit Aktualisieren von bf:
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;
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
162
Bemerkung:
• Die obige Fassung veranschaulicht die Vorgehensweise, 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.
3. Einfügen mit Aktualisieren von bf und Rotation:
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;
}
} }
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
163
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;
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
164
4. Rotation mit Aktualisieren von bf:
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
h+2
vor Einfügen:
Y
h+1
X
0
1
h
C
h
h
A
08.01.09
B
© A. Poetzsch-Heffter, TU Kaiserslautern
165
nach Einfügen:
h+3
Y
h+2
X
2
h
1
C
h+1
h
A
nach Rotation:
h+2
h+1
A
B
X
0
h+1 Y
0
h
h
B
C
(der Fall rechts-rechts geht analog)
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
166
Beachte:
Die Höhe nach der Rotation ist gleich der Höhe
vor dem Einfügen. Damit wird die AVLEigenschaft der Baumteile oberhalb des
kritischen Knotens nicht beeinflusst.
Fall: links-rechts:
vor Einfügen:
1
0
Y
0
Z
h+2
1
h+1
1
Z
h
0
Y
D
h
h
X
0
A
h-1
h-1
B
08.01.09
C
© A. Poetzsch-Heffter, TU Kaiserslautern
167
nach Einfügen
links-rechts:
2
1
2
Z
-1
Y
0
X
0
nach Rotation:
1
0
Y
0
X
0
0
Z
0
(der Fall rechts-links geht analog)
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
168
nach Einfügen
links-rechts-links:
h+3
h+2
2
Z
h
-1
Y
D
h
h+1
X
1
A
h
h-1
B
C
nach Rotation:
h+2
h+1
h
Y
0
h
A
X
0
h+1
h-1
B
C
Z
-1
h
D
(der Fall rechts-links-rechts geht analog)
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
169
nach Einfügen
links-rechts-rechts:
h+3
h+2
2
Z
h
-1
Y
D
h
h+1
X
-1
A
h-1
h
B
C
nach Rotation:
h+2
h+1
h
Y
1
h-1
A
B
X
0
h+1
h
Z
0
h
C
D
(der Fall rechts-links-links geht analog)
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
170
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
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
171
Diskussion:
Beim Einfügen eines Knotens hat der rebalancierte 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 Streuspeicherverfahren aus dem Schlüssel die
Positionsinformation des Datensatzes (z.B.
den Feldindex) zu berechnen.
Für viele praktisch relevante Szenarien erreicht
man dadurch Datenzugriff mit konstantem Aufwand.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
172
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:
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
173
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.
Anforderungen an Hashfunktionen:
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
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
174
Klassifikation von Hashverfahren:
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 Kollisionsauflösung durch ein Dictionary mit binärer Suche.
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;
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
175
Bemerkung:
Schlecht wäre beispielsweise eine Wahl von m = 2i
als Tabellengröße bei dem Divisionsrest-Verfahren,
da bei Binärdarstellung der Schlüssel dann nur die
letzten i Bits relevant sind.
Datenstruktur:
Wir realisieren eine Hashtabelle als Implementierung
der Schnittstelle Dictionary:
class HashDictionary implements Dictionary {
private int[] hashtable;
private 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 ) { ... }
public void put (int key,Object v ){ ... }
public void remove( int key ) { ... }
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
176
Sei s ein Schlüssel, h(s) sein Hashwert.
Es werden nur Objekte eingetragen (value-Parameter
von put ist immer ungleich null).
Der Datenstruktur HashDictionary liegen folgende
Invarianten zugrunde:
- Die Hashtabelle enthält den Datensatz zu s, wenn
 hashtable[ h(s) ] == s
 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);
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
177
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 ) {
hashtable[hix] = key;
datatable[hix] = v;
overflowtable[hix].remove(key);
}
return v;
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
178
Einfügen:
public void put ( int key, Object value )
// Vorbedingung: value != null
{
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 AVLTreeDictionary();
}
overflowtable[hix].put(key,value);
}
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
179
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.
Bemerkung:
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üsselrä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.
- Nachteile: Schlüsselvergleiche sehr teuer;
nicht zur Indizierung geeignet.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
180
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.
Beispiel:
(Strings als Schlüssel)
In Bezeichnerumgebungen (vgl. Folie 3.30)
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 Zahlschlüssel wird die Deklarationsinformation verwaltet.
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.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
181
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:
Das Beispiel illustriert eine Anwendung des
Entwurfmusters „Beobachter“ (engl. „observer“).
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
182
Da über die Beobachter nichts bekannt ist,
realisiert man sie sinnvollerweise durch einen
Schnittstellentyp:
interface Beobachter {
void steigen( Aktie a );
void fallen( Aktie a );
} // Fortsetzung nächste Folie
Die Assoziation zwischen Aktien und Beobachtern
implementieren wir durch eine Liste in der Klasse
Aktie, die alle Beobachter der Klasse enthält:
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
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
183
Beim Setzen des Kurswertes werden auch die
Beobachter benachrichtigt:
// 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 );
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
184
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 );
...
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
185
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
von Supertyp-Methoden (Anpassen der
Funktionalität)
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
186
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;
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
187
Vererben von Attributen:
class C {
public int
int
private int
static int
a
b
c
d
=
=
=
=
0;
1;
2;
3;
int getC() { return c; }
}
class D extends C {
int getB() { return b; }
}
public class Attributvererbung {
public static void main( String[] args ) {
D dv = new D();
System.out.println("a,D-Obj:"+ dv.a);
System.out.println("b,D-Obj:"+ dv.getB());
System.out.println("c,D-Obj:"+ dv.getC());
System.out.println("C.d: "+ C.d);
D.d = 13;
System.out.println("D.d: "+ D.d);
System.out.println("C.d: "+ C.d);
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
188
Feststellungen:
• Objekte der Subklassen haben für alle
nicht-statischen Attribute der Superklasse
eine objektlokale Variable.
• Statische Attribute werden nicht in dem Sinn
vererbt, dass die Subklasse eine eigene
Klassenvariable bekommt.
• Vererbung ist transitiv.
Hinzufügen von Attributen:
Um den Zustandsraum in Subklassenobjekten
zu erweiteren, können Attribute hinzugefügt werden:
class C {
public int a = 0;
int b = 1;
private int c = 2;
int getC() { return c; }
}
class D extends C {
public int e = 10;
int b = 11;
public int c = 12;
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
189
Folgendes Fragment demonstriert den Zugriff
auf die Attribute:
public class Zustandserweiterung {
public static void main( String[] args ) {
D dv = new D();
... dv.e
// deklariertes e
... dv.b
// deklariertes b
... dv.c
// deklariertes c
... dv.a
// ererbtes a
... ((C)dv).b
// ererbtes b
... dv.getC()
// ererbtes c
}
}
Feststellungen:
• Attribute können ererbte Attribute gleichen
Namens verschatten. Dies sollte vermieden werden,
kann aber nicht ausgeschlossen werden.
• Attribute werden statisch gebunden. D.h. :
Maßgebend ist der (statische) Typ des
selektierten Ausdrucks und nicht der Typ des
Objekts, dass sich bei Auswertung des Ausdrucks
ergibt.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
190
Vererben von Methoden:
class C {
public int
int
private int
static int
ma(){
mb(){
mc(){
md(){
return
return
return
return
0;
1;
2;
3;
}
}
}
}
int getC() { return mc(); }
}
class D extends C { }
public class Methodenvererbung {
public static void main( String[] args )
D dv = new D();
System.out.println("ma: " + dv.ma()
System.out.println("mb: " + dv.mb()
System.out.println("mc: " + dv.getC()
System.out.println("D.md: "+ D.md()
}
}
{
);
);
);
);
Feststellung:
Alle Methoden der Superklasse arbeiten auch
auf den Objekten der Subklasse. „Vererbt“ werden
aber nur die Methoden, die in der Subklasse
zugreifbar sind.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
191
Hinzufügen von Methoden:
Um die Funktionalität von Subklassenobjekten
zu erweitern, können Methoden hinzugefügt werden:
class C {
public int ma(){ return 0; }
int mb(){ return 1; }
private int mc(){ return 2; }
int getC() { return this.mc(); }
}
class D extends C {
public int me(){ return 10; }
public int mc(){ return 12; }
}
public class MethodenHinzufuegen {
public static void main( String[] args ) {
D dv = new D();
System.out.println("me: " + dv.me() );
System.out.println("mc: " + dv.mc() );
System.out.println("C:mc:"+ dv.getC() );
}
}
Feststellung:
Auch bei den Methoden kann es zur Verschattung
kommen.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
192
Anpassen von Methoden:
In vielen Fällen ist es nötig, die Implementierung
einer Methode der Superklasse in der Subklasse
anzupassen, insbesondere um den erweiterten
Zustand mit zu berücksichtigen.
In den meisten objektorientierten Programmiersprachen geschieht die Anpassung durch einen
Mechanismus, den man Überschreiben nennt:
Begriffsklärung: (Überschreiben)
Überschreiben (engl. overriding) einer ererbten
Methode m der Superklasse bedeutet, dass man
in der Subklasse eine neue Deklaration für m angibt.
Die überschreibende Methode muss in Java die
gleiche Signatur wie die überschriebene haben und
mindestens so zugreifbar sein. Die überschriebene
Methode muss zugreifbar sein und kann durch
„super“-Aufrufe benutzt werden:
super.<methodenName>( <AktParam1>,...)
Der aktuelle implizite Parameter eines Super-Aufrufs
ist der aktuelle implizite Parameter der aufrufenden
Methode.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
193
Beispiel: (Überschreiben)
class C {
private int a = 0;
public void drucke(){
System.out.println("a: " + a);
this.spruch();
}
public void spruch() {
System.out.println("Er erblich.");
}
}
class D extends C {
private int b = 1;
public void drucke(){
super.drucke();
System.out.println("b: " + b);
this.spruch();
}
public void spruch() {
System.out.println("erblich vorbelastet");
}
}
public class UeberschreibenTest {
public static void main( String[] args ) {
D dv = new D();
dv.drucke();
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
194
Beachte:
Überschreiben findet nur statt, wenn die Methode
in der Subklasse zugreifbar ist.
Konstruktoren und Vererbung:
Konstruktoren werden in Java nicht vererbt; d.h. wenn
eine Subklasse keinen eigenen Konstruktor deklariert,
steht nur der default-Konstruktor zur Verfügung.
Jeder Konstruktor kann in seiner ersten Anweisung
einen Konstruktor der Superklasse aufrufen. Impliziter
Parameter ist das neu erzeugte Objekt (Syntax siehe
Beispiel).
Fehlt ein expliziter Aufruf eines SuperklassenKonstruktors, wird implizit der Konstruktor mit leerer
Parameterliste aufgerufen.
Gibt es keinen solchen Konstruktor oder ist er nicht
zugreifbar, meldet der Übersetzer einen Fehler.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
195
Beispiel: (zum Umgang mit Konstruktoren)
class Superklasse {
String a;
int
b;
Superklasse(){
a = "\"Java ist ";
}
Superklasse( int i ){
a = "Auch "+ new Integer(i).toString();
}
}
class Subklasse extends Superklasse {
Subklasse( String s ){
a = a + s;
}
Subklasse( int i, int j ){
super(i*j);
a = a + " Wiederholungen machen Legenden";
}
}
class VererbungsTest {
public static void main( String[] args ){
Subklasse sk = new Subklasse("einfach.\"");
System.out.println( sk.a );
sk = new Subklasse(100,10);
System.out.print( sk.a );
System.out.println(" nicht wahr.");
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
196
Diskussion von Vererbung
Vererbung ist ein mächtiges Sprachkonzept.
Das Konzept ist im Kern einfach:
Statt Programmcode explizit von der Super- in
die Subklassen zu kopieren, steht der vererbte
Code automatisch in der Subklasse bereit.
Vorteile gegenüber explizitem Kopieren & Einfügen:
- zuverlässiger
- Reduktion der Programmcodegröße
- besser zu warten/pflegen
- Spezialisierung unzugänglicher Programmteile
wird erleichtert
Die sprachliche Umsetzung führt bei den meisten
Sprachen zu komplexen Wechselwirkungen zwischen
den Konstrukten:
- zur Vererbung
- zum Information Hiding (private, protected, ...)
- zur dynamischen Methodenauswahl.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
197
Beispiel: (Konstruktoren/dyn. Bindung)
class Oberklasse {
String a;
Oberklasse(){
a = "aha";
m();
}
void m(){
System.out.print("Laenge a:"+a.length());
}
}
class Unterklasse extends Oberklasse {
String b;
Unterklasse(){
b = "boff";
m();
}
void m(){
System.out.print("Laenge b:"+b.length());
}
}
class KonstruktorProblemTest {
public static void main( String[] args ){
new Unterklasse();
}
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
198
In Verbindung mit Subtyping ist Vererbung:
- ein sehr mächtiger Strukturierungsmechanismus,
- der die Entwicklung offener Programme unterstützt
- und Wartbarkeit und Lesbarkeit verbessert
(bei geeignetem Einsatz).
Subclassing = Subtyping + Vererbung
Wir betrachten den Zusammenhang zwischen
Subtypbildung und Vererbung. Für Java ist das
insbesondere der Zusammenhang von
- Schnittstellentypen
- abstrakten Klassen
- (vollständigen) Klassen
Begriffsklärung: (abstrakte Meth. & Klassen)
Eine Methode heißt abstrakt, wenn für sie kein
Rumpf angegeben ist. Eine Klasse heißt abstrakt,
wenn sie abstrakte Methoden besitzt oder als
abstrakt deklariert ist (Modifikator abstract).
Es ist unzulässig, Instanzen abstrakter Klassen
zu erzeugen.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
199
Abstrakte Klassen stehen zwischen Schnittstellen
und vollständigen Klassen:
• Schnittstellentyp:
- keine Attribute, keine Methodenimplementierung
- Typ umfasst alle Objekte der Subklassen
• Typ deklariert durch abstrakte Klasse:
- Attribute, Methodenimplementierung (Vererbung)
- Typ umfasst alle Objekte der Subklassen
• Typ deklariert durch vollständige Klasse K:
- Attribute, Methodenimplementierung (vollständig)
- Objekterzeugung
- Typ umfasst die Objekte von K und
alle Objekte in Subklassen
Den Zusammenhang zwischen diesen Sprachkonzepten studieren wir anhand folgenden Beispiels.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
200
Beispiel: (Realisieren von Typhierarchien)
A
B
void mm()
void mp()
void mm()
void mq()
C
D
void mm()
void mp()
void mq()
void mr()
void mm()
void mp()
void mq()
void ms()
Drei Realisierungsvarianten:
1. Nur Subtyping, keine Vererbung:
- A und B als Schnittstellen
- C und D als Klassen
2. Einfache Vererbung von einer Klasse:
- A als abstrakte Klasse
- B als Schnittstelle
- C und D als Klassen, erben von A.
3. Mehrfachvererbung:
- A, B, C und D als Klassen; C, D erben von A u. B.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
201
1. Variante:
interface A {
void mm();
void mp();
}
interface B {
void mm();
void mq();
}
class C implements A, B {
int a, b, c;
public void mm(){
c = 2000; ...
{ // dieser Block benutzt nur Attribut a
// und ist identisch mit entsprechendem
// Block in Klasse D
... a ...
}
c = a + c;
}
public void mp(){
// benutzt die Attribute a und c
}
public void mq(){
b = 73532;
}
public void mr(){ ... }
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
202
class D implements A, B {
int a, b;
String d;
public void mm(){
// dieser Block benutzt nur Attribut a
// und ist identisch mit Block in Klasse C
... a ...
}
public void mp(){
// benutzt die Attribute a und d
}
public void mq(){
b = 73532;
}
public void ms(){ ... }
}
C und D haben das Attribut a und den Block in
der Methode mm gemeinsam. Diese Programmteile
lassen sich in A zusammenfassen.
2. Variante:
abstract class A {
int a;
public void mm() {
// der Block von mm in C aus der ersten
// Variante bzw. der Rumpf von mm in D
... a ...
}
public abstract void mp();
}
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
203
In der zweiten Variante ist der Typ B nach wie vor
als Schnittstelle realisiert. Die Klasse C und D erben
von der abstrakten Klasse A:
class C extends A implements B {
int b, c;
public void mm(){
c = 2000; ...
super.mm();
c = a + c;
}
public void mp(){
// benutzt die Attribute a und c
}
public void mq(){ b = 73532; }
public void mr(){ ... }
}
class D extends A implements B {
int b;
String d;
public void mp(){
// benutzt die Attribute a und d
}
public void mq(){ b = 73532; }
public void ms(){ ... }
}
Diese Variante ist knapper als die erste.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
204
Begriffsklärung: (Mehrfachvererbung)
Übernimmt eine Klasse Programmteile von
mehreren anderen Klassen spricht man von
Mehrfachvererbung (engl. multiple inheritance).
C++ unterstützt Mehrfachvererbung, Java nicht.
3. Variante:
Wir illustrieren Mehrfachvererbung hier mit einer
fiktiven Spracherweiterung von Java, die
Mehrfachvererbung unterstützt.
Klasse A bleibt wie in Variante 2. Typ B wird durch
eine Klasse realisiert:
class B {
int b;
public void mm(){
... // gemaess den Anforderungen von B
}
public void mq(){ b = 73532; }
}
Die Klassen C und D können nun von B erben.
Allerdings müssen sie den Konflikt bzgl. mm auflösen.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
205
class C extends A, B {// KEIN Java!!!
int c;
public void mm(){
c = 2000; ...
super.A:mm();
c = a + c;
}
public void mp(){
// benutzt die Attribute a und c
}
public void mr(){ ... }
}
class D extends A,B { // KEIN Java!!!
String d;
public void mm(){
super.A:mm();
}
public void mp(){
// benutzt die Attribute a und d
}
public void ms(){ ... }
}
Da in Java Mehrfachvererbung nicht möglich ist,
muss man es durch Einfachvererbung und
mehrfache Subtypbildung ersetzen, z.B. wie in der
zweiten Variante demonstriert.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
206
Vererbung und Information Hiding
Durch die Vererbung gibt es nun zwei Arten, eine
Klasse K zu nutzen:
- Anwendungsnutzung: Erzeugen und nutzen der
Objekt von K.
- Vererbungsnutzung: Spezialisieren und erben von K.
Geschützter Zugriff:
Damit die erbende Klasse die geerbten Programmteile geeignet nutzen kann, benötigt sie meist einen
intimeren Zugriff als ein Anwendungsnutzer.
Deshalb gibt es einen Zugriffsbereich für Vererbung,
der alle Subklassen einer Klasse umfasst.
Programmelemente, die als geschützt deklariert sind,
d.h. mit dem Modifikator protected, sind in allen
Subklassen zugreifbar.
Will man also Programmelemente, insbesondere
Attribute, für Subklassen bereitstellen, müssen sie
mindestens geschützten Zugriff gewähren.
Geschützter Zugriff ermöglicht allerdings erhebliches
Verändern einer Klasse in Subklassen und birgt
dementsprechend auch Gefahren, wie folgendes
Beispiel zeigt.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
207
Beispiel: (geschütztes Zugriffsrecht)
package soweitAllesOk;
public class A_nicht_Null {
protected int a = 1;
public
int
getA() {
return a;
}
protected void setA( int i ) {
if( i>0 ) a = i;
}
}
public class Anwendung {
...
public static void m( A_nicht_Null ap ){
float f = 7 / ap.getA();
}
}
Die Anwendung kann hier davon ausgehen, dass
die Instanzvariable a nie den Wert 0 annimmt.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
208
Durch Vererbung können Subtyp-Objekte erzeugt
werden, die sich ganz anders als die Objekte der
Superklasse verhalten:
package einHackMitZweck;
import soweitAllesOk.*;
public class A_doch_Null extends A_nicht_Null
{
public
int
getA() { return -a; }
public
void setA( int i ) { a = i; }
}
public class Main {
public static void main( String[] args ) {
A_doch_Null adn = new A_doch_Null();
adn.setA( 0 );
A_nicht_Null ann = adn;
... // hier könnte die Herkunft von
// ann verschleiert sein
Anwendung.m(ann);
}
}
Um Szenarien wie im obigen Beispiel zu vermeiden,
sollten Subklassen-Objekte das Verhalten der
Superklassen-Objekte spezialisieren und sich
ansonsten konform verhalten.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
209
Zusammenfassende Bemerkungen zu 5.3
• Subtypen erlauben es, spezialisierte Objekte
anstelle von Supertyp-Objekten zu verwenden.
Dadurch können Programme auf der Ebene
allgemeinerer Objekte formuliert und
wiederverwendet werden.
• Vererbung erlaubt die Weitergabe und damit
Wiederverwendung von Programmteilen der
Superklasse an die Subklasse.
• Subtypen in Kombination mit Vererbung erlauben
eine direkte Realisierung von Klassifikationen im
Rahmen der Programmierung.
• Die Vorteile wirken sich vor allem bei der
Entwicklung von Programmbibliotheken und
Programmgerüsten/Frameworks aus.
08.01.09
© A. Poetzsch-Heffter, TU Kaiserslautern
210
Herunterladen