Vererbung
Überschreiben
Typanpassungen
Interfaces
abstrakte Klassen
Mit den Konzepten der Vererbung (inheritance) kommen wir zu höheren objektorientierten Konzepten.
Modellierung einer "ist-ein"-Beziehung zwischen Datentypen
manchmal ist ein Datentyp A ein Untertyp eines Obertyps B
die Klasse A ist dann eine Erweiterung der Klasse B, d.h. alles was B hat hat auch A
um die Implementierung von B bei der Implementierung von A
wiederzuverwenden (re-use) verwendet man in Java
die Vererbung mit class A extends B
B ist die sogenannte Basisklasse und A ist die abgeleitete Klasse, man sagt auch A ist eine Unterklasse (sub-class) und B ist die Oberklasse (super class)
alle Methoden von B sind somit in A und über A-Objekte verwendbar
A kann Methoden von B durch eigene (passendere) Methoden mit der gleichen Signatur überschreiben
von einer Klasse B können beliebig viele Klassen abgeleitet werden
eine Klasse A kann nur von einer Klasse erben (single inheritance)
in einem Konstruktor von A wird implizit der
argumentlose Konstruktor von B aufgerufen super()
andere Konstruktoren können explizit mit super(...)
(als erste Anweisung) angesprochen werden
alle Klassen erben implizit von Object
durch endgültige Klassen (final class) lässt sich die Vererbung / Ableitung verhindern
public class Vererbung { public static void main(String[] args) { B1 b = new B1(); System.out.println("b = " + b); B1.method(); b.objmethod(); A1 a = new A1(); System.out.println("a = " + a); a.objmethod(); a.andere(); b = a; System.out.println("b = " + b); b.objmethod(); //b.andere(); // Fehler C1 c = new C1(); System.out.println("c = " + c); C1.method(); c.objmethod(); D1 d = new D1(); System.out.println("d = " + d); d.andere(); b = c; System.out.println("b = " + b); b = d; System.out.println("b = " + b); } } class B1 { static void method() { } void objmethod() { } public String toString() { return "ich bin ein B1"; } } class A1 extends B1 { void andere() { } public String toString() { return "ich bin ein A1"; } } class C1 extends B1 { public String toString() { return "ich bin ein C1"; } } class D1 extends A1 { public String toString() { return "ich bin ein D1"; } }
Eine Methode in A implementiert eine Methode mit gleicher Schnittstelle von B, aber mit anderer Bedeutung (Semantik) (overriding).
Auch virtuelle Vererbung und virtuelle Methoden genannt im Gegensatz zu realer Vererbung (oben).
es gelte class A extends B { ... }
gleiche Schnittstelle: d.h. gleiche Signatur (Name, Anzahl und Typ der Parameter)
das reale Objekt entscheidet über die Bedeutung der Methode, nicht der Typ der Objektvaraiblen
die Auswahl der realen Methode erfolgt erst zur Laufzeit, nicht bei der Kompilierung (dynamische Bindung, dynamic/late binding)
z.B. toString()
konvertiert immer das reale Objekt
in eine Zeichenkette
das Überschreiben lässt sich durch endgültige Methoden
(final
) verhindern,
Überschreiben von Klassenmethoden (static) macht keinen Sinn
durch das Ableiten einer Klasse wird implizit das Versprechen abgegeben (Kontrakt vereinbart), dass sich Objekte, die auf die Basisklasse reduziert werden genau so verhalten wie Objekte der Basisklasse
public class Ueberschreiben { public static void main(String[] args) { B2 b = new B2(); System.out.println("b = " + b); B2.method(); System.out.println("b.objmethod() = " + b.objmethod()); A2 a = new A2(); System.out.println("a = " + a); System.out.println("a.objmethod() = " + a.objmethod()); b = a; System.out.println("b = " + b); System.out.println("b.objmethod() = " + b.objmethod()); } } class B2 { static void method() { } String objmethod() { return "von B2"; } public String toString() { return "ich bin ein B2"; } } class A2 extends B2 { static void method() { } String objmethod() { return "von A2"; } void andere() { } public String toString() { return "ich bin ein A2"; } }
es gelte wieder class A extends B { ... }
Ausweitung (widening): von der abgeleiteten Klasse zur Basisklasse
B b = new A()
,
auch Aufwärtsanpassung genannt
Verengung (narrowing): von der Basisklasse zu einer abgeleiteten Klasse
A a = (A) b
nur mit explizitem Typecast,
auch Abwärtsanpassung genannt
falls die Verengung nicht möglich ist wird eine ClassCastException ausgelöst
mit Hilfe von instanceof
lässt sich testen ob ein Objekt zu einer bestimmten (abgeleiteten) Klasse
gehört,
if (b instanceof A) ((A) b).method();
public class Anpassung { public static void main(String[] args) { B3 b = new B3(); System.out.println("b = " + b); A3 a = new A3(); System.out.println("a = " + a); b = a; System.out.println("b = " + b); if (b instanceof A3) a = (A3) b; System.out.println("a = " + a); ((A3) b).andere(); } } class B3 { void objmethod() { } public String toString() { return "ich bin ein B3"; } } class A3 extends B3 { void andere() { } public String toString() { return "ich bin ein A3"; } }
Wir definieren eine einfache Klasse für Autos (Car). Man kann mit dieser Klasse ein Auto mit den Parametern Modell (model), Baujahr (year) und (Neu-)Preis (price) definieren.
Die Klassen-Methoden getModel(), getYear() und getPrice() dienen dem Zugriff auf die Objekt-Variablen.
public class Car { String model; int year; int price; public Car(String m, int y, int p) { model = m; year = y; price = p; } public String getName() { return model+" [year="+year+"]"; } public int getPrice() { return price; } public static void main(String[] args) { Car ford = new Car("ford bronco", 1992, 35000); Car vw = new Car("vw golf", 1984, 25000); System.out.println( ford.getName()+" costs "+ford.getPrice()); System.out.println( vw.getName()+" costs "+vw.getPrice()); } }
Mit dieser Klasse kann man zum Beispiel die Objekte ford und vw erzeugen. Dies ergibt dann folgende Ausgabe.
ford bronco [year=1992] costs 35000 vw golf [year=1984] costs 25000
Mit dem Schlüsselwort extends kann man eine Klasse von einer anderen Klasse ableiten. Als Beispiel einer abgeleiteten Klasse von Car sei die Klasse Gebrauchtwagen (UsedCar) definiert, die den zusätzlichen Parameter `gefahrene Kilometer' (mileage) hat.
public class UsedCar extends Car { int mileage; public UsedCar(String m, int y, int p, int k) { super(m, y, p); mileage = k; } public String getName() { return super.getName()+" (mileage="+mileage+")"; } public int getMileage() { return mileage; } public int getPrice() { return 10000*price/(mileage+10000); } }
Die Methode getMilage() dient wieder dem Zugriff auf die Klassen-Parameter. Der Konstruktor für UsedCar ruft mit super(m, y, p) den Konstruktor der Basisklasse auf, dann wird der Parameter mileage gesetzt. Die Methode getPrice() überschreibt die Methode mit dem gleichen Namen und der gleichen Signatur (d.h. der gleichen Art und Anzahl der Parameter) von der Basisklasse. getPrice() berechnet in dieser Klasse den Gebrauchtwagenpreis in Abhängigkeit von der gefahrenen Kilometerzahl.
Mit diesen Klassen kann man dann zum Beispiel in dem Hauptprogramm main die folgenden Beziehungen definieren und ausdrucken.
public static void main(String[] args) { Car cars[] = new Car[4]; cars[0] = new Car("ford bronco", 1992, 35000); cars[1] = new UsedCar("ford bronco", 1992, 35000, 8000); cars[2] = new Car("vw golf", 1984, 25000); cars[3] = new UsedCar("vw golf", 1984, 25000, 20000); for (int i=0; i<4; i++) System.out.println(cars[i].getName() +" costs "+cars[i].getPrice()); }
Als Ausgabe erhält man zum Beispiel.
java UsedCar ford bronco [year=1992] costs 35000 ford bronco [year=1992] (mileage=8000) costs 19444 vw golf [year=1984] costs 25000 vw golf [year=1984] (mileage=20000) costs 8333
Eine Java-Klasse ist nur von einer anderen Klasse ableitbar. Java kennt keine Mehrfachvererbung (multiple inheritance). Aber mit dem Sprachkonstrukt interface kann eine Klasse mehrere Methodensignaturen implementieren (erben). Ein Interface deklariert nur abstrakte Methoden.
Zum Beispiel kann man ein Interface Rank definieren, das das Vergleichen zweier Objekte erlaubt. compare() gibt eine positive ganze Zahl zurück, falls dieses Objekt (this) in der implementierten Ordnung vor dem anderen Objekt (obj) steht.
public interface Rank { public int compare(Rank obj) throws RankException; }
Ist die implementierte Ordnung nicht total, so wird für unvergleichbare Objekte eine RankException ausgelöst.
public class RankException extends RuntimeException { }
Eine Subklasse von Rectangle (aus java.awt), die dieses Interface unterstützt, kann man wie folgt definieren.
public class RankedRect extends Rect implements Rank { public RankedRect(int w, int h) { super(w,h); } public int compare(Rank obj) throws RankException { try { return area() - ((Rect)obj).area(); } catch (ClassCastException e) { throw new RankException(); } } }
In einer ähnlichen Weise kann man eine Subklasse von Car definieren.
public class RankedCar extends Car implements Rank { public RankedCar(String m, int y, int p) { super(m,y,p); } public int compare(Rank obj) { try { return getPrice()-((Car)obj).getPrice(); } catch (ClassCastException e) { throw new RankException(); } } }
Diese beiden Klassen können behandelt werden, als ob sie vom Typ Rank wären. Zum Beispiel als Argument in einer Methode foo(Rank obj). Eine Klasse kann mehrere Interfaces implementieren. Bei der Erzeugung von Objekten von Klassen, die ein Interface implementieren, kann man die Struktur, die nicht von dem Interface stammt, ignorieren.
Rank r = new RankedCar(model, year, price);
Dann ist aber nur noch die Verwendung der Methode r.compare(.) möglich und nicht mehr r.getModel(). Um dennoch getModel() zu verwenden, muß erst ein Cast (eine Typenanpassung) auf RankedCar gemacht werden:
( (RankedCar) r ).getModel()
Man kann eine Klasse definieren, die keine vollständige Implementierung der deklarierten Methoden enthält.
Diese kann man sich als Mischform zwischen einem Interface und einer richtigen Klasse vorstellen. Ein Teil der Methoden wird implementiert, und für einen anderen Teil der Methoden werden nur die Spezifikationen (das Interface) festgelegt.
Eine solche Klasse wird als abstrakte Klasse bezeichnet und muß mit dem Schlüsselwort abstract gekennzeichnet werden.
public abstract class Cell { int size; public int getSize() { return size; } public abstract void interact(Cell neighbor); }
Aus abstrakten Klassen können können keine Objekte erzeugt werden. Aus einer Subklasse, die die fehlenden Implementationen definiert, können Objekte erzeugt werden.
Interface: BitStringInterface
Methoden: toString()
und toBitString()
liefern die entsprechende Darstellung als Binärfolge.
public interface BitStringInterface { public String toString(); public String toBitString(); }
Abstrakte Klasse: AbstractBitString
Methoden: toString()
wird mit Hilfe
von toBitString()
implementiert.
public abstract class AbstractBitString implements BitStringInterface { protected int art; final static int BYTE = 1; final static int SHORT = 2; final static int INT = 3; final static int LONG = 4; final static byte BYTEsize = 7; final static byte SHORTsize = 15; final static byte INTsize = 31; final static byte LONGsize = 63; protected long ls = 0; public String toString() { return toBitString(); } public abstract String toBitString(); }
Klasse: BitStringSign
Darstellung von ganzen Zahlen als Binärfolge. Die Zahlen werden als Paare von Vorzeichen und positiven Zahlen dargestellt.
Konstruktoren: für byte
, short
,
int
und long
,
Methoden: toBitString()
liefert Zeichenkette entsprechend dem Datentyp.
bitStringSign()
liefert Zeichenkette entsprechend der
'Vorzeichen, Grösse'-Darstellung.
public class BitStringSign extends AbstractBitString { public BitStringSign(byte b) { art = BYTE; ls = b; } public BitStringSign(short s) { art = SHORT; ls = s; } public BitStringSign(int s) { art = INT; ls = s; } public BitStringSign(long s) { art = LONG; ls = s; } public String toBitString() { switch (art) { case BYTE: return bitStringSign(BYTEsize,ls); case SHORT: return bitStringSign(SHORTsize,ls); case INT: return bitStringSign(INTsize,ls); case LONG: return bitStringSign(LONGsize,ls); default: return ""+ls; } } protected static String bitStringSign(byte l, long b) { String e = ""; long x = 0; String s = ""; byte einz = (byte)1; if (b < 0L ) { x = (long)-b; s = "1"; } else { x = (long)b; s = "0"; } for (int i = 0; i < l; i++) { // (x % 2) == 0 if ( ( ((byte)x) & einz) == 0 ) { e = "0" + e; } else { e = "1" + e; } x = (long)(x >> 1); } return s+e; } }
Klasse: BitStringComplement
Darstellung von ganzen Zahlen als Binärfolge. Die Zahlen werden im Zweier-Komplement dargestellt.
Konstruktoren: für byte
, short
,
int
und long
,
Methoden: toBitString()
liefert Zeichenkette entsprechend dem Datentyp.
bitStringComplement()
liefert Zeichenkette entsprechend der
Komplement-Darstellung.
public class BitStringComplement extends AbstractBitString { public BitStringComplement(byte b) { art = BYTE; ls = b; } public BitStringComplement(short s) { art = SHORT; ls = s; } public BitStringComplement(int s) { art = INT; ls = s; } public BitStringComplement(long s) { art = LONG; ls = s; } public String toBitString() { switch (art) { case BYTE: return bitStringComplement(BYTEsize,ls); case SHORT: return bitStringComplement(SHORTsize,ls); case INT: return bitStringComplement(INTsize,ls); case LONG: return bitStringComplement(LONGsize,ls); default: return ""+ls; } } protected static String bitStringComplement(byte l, long b) { String e = ""; long x = b; byte einz = (byte)1; for (int i = 0; i < l+1; i++) { // (x % 2) == 0 if ( ( ((byte)x) & einz) == 0 ) { e = "0" + e; } else { e = "1" + e; } x = (long)(x >> 1); } return e; } }
Klasse: BitTest
Testen der zwei Darstellungen.
Es werden Objekte von BitStringSign
und BitStringComplement
erzeugt. Diese Objekte können in Variablen der entsprechenden Klassen abgespeichert
werden, aber auch in Variablen vom Typ BitStringInterface
als auch von Typ AbstractBitString
.
public class BitTest { public static void main(String[] args) { test1(); test2(); test3(); test4(); } static void test1() { BitStringInterface a = new BitStringSign((int)7); System.out.println("sign( 7) = " + a); BitStringInterface b = new BitStringComplement((int)7); System.out.println("cmpl( 7) = " + b); BitStringInterface g = new BitStringSign((int)-7); System.out.println("sign( -7) = " + g); BitStringInterface h = new BitStringComplement((int)-7); System.out.println("cmpl( -7) = " + h); BitStringInterface d = new BitStringSign((short)32767); System.out.println("sign( 32767) = " + d); BitStringInterface c = new BitStringComplement((short)32767); System.out.println("cmpl( 32767) = " + c); AbstractBitString e = new BitStringSign((int)-32767); System.out.println("sign(-32767) = " + e); AbstractBitString f = new BitStringComplement((int)-32767); System.out.println("cmpl(-32767) = " + f); System.out.println(); } static void test2() { short x = -1; for (byte l = 0; l <= 15; l++ ) { BitStringInterface g = new BitStringSign((short)x); System.out.println("sign( "+ x + ") = " + g); x *= (short)2; } System.out.println(); } static void test3() { short x = -1; for (byte l = 0; l <= 15; l++ ) { AbstractBitString h = new BitStringComplement((short)x); System.out.println("cmpl( "+ x + ") = " + h); x *= (short)2; } System.out.println(); } static void test4() { short x = -1; for (byte l = 0; l <= 15; l++ ) { System.out.println("Integer( "+ x + ") = " + Integer.toBinaryString(x)); x *= (short)2; } System.out.println(); } }
Im letzten Test erfolgt ein Vergleich mit der 'eingebauten'
Funktion Integer.toBinaryString(x))
Ausgabe:
sign( 7) = 00000000000000000000000000000111 cmpl( 7) = 00000000000000000000000000000111 sign( -7) = 10000000000000000000000000000111 cmpl( -7) = 11111111111111111111111111111001 sign( 32767) = 0111111111111111 cmpl( 32767) = 0111111111111111 sign(-32767) = 10000000000000000111111111111111 cmpl(-32767) = 11111111111111111000000000000001 sign( -1) = 1000000000000001 sign( -2) = 1000000000000010 sign( -4) = 1000000000000100 sign( -8) = 1000000000001000 sign( -16) = 1000000000010000 sign( -32) = 1000000000100000 sign( -64) = 1000000001000000 sign( -128) = 1000000010000000 sign( -256) = 1000000100000000 sign( -512) = 1000001000000000 sign( -1024) = 1000010000000000 sign( -2048) = 1000100000000000 sign( -4096) = 1001000000000000 sign( -8192) = 1010000000000000 sign( -16384) = 1100000000000000 sign( -32768) = 1000000000000000 cmpl( -1) = 1111111111111111 cmpl( -2) = 1111111111111110 cmpl( -4) = 1111111111111100 cmpl( -8) = 1111111111111000 cmpl( -16) = 1111111111110000 cmpl( -32) = 1111111111100000 cmpl( -64) = 1111111111000000 cmpl( -128) = 1111111110000000 cmpl( -256) = 1111111100000000 cmpl( -512) = 1111111000000000 cmpl( -1024) = 1111110000000000 cmpl( -2048) = 1111100000000000 cmpl( -4096) = 1111000000000000 cmpl( -8192) = 1110000000000000 cmpl( -16384) = 1100000000000000 cmpl( -32768) = 1000000000000000 Integer( -1) = 11111111111111111111111111111111 Integer( -2) = 11111111111111111111111111111110 Integer( -4) = 11111111111111111111111111111100 Integer( -8) = 11111111111111111111111111111000 Integer( -16) = 11111111111111111111111111110000 Integer( -32) = 11111111111111111111111111100000 Integer( -64) = 11111111111111111111111111000000 Integer( -128) = 11111111111111111111111110000000 Integer( -256) = 11111111111111111111111100000000 Integer( -512) = 11111111111111111111111000000000 Integer( -1024) = 11111111111111111111110000000000 Integer( -2048) = 11111111111111111111100000000000 Integer( -4096) = 11111111111111111111000000000000 Integer( -8192) = 11111111111111111110000000000000 Integer( -16384) = 11111111111111111100000000000000 Integer( -32768) = 11111111111111111000000000000000
Vergleiche auch die Hüll-Klassen java.lang.Integer
und
java.lang.Long
.
Für die Klassen java.lang.Byte
und
java.lang.Short
existieren keine vergleichbaren Methoden.
© Universität Mannheim, Rechenzentrum, 1998-2005.
Heinz Kredel Last modified: Mon Jan 23 22:52:52 CET 2006