Schreiben von Basisklassen und Erweiterungen in Java
Sichtbarkeit und Vererbung
public-Einträge sind von überall aus sichtbar.
protected-Einträge können nur in Unterklassen oder in Klassen aus demselben Paket gesehen werden.
Einträge ohne »public«, »protected« oder »private« können nur aus dem gesamten Paket, in dem sie deklariert sind, heraus gesehen werden. Sie werden nicht an Unterklassen aus anderen Paketen vererbt.
private-Einträge können nur aus der Klasse heraus aufgerufen werden, in der sie deklariert sind. Sie werden nicht vererbt.
Sichtbarkeit "public" "protected" (Fehlen) "private"
Klasse selber Ja Ja Ja Ja
Paket Ja Ja Ja
Unterklasse Ja Ja
Rest Ja
Sichtbarkeit in Unterklassen
Die Zugriffsmöglichkeiten auf eine Methode dürfen durch Ersetzen der Methode nicht abgeschwächt werden, sondern nur unverändert gelassen oder verstärkt werden.
Daher ist die Deklaration der Klasse "E0" der Quelldatei "Test.java" in dem folgenden Quelltext zulässig, während die auskommentierte Deklaration der Klasse "E1" nicht erlaubt ist.
- Quelltext
class B0 { void m(){} } class E0 extends B0 { public void m(){} }
// class B1 { public void m(){} } class E1 extends B1 { void m(){} }
Formales und semantisches Ersetzen
Beim semantischen Ersetzen erfüllt die Implementation einer Methode in einer Klasse die Kontrakt dieser Methode in allen Oberklassen.
Als Grundlage des folgenden Beispiels zum Ersetzen in Java dient die Klasse "Account.java" zur Modellierung eines Kontos.
Account.java
class Account
{ private double balance;
/** Initialize the account. */
Account(){ balance = 0.; }
/** Converts a string representation of a number
to a double value.
@param text the string to be converted
@return the double value of the text */
protected static double val( final String text )
{ return new Double( text ).doubleValue(); }
/** Represents a double value by a string.
@param number the number to be represented.
@return the string representing the number */
protected static String str( final double number )
{ return new Double( number ).toString(); }
/** Deposit money into an account.
@param deposit the money to be deposited into the account
@return the money accepted and added to the balance */
String deposit( final String deposit )
{ double amount = val( deposit );
balance += amount; return str( amount ); }
/** Retrieve the balance of the account.
@return the current balance of the account */
String balance(){ return str( balance ); }
/** Retrieve a description of the account.
@return the description of the account */
String description(){ return "standard account"; }}
Die Klasse "Account" enthält eine Methode "description", die eine kurze Beschreibung des Konto ergibt.
Nun ist es denkbar, daß ein solches Konto benötigt wird, aber die Beschreibung in Deutsch sein soll. Um die gewünschte Kontoklasse herzustellen, kann die Klasse "Account" „erweitert“ werden. Dabei wird diesmal aber nicht wirklich eine Erweiterung hinzugefügt, sondern die unpassende Methode "description" ersetzt. Daher ist in diesem Fall der Name „Erweiterung“ nicht ganz passend, man könnte eher von einer „Veränderung“ sprechen, bei der allerdings das Original nicht angetastet wird, sondern eine Art von Kopie erstellt und dann teilweise verändert wird. Eine Klasse, die mit dem Schlüsselwort "extends" von einer Basisklasse abgeleitet ist und dann nur Methoden dieser Basisklasse ersetzt, wird hier auch als eine Ersetzung bezeichnet.
Vergleiche kann man dies mit einer Situation, in der jemand wissen will, wie die Mona Lisa aussieht, wenn sie nicht lächelt. Dazu könnte er das Lächeln der Mona Lisa übermalen. Es kann aber wünschenswert sein, die originale Mona Lisa nicht zu verändern, da sie auch weiterhin noch in ihrem ursprünglichen Zustand benötigt wird. Dann kann eine Kopie der Mona Lisa angefertigt werden, die nun übermalt werden kann, während das Original weiterhin unverändert bleibt.
Wenn in der erweiternden Klasse eine Methode mit der gleichen Signatur wie eine Methode der Basisklasse deklariert wird, dann wird die durch diese Signatur bestimmte Operation von Objekten der erweiternden Klasse durch jene in der Deklaration der erweiternden Klasse deklarierte Methode implementiert. Wird also eine parameterlose Exemplarmethode "description" in der erweiternden Klasse deklariert, dann verwenden Exemplare der erweiternden Klasse diese Methode für die Operation "description()". Dadurch kann diese Methode dort nun so definiert werden, daß sie eine deutschsprachige Kontobezeichnung zurückgibt, während alle anderen Methoden und Felder von der Basisklasse übernommen werden.
AccountGerman.java
class AccountGerman extends Account
{ /** Retrieve a description of the account.
@return the description of the account */
String description(){ return "Standardkonto"; }}AccountGerman [UML class diagram]
.--------------------------------------.
| Account |
|--------------------------------------|
|--------------------------------------|
| + deposit( String ) : String |
| + balance() : String |
| + description() : String |
'--------------------------------------'
^
/_\
|
|
.--------------------------------------.
| AccountGerman |
|--------------------------------------|
|--------------------------------------|
| + description() : String |
'--------------------------------------'
Durch diese Art der „Veränderung“ einer Klasse wird das Offen-Geschlossen-Prinzip gewahrt: Die Basisklasse "Account" war „offen“, um zur Herstellung einer anderen Klasse genutzt werden zu können; sie blieb „geschlossen“, da sie selber dabei nicht verändert wurde.
Nun kann ein Exemplar der Klasse "AccountGerman" wie ein Exemplar der Klasse "Account" verwendet werden, der Unterschied besteht nur in der Rückgabe eines deutschsprachigen Textes als Kontobeschreibung.
Test.java
public final class Test
{ final public static void main( String[] args )
{ AccountGerman a = new AccountGerman();
a.deposit( "20" ); java.lang.System.out.println( a.description() );
a.deposit( "10" ); java.lang.System.out.println( a.balance() ); }}System.out
Standardkonto
30.0
Spezifikationstreues Ersetzen
Das Ersetzen von Methoden in Erweiterungen einer Klasse sollte aber semantisch treu hinsichtlich der Spezifikation der Operation erfolgen. Das setzt natürlich voraus, daß es überhaupt eine Spezifikation gibt.
ℛ spezifikationstreue Ersetzen Zu jeder Operation einer Klasse sollte es eine Spezifikation geben, welche neben der Signatur die Rückgabespezifikation enthält und das Verhalten (Wert und Wirkung) der Operation und seine Abhängigkeit von Umständen des Aufrufs (z.B. Argumentwerten) beschreibt. Wenn solch eine Operation einer Klasse dann in einer Unterklasse ersetzt wird, sollte die Spezifikation der Operation beachtet werden: Die Ersetzung sollte die Spezifikation der ersetzten Operation erfüllen.
Eine Forderung mit ähnlichem Sinn ist auch als Liskov-Substitutions-Prinzip (LSP ) bekannt. Da das LSP aber recht kompliziert formuliert ist und der Bezug auf eine Spezifikation fehlt, wird es hier nicht verwendet.
Die Spezifikation der Operation "description()" legt beispielsweise fest, daß diese eine Beschreibung des Kontos vom Typ "String" ergibt, aber nicht, in welcher Sprache diese Beschreibung geschrieben ist.
description() [specification]
/** Retrieve a description of the account.
@return the description of the account */
String description();
Beide Implementationen von "description()" sind dieser Spezifikation treu, denn die Signatur, der Rückgabetyp und die Beschreibung werden von beiden Implementationen so verwirklicht, wie die Spezifikation dies verlangt.
Wenn man sagt, daß die Spezifikation einer Operation ihre Bedeutung festlegt, dann kann man das Treue-Ersetzung-Prinzip auch so formulieren: Durch das Ersetzen einer Operation in einer Unterklasse sollte die Bedeutung der Operation nicht verändert werden.
Was die Bedeutung einer Operation ist, wird aber durch ihre Spezifikation festgelegt. Wenn die Spezifikation der Operation "balance()" die Rückgabe einer englischsprachigen Kontobeschreibung verlangen würde, dann wäre das Ersetzen der Operation, wie in der Klasse "AccountGerman" geschehen, nicht spezifikationstreu. Da die Spezifikation die Sprache der Kontobeschreibung aber offenläßt, ist es spezifikationstreu, sie in Englisch auszugeben, genauso, wie es spezifikationstreu ist, die Kontobeschreibung in Deutsch auszugeben.
Der Java -Übersetzer kann nur erkennen, daß die Signatur einer ersetzten Methode mit der Signatur einer Methode der Basisklasse übereinstimmt, er kann Spezifikationen aber nicht lesen und verstehen und daher die fehlende Spezifikationstreue einer Ersetzung nicht erkennen.
Mit den Begriffen „formaler Untertyp“ und „semantischer Untertyp“ kann man dies auch so formulieren: Die Java -Implementation stellt sicher, daß eine Erweiterung einer Klasse ein formaler Untertyp der Klasse ist, der Programmierer sollte aber darüber hinaus noch sicherstellen, daß sie auch eine semantischer Untertyp ist.
Die Dynamische Bindung nicht-statischer Methoden
Die Anwendung "a.description()" aktiviert die Methode "AccountGerman#description". Um die Methode "Account#description" der Oberklasse aufzurufen, könnte man nun auf die Idee kommen, den Ausdruck "( Account )a" zu verwenden, der den Typ der Oberklasse "Account" hat.
Test1.java
public final class Test1
{ final public static void main( String[] args )
{ AccountGerman a = new AccountGerman();
java.lang.System.out.println( a.description() );
java.lang.System.out.println( (( Account )a ).description() ); }}System.out
Standardkonto
Standardkonto
Doch tatsächlich wird bei der Auswertung der Anwendung "(( Account )a ).description()" wieder die Methode "AccountGerman#description" der Klasse des Objekts "a" und nicht der Klasse des Ausdrucks aufgerufen. Dies liegt daran, daß die Zuordnung von Nachrichten zu nicht-statischen Methode nach dem „dynamischen“ Typ des Empfängerobjekts und nicht nach dem „statischen“ Typ des Empfängerausdrucks vorgenommen wird. Die Formung erzeugt einen Ausdruck mit dem Typ "Account" der aber weiterhin ein Objekt des Typs "AccountGerman" referenziert. Die Formung verändert also nur den statischen Typ eines Ausdrucks, nicht aber den dynamischen Typ des referenzierten Objekts.
Der Ausdruck "( Account )a"
_____
.-' '-.
.' '.
/ Von \
.--------------. ; "( Account )a" ;
| ( Account )a --------------------->| referenziertes |
'--------------' ; Objekt ;
\ /
'. .'
'-._____.-'
Account AccountGerman
(statischer Typ) (dynamischer Typ)
Durch eine Typformung wird die Zuordnung von Nachrichten zu Exemplarmethoden nicht beeinflußt. (Es kann nur beeinflußt werden, ob bestimmte Nachrichten überhaupt akzeptiert werden.)
Das folgende Beispielprogramm zeigt die dynamische Bindung noch einmal mit allen beteiligten Klassen und mit einer main -Methode ohne Anwendungen des Selektors "System.out.println".
Test2.java
class Basis
{ void methode(){ java.lang.System.out.println( " Basis#methode" ); }}
class Erweiterung extends Basis
{ void methode(){ java.lang.System.out.println( "Erweiterung#methode" ); }}
public final class Test2
{ final public static void main( final java.lang.String[] args )
{ Erweiterung e = new Erweiterung();
e .methode();
(( Basis )e ).methode(); }}System.out
Erweiterung#methode
Erweiterung#methode
Der erzeugte Bytecode erweckt zunächst den Eindruck, als würde im zweiten Falle die Methode "Basis#methode" gerufen, doch tatsächlich erfolgt hier nur der Aufruf über die Angabe "Basis.methode". Die Instruktion "invokevirtual" ermittelt dann die aktivierte Methode anhand des Typs des Empfängerobjektes.
main [Bytecode, 1.4.2 beta, javap -c -l -s -verbose]
public static final void main(java.lang.String[]);
Signature: ([Ljava/lang/String;)V
Code:
Stack=2, Locals=2, Args_size=1
0: new #2; //class Erweiterung
3: dup
4: invokespecial #3; //Method Erweiterung."<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4; //Method Erweiterung.methode:()V
12: aload_1
13: invokevirtual #5; //Method Basis.methode:()V
16: return
Statischer Bindung statischer Methoden (Verdeckung)
Über einen Ausdruck mit Klassentyp können auch statische Methoden aufgerufen werden. Dies sollte vermieden werden, da die statischen Methoden konzeptuell einer Klasse und nicht einem ihrer Objekte zugehörig sind. Falls dies aber doch geschieht so wird bei Verdeckung einer statischen Methode einer Basisklasse die aufzurufende Methode über den statischen Typ des Ausdrucks ermittelt, so daß eine Typformung hier relevant ist.
Test3.java
class Basis
{ static void methode(){ java.lang.System.out.println( " Basis#methode" ); }}
class Erweiterung extends Basis
{ static void methode(){ java.lang.System.out.println( "Erweiterung#methode" ); }}
public final class Test3
{ final public static void main( final java.lang.String[] args )
{ Erweiterung e = new Erweiterung();
e .methode();
(( Basis )e ).methode(); }}System.out
Erweiterung#methode
Basis#methode
Wenn eine nicht-statische Methode in einer Erweiterung mit derselben Signatur wie eine nicht-statische Methode in der Basisklasse deklariert wird, dann wird die nicht-statische Methode der Basisklasse ersetzt. Wenn eine statische Methode in einer Erweiterung mit derselben Signatur wie eine statische Methode in der Basisklasse deklariert wird, dann wird die nicht-statische Methode der Basisklasse verdeckt.
Verdeckte Methoden werden nach dem „statischen“ Typ des Empfänger-Ausdrucks ermittelt werden und ersetzte nach dem „dynamischen“ Typ des Empfänger-Objekts.
Die statische Auflösung von Überladung
Main.java
class Alpha {}
class Beta extends Alpha {}public final class Main
{ public static String m( final Alpha alpha ){ return "Alpha"; }
public static String m( final Beta beta ){ return "Beta"; }public static void main(String[] args)
{ final Alpha alpha = new Beta();
java.lang.System.out.println( m( alpha )); }}transcript
Alpha
Die dynamische Auflösung von Ersetzung
Main.java
class Alpha { public String m(){ return "Alpha"; } }
final class Beta extends Alpha { public String m(){ return "Beta"; }}public final class Main
{
public static void main(String[] args)
{ final Alpha alpha = new Beta();
java.lang.System.out.println( alpha.m() ); }}transcript
Beta
Kovariantes Ersetzen ist beim Rückgabetyp möglich
Main.java
class Alpha { public Object m(){ return "Alpha"; } }
final class Beta extends Alpha { public String m(){ return "Beta"; }}public final class Main
{
public static void main(String[] args)
{ final Alpha alpha = new Beta();
java.lang.System.out.println( alpha.m() ); }}transcript
Beta
Kovariantes Ersetzen ist nicht beim Parametertyp möglich
Dieses Thema wird in einem anderen Abschnitt dieser Lektion ausführlicher behandelt.
Main.java
class Alpha { public void m( Object o ){ java.lang.System.out.println( "Alpha" ); } }
class Beta extends Alpha { public void m( String s ){ java.lang.System.out.println( "Beta" ); }}public final class Main
{
public static void main(String[] args)
{ final Alpha beta = new Beta();
beta.m( "beta" ); }}transcript
Alpha
Veränderung der Methodengattung
Eine Veränderung der Gattung einer Methode ist nicht möglich. In einer Erweiterung einer Basisklasse kann also keine statische Methode mit derselben Signatur wie eine nicht-statische Methode der Basisklasse deklariert werden. Umgekehrt kann in einer Erweiterung einer Basisklasse kann keine nicht-statische Methode mit derselben Signatur wie eine statische Methode der Basisklasse deklariert werden.
Statischer Zuordnung überladener Methodennamen
Methoden mit unterschiedlichen Signaturen sind unabhängig voneinander. Wird in der Erweiterung eine Methode mit dem gleichen Namen (Selektor) aber anderer Signatur wie eine Methode in der Basisklasse deklariert, so beeinflussen diese Deklarationen einander nicht.
Überladung in der Erweiterung
In einer Erweiterung kann ein Methodennamen auch überladen werden.
Test4.java
class Basis
{ void exemplarmethode()
{ java.lang.System.out.println( "Basis#exemplarmethode" ); }
static void klassenmethode()
{ java.lang.System.out.println( "Basis#klassenmethode" ); }}
final class Erweiterung extends Basis
{ void exemplarmethode( double i )
{ java.lang.System.out.println( "Erweiterung#exemplarmethode" ); }
static void klassenmethode( double i )
{ java.lang.System.out.println( "Erweiterung#klassenmethode" ); }}
public final class Test4
{ private static void zeile( String s )
{ java.lang.System.out.print( s + " " ); }
public static void main( String[] args )
{ Erweiterung e = new Erweiterung();
;; zeile( "e0" ); e .exemplarmethode( 2. );
// zeile( "e1" ); (( Basis )e ).exemplarmethode( 2. );
;; zeile( "e2" ); e .exemplarmethode( );
;; zeile( "e3" ); (( Basis )e ).exemplarmethode( );
;; zeile( "k0" ); e .klassenmethode ( 2. );
// zeile( "k1" ); (( Basis )e ).klassenmethode ( 2. );
;; zeile( "k2" ); e .klassenmethode ( );
;; zeile( "k3" ); (( Basis )e ).klassenmethode ( ); }}System.out
e0 Erweiterung#exemplarmethode
e2 Basis#exemplarmethode
e3 Basis#exemplarmethode
k0 Erweiterung#klassenmethode
k2 Basis#klassenmethode
k3 Basis#klassenmethode
Die Anwendung in der Zeile "e0" aktiviert die Exemplarmethode, die in der Erweiterung definiert wurde. Die Deklaration einer Methode mit gleichem Namen aber anderer Signatur in der Basisklasse stört dies nicht.
In der Zeile "e1" wird dieselbe Nachricht wie in der vorherigen Zeile an einen Ausdruck geschickt, dessen Typ die Basisklasse ist. Obwohl der Typ des Exemplars wie zuvor ist, wird nun zur Übersetzungszeit geprüft, ob der Typ des Ausdrucks die Operation "exemplarmethode( double )" überhaupt enthält. Da dies nicht so ist, wird diese Zeile nicht übersetzt und daher dadurch auch keine Methode aktiviert (deswegen wurde diese Zeile auskommentiert).
In den beiden nächsten Zeilen wird die Exemplarmethode der Basisklasse aufgerufen.
Die Situation bei den statischen Methoden ist entsprechend.
Das Schlüsselwort "super"
- Aussprachehinweise
- super ˈsupɚ
Die Methode "AccountGerman#description" ergibt eine deutschsprachige Beschreibung ihres Kontos. Es kann aber auch wünschenswert sein, hinter dieser deutschsprachigen Kurzbeschreibung die englische Kurzbeschreibung in Klammern anzugeben. Dazu könnte die Methode "AccountGerman#description" die Methode "Account#description" aufrufen, was aber nicht direkt möglich ist, da die Anwendung "this.description()" die Methode "AccountGerman#description" selber wieder aufriefe. Auch eine Formung "( Account )this.description()" erlaubt es nicht, die Methode "Account#description" aufzurufen, da die Zuordnung der Nachricht zu einer nicht-statischen Methode immer über den dynamischen Typ des Objekts erfolgt, der durch die Formung nicht verändert werden kann.
Um nun wirklich die Methode der Basisklasse aufzurufen, kann das Schlüsselwort "this" durch das Schlüsselwort "super" ersetzt werden, welches zwar ebenfalls für das Objekt "this" steht, aber die Nachrichten an die Basisklasse weiterleitet, auch wenn sie zu nicht-statischen Methoden gehören.
AccountGerman1.java
class AccountGerman1 extends Account
{ /** Retrieve a description of the account.
@return the description of the account */
String description()
{ return "Standardkonto (" + super.description() + ")"; }}Test5.java
public final class Test5
{ final public static void main( String[] args )
{ AccountGerman1 a = new AccountGerman1();
a.deposit( "20" ); java.lang.System.out.println( a.description() );
a.deposit( "10" ); java.lang.System.out.println( a.balance() ); }}System.out
Standardkonto (standard account)
30.0
Wenn die Basisklasse selber eine Erweiterung eines Basisbasisklasse ist, so liegt es nahe zu vermuten, daß eine in der Basisklasse ersetzte Methode der Basisbasisklasse mit dem Quelltext "super.super" oder einem ähnlichen Quelltext erreicht werden kann. Dies ist aber nicht möglich, es würde auch die Kapselung der Basisklasse zu sehr durchbrechen, da der Aufruf von dort mit Bedacht ersetzten Methoden, höchst fehleranfällig ist. Mit dem Schlüsselwort "super" kann also immer nur eine Methode der direkten Basisklasse einer Erweiterung aufgerufen werden.
Zugriff auf statische Methoden der Basisklasse
Aus einer statischen Methode heraus kann weder das Schlüsselwort "this" noch das Schlüsselwort "super" verwendet werden, da diese immer ein aktuelles Objekt voraussetzen. Wenn aus einer statischen Methode eine verdeckte Methode der Basisklasse aufgerufen werden soll, so kann eine entsprechende Nachricht an eine Angabe der Klasse geschickt werden, wie in der Versendung "Basis.klassenmethode()".
In einer Exemplarmethode kann sowohl das Schlüsselwort "super" als auch eine Formung mit dem Operator "( Basis )" verwendet werden, um eine verdeckte statische Klassenmethode zu erreichen. Die zweite Möglichkeit ist zu bevorzugen, da statische Klassenmethoden auch über eine statische Klassenangabe und nicht über nicht-statische Variablen aufgerufen werden sollten.
Test4.java
class Basis
{ static void klassenmethode()
{ java.lang.System.out.println( "Basis#klassenmethode" ); }}
class Erweiterung extends Basis
{ static void klassenmethode()
{ // static context here,
// but "super" and "this" are non-static variables
// super.klassenmethode();
// (( Basis )this ).klassenmethode();
Basis.klassenmethode();
java.lang.System.out.println( "Erweiterung#klassenmethode" ); }
void exemplarmethode()
{ super.klassenmethode();
(( Basis )this ).klassenmethode();
java.lang.System.out.println( "Erweiterung#exemplarmethode" ); }}
public final class Test4
{ public static void main( String[] args )
{ Erweiterung e = new Erweiterung();
e.klassenmethode ();
e.exemplarmethode (); }}System.out
Basis#klassenmethode
Erweiterung#klassenmethode
Basis#klassenmethode
Basis#klassenmethode
Erweiterung#exemplarmethode
Ersetzungsblockade für Methoden mit "final"
Wenn es verhindert werden soll, daß eine Methode in Erweiterungen ersetzt wird, dann kann diese Methode mit dem Methodenmodifizierer "final" gekennzeichnet werden. Ersetzungen sind dann nicht erlaubt.
Basis.java
class Basis
{ final void exemplarmethode()
{ java.lang.System.out.println( "Basis#exemplarmethode" ); }}
class Erweiterung extends Basis
{ void exemplarmethode()
{ java.lang.System.out.println( "Erweiterung#exemplarmethode" ); }}Konsole
Basis.java:5: exemplarmethode() in Erweiterung cannot override exemplarmethode() in Basis; overridden method is final
Gelegentlich wird empfohlen, Methoden als "final" zu kennzeichnen, weil sie dann schneller aufgerufen werden könnten. Bei neueren Java -Implementationen ist solch ein Geschwindigkeitsvorteil aber nicht mehr oder nicht mehr in dem Maße erkennbar wie bei älteren Java -Implementationen. Daher sollte dieser nicht ohne weitere Untersuchungen zum Anlaß genommen werden, etwa möglichst viele Methoden mit dem Methodenmodifizierer "final" zu kennzeichnen. Nur wenn es einen guten, sich aus dem Begriff einer Klasse ergebenden, Grund dafür gibt, daß eine Methode nicht ersetzt werden soll, dann sollte dieser Methodenmodifizierer verwendet werden.
Ersetzungsblockade für Klassen mit "final"
Wenn es verhindert werden soll, daß eine Klasse erweitert wird, dann kann diese Klasse mit dem Klassenmodifizierer "final" gekennzeichnet werden. Die Verwendung des Namens solch einer Klasse in einer extends -Klausel ist dann nicht erlaubt.
Basis.java
final class Basis {} class Erweiterung extends Basis {}
Konsole
Test.java:1: cannot inherit from final Basis
final class Basis {} class Erweiterung extends Basis {}
^
Exemplare einer Erweiterung können ja anstelle eines Exemplars einer Basisklasse verwendet werden. Wenn eine Standardklasse, wie die Klasse "String", ersetzt werden könnte, dann könnte eine beliebige Funktionalität in der Erweiterung implementiert werden, die möglicherweise die Operationen der Klasse "String" nicht spezifikationstreu ersetzt. Teile der Java -Standard-Pakete verlassen sich aber darauf, daß ein von einem String-Ausdruck referenziertes Objekt die Operationen der Klasse "String" spezifikationstreu implementiert. Eine finale Klasse ist vor solch einer Störung geschützt.
Im Programm "BadExtension.java" wird die Methode "doppel( int )" in der Erweiterung ersetzt. Wenn man annimmt, daß die Dokumentation der Operation "doppel" der Basis spezifiziert, daß das Ergebnis das Doppelte eines hinreichend kleinen Argumentwerts ist, dann ist die Methodendeklaration in der Erweiterung nicht spezifikationstreu. Die Operation "printdoppel( int, Basis )" verwendet ein Exemplar der Basisklasse, um das Doppelte eines hinreichend kleinen Argumentwerts auszugeben. In der Hauptmethode "main" wird der Operation "printdoppel( int, Basis )" dann der Wert "7" und ein Exemplar der Erweiterung übergeben, das von dem Parameter "b" der Methode "Client#printdoppel( int, Basis )" referenziert werden kann, weil es ein Untertyp des Parametertyps "Basis" ist.
Da die Erweiterung aber nicht spezifikationstreu ist, arbeitet die Operation "printdoppel( int, Basis )" dann nicht richtig und gibt für hinreichend kleine Werte des ersten Arguments das Vierfache aus. Wenn die Klasse "Basis" mit dem Klassenmodifizierer "final" gekennzeichnet worden wäre, dann könnte die Methode "Client#printdoppel( int, Basis )" jedoch sicher sein, ein Exemplar der Klasse "Basis" zu referenzieren. Dies wäre dann vorteilhaft, wenn der Autor dieser Methode der Spezifikationstreue der Basisklasse mehr vertraut als der Spezifikationstreue eventueller Erweiterungen, etwa weil ihm der Autor der Basisklasse bekannt ist, aber beliebige Autoren Erweiterungen schreiben könnten.
NotFinal.java
class Basis
{ /** Returns twice the argument value.
@param i must be less than 1073741823
@return twice the argument value */
int doppel( final int i ){ return 2 * i; }}
class Erweiterung extends Basis
{ int doppel( final int i ){ return 4 * i; }}
class Client
{ /** Prints twice the argument value.
@param i must be less than 1073741823 */
void printdoppel( final int i, final Basis b )
{ java.lang.System.out.println( b.doppel( i )); }}
public final class BadExtension
{ public static void main( final String[] args )
{ new Client().printdoppel( 7, new Erweiterung()); }}System.out
28
Aufrufe ersetzter Methoden in der Basisklasse
Wenn in einer Basisklassenmethode eine „Methode der Basisklasse aufgerufen wird“, dann wird dadurch möglicherweise tatsächlich eine Methode der Erweiterung aufgerufen. Dies geschieht, dann wenn die aufgerufene Methode in der Erweiterung ersetzt wurde.
In dem folgenden Programmbeispiel wird in der Methode "versendung" „die Methode "exemplarmethode" aufgerufen“. Doch wenn der Typ des Objekts "this" eine Erweiterung ist, in der diese aufgerufene Methode ersetzt wurde, dann wird hier anhand des Objekttyps tatsächlich die ersetzte Methode der Erweiterung aufgerufen. Daher gibt das Programm "Beispiel5.java" einmal den Text "Basis#exemplarmethode" und einmal den Text "Erweiterung#exemplarmethode" aus.
Beispiel5.java
class Basis
{ static void klassenmethode()
{ java.lang.System.out.println( " Basis#klassenmethode" ); }
void exemplarmethode()
{ java.lang.System.out.println( " Basis#exemplarmethode" ); }
final void finalmethode()
{ java.lang.System.out.println( " Basis#finalmethode" ); }
void versendung()
{ this.klassenmethode();
this.exemplarmethode();
this.finalmethode(); }}
class Erweiterung extends Basis
{ static void klassenmethode()
{ java.lang.System.out.println( "Erweiterung#klassenmethode" ); }
void exemplarmethode()
{ java.lang.System.out.println( "Erweiterung#exemplarmethode" ); }}
public final class Beispiel5
{ public static void main( String[] args )
{ Basis b = new Basis (); b.versendung();
Erweiterung e = new Erweiterung(); e.versendung(); }}System.out
Basis#klassenmethode
Basis#exemplarmethode
Basis#finalmethode
Basis#klassenmethode
Erweiterung#exemplarmethode
Basis#finalmethode
Das Verhalten bei diesem Aufruf zeigt, daß die eingangs in Anführungszeichen wiedergegeben Ansicht, es werde in der Methode "versendung" eine bestimmte Methode aufgerufen, falsch ist. Die tatsächlich aktivierte Methode hängt eben von Umständen ab, die durch den Quelltext der Methode "versendung" nicht festgelegt sind.
Deswegen ist es besser zu sagen, hier werde eine Nachricht an einen Ausdruck geschickt. An welches Objekt diese Nachricht dann weitergeleitet wird, hängt vom Wert dieses Ausdrucks ab. Dieser Wert ist zunächst eine Referenz, die ein bestimmtes Objekt referenziert. Die Nachricht führt dann zur Aktivierung der zur Signatur der Nachricht passenden Methode dieses Objekts.
Welches Objekt diese Nachricht erhält und welche Methode dadurch schließlich aktiviert wird hängt von Umständen ab, die im allgemeinen erst zur Laufzeit des Programms bekannt sind, eben vom Wert des Ausdrucks "this". Es wird also nicht eine zur Schreibzeit bestimmte Methode aufgerufen.
Eine solche Nachrichtenversendung an ein Objekt, dessen Typ erst zur Laufzeit feststeht, wird als dynamische Versendung oder dynamische Bindung bezeichnet. Der Übersetzer kann bei solch einer dynamischen Versendung nicht einfach einen Aufruf einer bestimmten Methode erzeugen, sondern muß einen Aufruf einer unbestimmten Methode mit der Instruktion "invokevirtual" erzeugen, die zur Laufzeit anhand der Nachricht und des Typs des Empfängerobjekts die zu aktivierende Methode ermittelt. Ein Aufruf einer statischen Klassenmethode wird hingegen mit der Instruktion "invokestatic" durchgeführt.
Konsole
javap -c Basis
(...)
Method void versendung()
0 invokestatic #6 <Method void klassenmethode()>
3 aload_0
4 invokevirtual #7 <Method void exemplarmethode()>
7 aload_0
8 invokevirtual #8 <Method void finalmethode()>
11 return
Oft ist die Instruktion "invokevirtual" etwas langsamer als eine Aufruf einer im Programmtext festgelegten Methode, da sie zur Laufzeit erst anhand des Objekttyps die aufzurufende Methode ermitteln muß.
Die Versendung einer Nachricht an ein Objekt sollte im allgemeinen nicht als „Aufruf einer Methode“ angesehen werden, weil durch den Quelltext eben nicht eine bestimmte Methode festgelegt ist. Diese wird im allgemeinen erst zur Laufzeit anhand des Typs des Empfängerobjekts ermittelt.
Ersetzung von Feldern (Exemplarvariablen)
Felder können in Java nicht ersetzt werden. Sie werden wie statische Methoden verdeckt. Dafür reicht es ein Feld mit dem gleichen Namen in einer Erweiterung zu deklarieren, es braucht nicht den gleichen Typ wie das gleichnamige Feld der Basisklasse haben (Felder haben keine Signaturen).
Welches Feld gemeint ist, wird durch den statischen Typ des Ausdrucks bestimmt, dessen Feld verwendet wird.
Beispiel6.java
public final class Beispiel6
{ public static void main( String[] args )
{ Basis b = new Basis ();
java.lang.System.out.println( "main: " + b.i ); b.printi();
Erweiterung e = new Erweiterung();
java.lang.System.out.println( "main: " + e.i );
java.lang.System.out.println( "main: " + (( Basis )e ).i );
e.printi(); e.printi1(); }}
class Basis
{ int i = 4; void printi()
{ java.lang.System.out.println( "printi: " + i );
java.lang.System.out.println( "printi: " + this.i ); }}
final class Erweiterung extends Basis
{ int i = 5; void printi1()
{ java.lang.System.out.println( "printi1: " + i );
java.lang.System.out.println( "printi1: " + this.i );
java.lang.System.out.println( "printi1: " + (( Basis )this ).i );
java.lang.System.out.println( "printi1: " + super.i ); }}System.out
main: 4
printi: 4
printi: 4
main: 5
main: 4
printi: 4
printi: 4
printi1: 5
printi1: 5
printi1: 4
printi1: 4
Ersetzung und Verdeckung
Als Ersetzen (overriding ) einer Exemplarmethode einer Klasse wird die Reimplementation dieser Methode in einer Erweiterung jener Klasse bezeichnet. Dabei wird ein Exemplarmethode der gleichen Signatur in der Erweiterung deklariert. Welche der beiden Methoden aufgerufen wird, wenn man eine Nachricht mit der Signatur der Methoden an einen Referenzausdruck versendet, wird durch den Typ des Objektes dieses Referenzausdrucks bestimmt.
Bei der Verdeckung (hiding ) wird in einer Erweiterung ein Feld mit dem gleichen Namen wie ein Feld der Basisklasse oder eine statische Methode mit der gleichen Signatur wie eine statische Methode der Basisklasse deklariert. Der verdeckende Eintrag wird dann verwendet, wenn der statische Typ des Ausdrucks auf der linken Seite des Punktes "." die Erweiterung ist. Ist der statische Typ jedoch die Basisklasse, so werden deren Einträge verwendet.
Zur Auflösung wird beim Ersetzen also der dynamische Typ des Objekts links vom Punkt "." verwendet und beim Verdecken der statische Typ der Angabe links vom Punkt ".".
Die Nachrichtenzustellung
Man kann unterscheiden zwischen
- der Zustellung nach dem Objekttyp bei Referenzen als Empfängern dynamischer Operationen,
- der Zustellung nach dem Ausdrucktyp bei Empfängern statischer Operationen und
- der Berücksichtigung des Ausdruckstyps bei Argumenten von Exemplarerzeugungsausdrücken und Methoden.
In manchen anderen Sprachen kann eine Nachricht auch mehrere Empfänger haben: Bei den sogenannten Multioperationen („Multimethoden“) wird die Nachricht dann unter Berücksichtigung des Typs aller (mehrerer) Empfängerobjekte zugestellt. Das ist in Java nicht möglich und muß bei Bedarf vom Programmierer durch das „Besuchermuster“ (“visitor pattern ”) nachgebildet werden.
Polymorphie in der Oberklasse
In einer Oberklasse werden unter Umständen Methoden der Unterklasse aufgerufen, da hier die späte Bindung zum Einsatz kommt. Dadurch kann eine Unterklasse einer Oberklasse beschädigen. Dies ist einer der Gründe, aus denen Klassen nicht ohne final deklariert werden sollten, außer die Vererbung wurde von Anfang an in diesem Sinne eingeplant, daß solche Effekte durch Unterklassen erwünscht sind.
Main.java
class Base
{public void m()
{ java.lang.System.out.println( "A" );
this.m(); }}
class Extension extends Base
{public void m()
{ java.lang.System.out.println( "B" ); }public void main()
{ super.m(); }}public final class Main
{public static void main( final java.lang.String[] args )
{ new Extension().main(); }}transcript
A
B
super.super
Es gibt kein »super.super« (dies könnte die Semantik der Oberklasse durchbrechen, welche sich bewußt entschieden hat, eine bestimmte Methode zu ersetzen.
Beispiel
Main.java
class Base
{ public void m( final java.lang.Object o )
{ java.lang.System.out.println( "Base + " + o ); }}class Derived extends Base
{ public void m( final java.lang.String o )
{ java.lang.System.out.println( "Derived + " + o ); }}public final class Main
{ public static void main( final java.lang.String[] args )
{ final Base o = new Derived();
o.m( "alpha" ); }}transcript
Base + alpha
Der Compiler sucht in dem Typ des Ausdrucks o nach einer passenden Methode und findet dort m(Object). Daher wird m(Object) aufgerufen. Zur Laufzeit wird dann Base.m aufgerufen, da Derived.m nicht die passende Signatur m(Object) hat.
Main.java
class Base
{ public void m( final java.lang.Object o )
{ java.lang.System.out.println( "Base + " + o ); }}final class Derived extends Base
{ public void m( final java.lang.String o )
{ java.lang.System.out.println( "Derived + " + o ); }}public final class Main
{ public static void main( final java.lang.String[] args )
{ final Derived o = new Derived();
o.m( "alpha" ); }}transcript
Derived + alpha
Der Compiler sucht in dem Typ des Ausdrucks o nach einer passenden Methode und findet dort m(String). Daher wird m(String) aufgerufen. Zur Laufzeit wird dann Derived.m() aufgerufen, da dies die passende Signatur hat.
Main.java
class Base
{ public void m( final java.lang.Object o )
{ java.lang.System.out.println( "Base + " + o ); }}class Derived extends Base
{ public void m( final java.lang.Object o )
{ java.lang.System.out.println( "Derived + " + o ); }}public final class Main
{ public static void main( final java.lang.String[] args )
{ final Base o = new Derived();
o.m( "alpha" ); }}transcript
Derived + alpha
Der Compiler sucht in dem Typ des Ausdrucks o nach einer passenden Methode und findet dort m(Object). Daher wird m(Object) aufgerufen. Da dies ersetzt ist, wird dann zur Laufzeit Derived.m() aufgerufen.
Übungsfragen
- Übungsfrage
- Wird das folgende Programm vom Übersetzer akzeptiert und was gibt es gegebenenfalls aus?
Uebungsfrage.java
public final class Uebungsfrage
{ public static void main( String[] args )
{ (( Basis )new Erweiterung()).m(); }}class Basis
{ int i = 4; void m(){ java.lang.System.out.println( i ); }}final class Erweiterung extends Basis
{ int i = 5; void m(){ java.lang.System.out.println( i ); }}- Übungsfrage 1
- Wird das folgende Programm vom Übersetzer akzeptiert und was gibt es gegebenenfalls aus?
Uebungsfrage1.java
public final class Uebungsfrage1
{ public static void main( String[] args )
{ final Basis b = new Erweiterung(); b.m(); }}class Basis
{ void m(){ java.lang.System.out.println( "Basis" ); }}final class Erweiterung extends Basis
{ void m(){ java.lang.System.out.println( "Erweiterung" ); }}- Übungsfrage 2
- Wird das folgende Programm vom Übersetzer akzeptiert und was gibt es gegebenenfalls aus?
Uebungsfrage2.java
public final class Uebungsfrage2
{ public static void main( String[] args )
{ final Basis b = new Erweiterung(); b.m(); }}class Basis
{ static void m(){ java.lang.System.out.println( "Basis" ); }}final class Erweiterung extends Basis
{ static void m(){ java.lang.System.out.println( "Erweiterung" ); }}- Übungsfrage 3
- Wird das folgende Programm vom Übersetzer akzeptiert und was gibt es gegebenenfalls aus?
Uebungsfrage3.java
public final class Uebungsfrage3
{ public static void main( String[] args )
{ final Basis b = new Erweiterung(); b.m(); }}class Basis
{ void m(){ java.lang.System.out.println( "Basis" ); }}final class Erweiterung extends Basis
{ void m( String s ){ java.lang.System.out.println( "Erweiterung" ); }}- Übungsfrage 4
- Wird das folgende Programm vom Übersetzer akzeptiert und was gibt es gegebenenfalls aus?
Uebungsfrage4.java
public final class Uebungsfrage4
{ public static void main( String[] args )
{ final Erweiterung e = new Erweiterung(); e.m(); }}class Basis
{ final String t = "Basis"; void m(){ this.m( t ); }}final class Erweiterung extends Basis
{ final String t = "Erweiterung";
void m( String s ){ java.lang.System.out.println( s ); }}- Übungsfrage 5
- Wird das folgende Programm vom Übersetzer akzeptiert und was gibt es gegebenenfalls aus?
Uebungsfrage5.java
public final class Uebungsfrage5
{ public static void main( String[] args )
{ Erweiterung e = new Erweiterung(); e.m( "main" ); }}class Basis
{ final String t = "Basis"; void m(){ m(); }}final class Erweiterung extends Basis
{ final String s = "Erweiterung";
void m( String s ){ java.lang.System.out.println( t ); }}- Übungsfrage 6
- Wird das folgende Programm vom Übersetzer akzeptiert und was gibt es gegebenenfalls aus?
Uebungsfrage6.java
public final class Uebungsfrage6
{ public static void main( String[] args )
{ Erweiterung e = new Erweiterung(); e.m(); }}class Basis
{ static void m(){ java.lang.System.out.println( "Basis" ); }}final class Erweiterung extends Basis
{ void m(){ java.lang.System.out.println( "Erweiterung" ); }}- Übungsfrage 7
- Wird das folgende Programm vom Übersetzer akzeptiert und was gibt es gegebenenfalls aus?
Uebungsfrage7.java
public final class Uebungsfrage7
{ public static void main( String[] args )
{ Erweiterung e = new Erweiterung(); e.m(); }}class Basis
{ void m(){ java.lang.System.out.println( "Basis" ); }}final class Erweiterung extends Basis
{ void m(){ super.m(); java.lang.System.out.println( "Erweiterung" ); }}- Übungsfrage 8
- Wird das folgende Programm vom Übersetzer akzeptiert und was gibt es gegebenenfalls aus?
Uebungsfrage8.java
public final class Uebungsfrage8
{ public static void main( String[] args )
{ Erweiterung e = new Erweiterung(); e.m(); }}class Basis
{ String t;
void m(){ t = "Basis"; m( "Java" ); }
void m( String s ){ java.lang.System.out.println( t ); }}final class Erweiterung extends Basis
{ void m(){ super.m(); java.lang.System.out.println( "Erweiterung" ); }
void m( String s ){ java.lang.System.out.println( s ); }}- Übungsfrage 9
- Wird das folgende Programm vom Übersetzer akzeptiert und was gibt es gegebenenfalls aus?
Uebungsfrage9.java
class Base { Base(){ method(); } void method() {} }
public final class Main extends Base
{ double field = 0.0;void method(){ field = 1.0; }
public static void main( final java.lang.String[] args )
{ final Main o = new Main(); java.lang.System.out.println( o.field ); }}- Übungsfrage 10
Warum wird hier nicht dynamisch aufgelöst?
Main.java
class Alpha { public static String m(){ return "Alpha"; } }
final class Beta extends Alpha { public static String m(){ return "Beta"; }}public final class Main
{
public static void main(String[] args)
{ final Alpha alpha = new Beta();
java.lang.System.out.println( alpha.m() ); }}transcript
Alpha
- Übungsaufgabe
- Schreiben Sie eine Klasse "English" mit drei nicht-statischen parameterlosen Wertmethoden, welche das zu ihrem Namen gehörende englische Wort (“house ”, “book ”, bzw. “color ”) als Objekt der Klasse "String" zurückgeben und somit zur Übersetzung dienen können.
English [Spezifikation]
String haus()
Returns a word for the German word "Haus".
String buch()
Returns a word for the German word "Book".
String farbe()
Returns a word for the German word "Farbe".
- Schreiben Sie eine weitere Klasse "British", welche dieselbe Spezifikation erfüllt, jedoch britische Wörter zurückgibt (dabei wird “colour ” für „Farbe“ verwendet). Die Klasse "British" soll dabei möglichst viele Methoden von der Klasse "English" übernehmen.
Zum Erweitern und Implementieren
java.lang.Override (Seit Java SE 6 auch für Schnittstellenimplementation verwendbar)