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