Lernpfad:Objektorientierte Programmierung mit Java/Vererbung: Unterschied zwischen den Versionen
Jneug (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
Jneug (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
||
| (41 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) | |||
| Zeile 1: | Zeile 1: | ||
{{Navigation}} | {{Navigation}} | ||
''Vererbung'' ist eines der wichtigsten Konzepte der | ''Vererbung'' ist eines der wichtigsten Konzepte der objektorientierten Programmierung. Mit ihr ist es möglich komplexe Problembereiche in sinnvolle Klassenhierarchien zu unterteilen. Dabei werden Eigenschaften und Fähigkeiten, die mehrere Klassen besitzen, nur einmal in einer '''Oberklasse''' implementiert und an eine oder mehrere '''Unterklassen''' weiter ''vererbt''. | ||
== Vererbung im Überblick == | |||
<center> | |||
{{#ev:youtube|https://www.youtube.com/watch?v=XICQ8TFOgUM}} | |||
</center> | |||
== Ober- und Unterklassen == | == Ober- und Unterklassen == | ||
Am besten verdeutlicht dies ein Beispiel: Nehmen wir an, es soll ein geometrisches Zeichenprogramm nach folgender Beschreibung umgesetzt werden. | Am besten verdeutlicht dies ein Beispiel: Nehmen wir an, es soll ein geometrisches Zeichenprogramm nach folgender Beschreibung umgesetzt werden. | ||
{{Schublade|Es gibt verschiedene Formen wie Rechteck, Dreieck und Quadrat. Jede Form besitzt eine x- und y-Koordinate | {{Schublade|Es gibt verschiedene Formen wie Rechteck, Dreieck und Quadrat. Jede Form besitzt eine x- und y-Koordinate und eine Richtung. Formen können versetzt und gedreht werden. Rechtecke besitzen zwei Seitenlängen. Bei Quadraten sind beide Seitenlängen gleich. Dreiecke werden über zwei Punkte festgelegt, die auch eine x- und y-Koordinate besitzen. Die dritte Ecke wird durch die Position des Dreiecks festgelegt.}} | ||
Ein erstes Implementierungsdiagramm ohne Vererbung könnte so aussehen: | |||
[[Datei:UML Vererbung 1.jpg|center]] | |||
Es ergeben sich einige Dopplungen bei Eigenschaften und Methoden. Jede Klasse besitzt x- und y-Koordinaten und eine Richtung als Attribut, sowie die Methoden <code>versetzen</code> und <code>drehen</code>. Diese müssten bei der Implementierung jeweils exakt gleich umgesetzt werden, was neben viel Aufwand sehr fehleranfällig ist. | |||
< | |||
Stattdessen können im Problembereich logische Zusammenhänge zwischen den Klassen identifiziert werden. Jede Klasse ''ist eine'' <code>Form</code>. Ein <code>Quadrat</code> ''ist ein'' <code>Rechteck</code>. Die konkreten Form-Klassen haben alle bestimmte Attribute und Methoden gemeinsam. | |||
<code>Form</code> ist also eine '''Oberklasse''' von <code>Dreieck</code> und <code>Rechteck</code>. <code>Quadrat</code> ist eine '''Unterklasse''' von <code>Rechteck</code>. (Also ist <code>Rechteck</code> wiederum '''Oberklasse''' von <code>Quadrat</code>.) Die Methoden müssen so nur einmal in <code>Form</code> implementiert werden, sind aber durch die Vererbung in allen Objektinstanzen verfügbar. | |||
Das neue Implementierungsdiagramm sieht so aus: | |||
[[Datei:UML Vererbung 2.jpg|center]] | |||
Folgende Anweisungen sind mit Vererbung korrekt, obwohl der Quelltext der Klasse <code>Quadrat</code> die Methode <code>public void versetzen()</code> nirgendwo explizit enthält. | |||
< | <syntaxhighlight lang="java" line=1> | ||
Quadrat q = new Quadrat(10); | |||
q.versetzen(3, 5); | |||
</syntaxhighlight> | |||
{{Aufgabe:Start}} | |||
# Folgende Begriffe sollen in Form eines Klassendiagramms in eine Klassenhierarchie umgesetzt werden: ''Gebäude - Kirche - Einfamilienhaus - Hochhaus - Haus - Bungalow - Dom - Kathedrale''.<br/>Von jedem Gebäude soll die Höhe und die zugelassene Anzahl Bewohner bzw. Besucher abrufbar sein. Die Höhe von Häusern berechnet sich aus der Anzahl der Stockwerke und der Höhe pro Stockwerk. In einem Hochhaus sind pro Stockwerk eine Anzahl Personen zugelassen.<br/>Wähle zu diesem Zweck geeignete Attribute für die einzelnen Klassen. Beachte dabei, welche Attribute von der bzw. den Oberklasse(n) geerbt werden. | |||
# Implementiere die Klassen. Nutze dazu das Vererbungskonzept so weit es geht aus. | |||
{{Aufgabe:End}} | |||
== Vererbung implementieren == | |||
Für die Implementierung von Vererbung gibt es in Java das Schlüsselwort <code>extends</code>. Man ergänzt es bei der Unterklasse direkt hinter dem Klassennamen mit dem Namen der Oberklasse. | |||
<syntaxhighlight lang="java" line=1> | |||
public class Form { | |||
// ... | |||
} | |||
public class Rechteck extends Form { | |||
// ... | |||
} | |||
public class Quadrat extends Rechteck { | |||
// ... | |||
} | |||
</syntaxhighlight> | |||
Hat die Oberklasse ''keinen leeren Konstruktor'' (also keinen Konstruktor ohne Parameter), dann muss die Unterklasse noch den Konstruktor der Oberklasse mithilfe des Schlüsselwortes <code>super</code> ''explizit aufrufen'', um die Parameter zu initialisieren. Dies muss die erste Anweisung im Konstruktor der Unterklasse sein. | |||
<syntaxhighlight lang="java" line=1 highlight="11"> | |||
public class Form { | |||
// Attribute ... | |||
public Form( Punkt pPosition, double pRichtung ) { | |||
position = pPosition; | |||
richtung = pRichtung; | |||
} | |||
// Methoden ... | |||
} | } | ||
</ | public class Rechteck extends Form { | ||
public Rechteck( Punkt pPosition, double pRichtung, int pA, int pB ) { | |||
super(pPosition, pRichtung); | |||
a = pA; | |||
b = pB; | |||
} | |||
} | |||
</syntaxhighlight> | |||
Weitere Verwendungen des Schlüsselwortes <code>super</code> sind im Abschnitt [[#super_und_this|<code>super</code> und <code>this</code>]]. | |||
== Methoden überschreiben == | == Methoden überschreiben == | ||
Beim Überschreiben bekommen abgeleitete Klassen eine eigene Version mindestens einer Methode der Basisklasse. | Beim Überschreiben bekommen abgeleitete Klassen eine eigene Version mindestens einer Methode der Basisklasse. | ||
Betrachte das folgende Klassendiagramm mit einer Ober- und drei erbenden Unterklassen und die dazugehörige Implementierung. | |||
[[Datei:UML Vererbung 3.jpg|center]] | |||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
| Zeile 61: | Zeile 80: | ||
} | } | ||
} | } | ||
class Biene extends Tier { | class Biene extends Tier { | ||
| Zeile 68: | Zeile 86: | ||
} | } | ||
} | } | ||
class Frosch extends Tier { | class Frosch extends Tier { | ||
| Zeile 75: | Zeile 92: | ||
} | } | ||
} | } | ||
class Unbekannt extends Tier { | class Unbekannt extends Tier { | ||
| Zeile 96: | Zeile 112: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Die Unterklassen <code>Biene</code> und <code>Frosch</code> implementieren die Methode <code>public void sagWas()</code>, die auch in der Oberklasse <code>Tier</code> vorhanden ist. Sie '''überschreiben''' die Methode in der Oberklasse und können so ihre Funktion (in diesem Beispiel die Ausgabe) verändern. | |||
{{Aufgabe:Start}} | |||
# Lies den [http://openbook.rheinwerk-verlag.de/javainsel/06_004.html#u6.4.1 "Abschnitt 6.4.1: Methoden in Unterklassen mit neuem Verhalten ausstatten"] im Onlinebuch "Java ist auch eine Insel" bis zur Überschrift "Die Annotation @Override". | |||
# Erstelle ein kleines Beispielprojekt, an dem das Konzept "Überschreiben" erklärt werden kann. | |||
{{Aufgabe:End}} | |||
=== Quiz === | |||
<lückentext> | <lückentext> | ||
Welche Ausgabe wird vom Quelltext oben erzeugt? | |||
Ein Tier sagt '''- Stille -'''<br/> | Ein Tier sagt '''- Stille -'''<br/> | ||
Ein Frosch sagt '''Quak, Quak!'''<br/> | Ein Frosch sagt '''Quak, Quak!'''<br/> | ||
Eine Biene sagt '''Summ, Summ, Summ!'''<br/> | Eine Biene sagt '''Summ, Summ, Summ!'''<br/> | ||
Was sage ich? '''- Stille -()''' | |||
</lückentext > | </lückentext> | ||
== <code>super</code> und <code>this</code> == | |||
Wird eine Methode von einer Unterklasse überschrieben, möchte man dennoch manchmal explizit die überschriebene Methode der Oberklasse aufrufen. Für diese Fälle gibt es das bekannte Schlüsselwort <code>super</code>. Es bezieht sich immer auf ''die Oberklasse der aktuellen Klasse''. Um ''explizit die aktuelle Klasse'' zu referenzieren, kann das Schlüsselwort <code>this</code> benutzt werden. | |||
Im folgenden Beispiel überschriebt die Klasse <code>Hund</code> die Methode <code>public void ausgabe()</code> er Oberklasse <code>Tier</code>, um auch die Rasse auszugeben. Die Ausgabe der Oberklasse soll dennoch gemacht werden, daher wird die Methode der Oberklasse explizit mit <code>super.ausgabe()</code> aufgerufen. | |||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java" line="1"> | ||
class Tier { | class Tier { | ||
// Das Attribut ist private und von der Unterklasse nicht nutzbar. | // Das Attribut ist private und von der Unterklasse nicht nutzbar. | ||
| Zeile 141: | Zeile 163: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
{{Aufgabe:Start}} | |||
# Ergänze eine Klasse <code>HundMitBesitzer</code>, die ein Attribut <code>String besitzer</code> hat, das den Namen des Hundebesitzers speichert. Bei Aufruf der Methode <code>public void ausgabe()</code> soll zunächst die Ausgabe von <code>Hund</code> ausgegeben werden, dann der Name des Besitzers. | |||
{{Aufgabe:End}} | |||
== Das Schlüsselwort <code>final</code> == | == Das Schlüsselwort <code>final</code> == | ||
Möchte man verhindern, dass eine Unterklasse eine Methode überschreibt, dann kann man sie als <code>final</code> deklarieren. | Möchte man verhindern, dass eine Unterklasse eine Methode ''überschreibt'', dann kann man sie als <code>final</code> deklarieren. Versucht eine Unterklasse es dennoch, dann kann das Programm nicht mehr [[wikipedia:Compiler|übersetzt]] werden. | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java" line="1"> | ||
class Tier { | class Tier { | ||
// Das Attribut ist private und von der Unterklasse nicht nutzbar. | // Das Attribut ist private und von der Unterklasse nicht nutzbar. | ||
| Zeile 175: | Zeile 200: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
== Typen von Unterklassen == | |||
Vererbung wirkt sich auch auf den Typ von Objekten einer Unterklasse aus. Eine Unterklasse <code>Quadrat</code> aus dem Beispiel ganz oben hat nicht mehr nur den Datentyp <code>Quadrat</code>, sondern auch den Typ aller ihrer Oberklassen. Ein Quadrat kann somit auch als Rechteck deklariert werden. | |||
<syntaxhighlight lang="java" line=1> | |||
{{ | // Bekannte Deklaration vom Typ Quadrat | ||
Quadrat q = new Quadrat(10); | |||
// Deklaration als Rechteck | |||
Rechteck q = new Quadrat(10); | |||
// Deklaration als Form | |||
Form q = new Quadrat(10); | |||
</syntaxhighlight> | |||
Speichert man ein <code>Quadrat</code> aber als <code>Form</code> ab, dann verliert es alle Methoden, die vom <code>Rechteck</code> und <code>Quadrat</code> implementiert wurden. Es sind nur noch die Methoden der <code>Form</code> aufrufbar. | |||
{{Kasten|Allgemeiner: Eine Objektreferenz hat nur diejenigen Methoden verfügbar, die von ihrem deklarierten Typ oder dessen Oberklassen implementiert werden.|Farbe={{Farbe:Info}}}} | |||
{{Inhalt/Übersicht}} | |||
Aktuelle Version vom 7. Februar 2022, 23:38 Uhr
Vererbung ist eines der wichtigsten Konzepte der objektorientierten Programmierung. Mit ihr ist es möglich komplexe Problembereiche in sinnvolle Klassenhierarchien zu unterteilen. Dabei werden Eigenschaften und Fähigkeiten, die mehrere Klassen besitzen, nur einmal in einer Oberklasse implementiert und an eine oder mehrere Unterklassen weiter vererbt.
Vererbung im Überblick
Ober- und Unterklassen
Am besten verdeutlicht dies ein Beispiel: Nehmen wir an, es soll ein geometrisches Zeichenprogramm nach folgender Beschreibung umgesetzt werden.
Es gibt verschiedene Formen wie Rechteck, Dreieck und Quadrat. Jede Form besitzt eine x- und y-Koordinate und eine Richtung. Formen können versetzt und gedreht werden. Rechtecke besitzen zwei Seitenlängen. Bei Quadraten sind beide Seitenlängen gleich. Dreiecke werden über zwei Punkte festgelegt, die auch eine x- und y-Koordinate besitzen. Die dritte Ecke wird durch die Position des Dreiecks festgelegt.
Ein erstes Implementierungsdiagramm ohne Vererbung könnte so aussehen:
Es ergeben sich einige Dopplungen bei Eigenschaften und Methoden. Jede Klasse besitzt x- und y-Koordinaten und eine Richtung als Attribut, sowie die Methoden versetzen und drehen. Diese müssten bei der Implementierung jeweils exakt gleich umgesetzt werden, was neben viel Aufwand sehr fehleranfällig ist.
Stattdessen können im Problembereich logische Zusammenhänge zwischen den Klassen identifiziert werden. Jede Klasse ist eine Form. Ein Quadrat ist ein Rechteck. Die konkreten Form-Klassen haben alle bestimmte Attribute und Methoden gemeinsam.
Form ist also eine Oberklasse von Dreieck und Rechteck. Quadrat ist eine Unterklasse von Rechteck. (Also ist Rechteck wiederum Oberklasse von Quadrat.) Die Methoden müssen so nur einmal in Form implementiert werden, sind aber durch die Vererbung in allen Objektinstanzen verfügbar.
Das neue Implementierungsdiagramm sieht so aus:
Folgende Anweisungen sind mit Vererbung korrekt, obwohl der Quelltext der Klasse Quadrat die Methode public void versetzen() nirgendwo explizit enthält.
Quadrat q = new Quadrat(10);
q.versetzen(3, 5);
- Folgende Begriffe sollen in Form eines Klassendiagramms in eine Klassenhierarchie umgesetzt werden: Gebäude - Kirche - Einfamilienhaus - Hochhaus - Haus - Bungalow - Dom - Kathedrale.
Von jedem Gebäude soll die Höhe und die zugelassene Anzahl Bewohner bzw. Besucher abrufbar sein. Die Höhe von Häusern berechnet sich aus der Anzahl der Stockwerke und der Höhe pro Stockwerk. In einem Hochhaus sind pro Stockwerk eine Anzahl Personen zugelassen.
Wähle zu diesem Zweck geeignete Attribute für die einzelnen Klassen. Beachte dabei, welche Attribute von der bzw. den Oberklasse(n) geerbt werden. - Implementiere die Klassen. Nutze dazu das Vererbungskonzept so weit es geht aus.
Vererbung implementieren
Für die Implementierung von Vererbung gibt es in Java das Schlüsselwort extends. Man ergänzt es bei der Unterklasse direkt hinter dem Klassennamen mit dem Namen der Oberklasse.
public class Form {
// ...
}
public class Rechteck extends Form {
// ...
}
public class Quadrat extends Rechteck {
// ...
}
Hat die Oberklasse keinen leeren Konstruktor (also keinen Konstruktor ohne Parameter), dann muss die Unterklasse noch den Konstruktor der Oberklasse mithilfe des Schlüsselwortes super explizit aufrufen, um die Parameter zu initialisieren. Dies muss die erste Anweisung im Konstruktor der Unterklasse sein.
public class Form {
// Attribute ...
public Form( Punkt pPosition, double pRichtung ) {
position = pPosition;
richtung = pRichtung;
}
// Methoden ...
}
public class Rechteck extends Form {
public Rechteck( Punkt pPosition, double pRichtung, int pA, int pB ) {
super(pPosition, pRichtung);
a = pA;
b = pB;
}
}
Weitere Verwendungen des Schlüsselwortes super sind im Abschnitt super und this.
Methoden überschreiben
Beim Überschreiben bekommen abgeleitete Klassen eine eigene Version mindestens einer Methode der Basisklasse.
Betrachte das folgende Klassendiagramm mit einer Ober- und drei erbenden Unterklassen und die dazugehörige Implementierung.
class Tier {
public void sagWas() {
System.out.println("- Stille -");
}
}
class Biene extends Tier {
public void sagWas() {
System.out.println("Summ, Summ, Summ!");
}
}
class Frosch extends Tier {
public void sagWas() {
System.out.println("Quak! Quak!");
}
}
class Unbekannt extends Tier {
}
public class Zoo {
public static void main(String[] args) {
Tier t = new Tier();
Frosch f = new Frosch();
Biene b = new Biene();
System.out.print( "Ein Tier sagt " ); t.sagWas();
System.out.print( "Ein Frosch sagt " ); f.sagWas();
System.out.print( "Eine Biene sagt " ); b.sagWas();
Tier werBinIch = new Unbekannt();
// Was sage ich?
werBinIch.sagWas();
}
}
Die Unterklassen Biene und Frosch implementieren die Methode public void sagWas(), die auch in der Oberklasse Tier vorhanden ist. Sie überschreiben die Methode in der Oberklasse und können so ihre Funktion (in diesem Beispiel die Ausgabe) verändern.
- Lies den "Abschnitt 6.4.1: Methoden in Unterklassen mit neuem Verhalten ausstatten" im Onlinebuch "Java ist auch eine Insel" bis zur Überschrift "Die Annotation @Override".
- Erstelle ein kleines Beispielprojekt, an dem das Konzept "Überschreiben" erklärt werden kann.
Quiz
Welche Ausgabe wird vom Quelltext oben erzeugt?
Ein Tier sagt - Stille -
Ein Frosch sagt Quak, Quak!
Eine Biene sagt Summ, Summ, Summ!
Was sage ich? - Stille -()
super und this
Wird eine Methode von einer Unterklasse überschrieben, möchte man dennoch manchmal explizit die überschriebene Methode der Oberklasse aufrufen. Für diese Fälle gibt es das bekannte Schlüsselwort super. Es bezieht sich immer auf die Oberklasse der aktuellen Klasse. Um explizit die aktuelle Klasse zu referenzieren, kann das Schlüsselwort this benutzt werden.
Im folgenden Beispiel überschriebt die Klasse Hund die Methode public void ausgabe() er Oberklasse Tier, um auch die Rasse auszugeben. Die Ausgabe der Oberklasse soll dennoch gemacht werden, daher wird die Methode der Oberklasse explizit mit super.ausgabe() aufgerufen.
class Tier {
// Das Attribut ist private und von der Unterklasse nicht nutzbar.
private String name;
public Tier( String pName ) {
this.name = pName;
}
public void ausgabe() {
System.out.println( "Mein Name ist " + this.name );
}
}
class Hund extends Tier {
private String rasse;
public Hund( String pName, String pRasse ) {
super(pName);
this.rasse = pRasse;
}
// Will die Ausgabe auch den Namen ausgeben (der in der Oberklasse als private markiert ist),
// muss die Methode "ausgabe()" der Oberklasse explizit aufgerufen werden.
public void ausgabe() {
System.out.println( "Ich bin ein " + this.rasse );
super.ausgabe();
}
}
- Ergänze eine Klasse
HundMitBesitzer, die ein AttributString besitzerhat, das den Namen des Hundebesitzers speichert. Bei Aufruf der Methodepublic void ausgabe()soll zunächst die Ausgabe vonHundausgegeben werden, dann der Name des Besitzers.
Das Schlüsselwort final
Möchte man verhindern, dass eine Unterklasse eine Methode überschreibt, dann kann man sie als final deklarieren. Versucht eine Unterklasse es dennoch, dann kann das Programm nicht mehr übersetzt werden.
class Tier {
// Das Attribut ist private und von der Unterklasse nicht nutzbar.
private String name;
public Tier( String pName ) {
this.name = pName;
}
public final void ausgabe() {
System.out.println( "Mein Name ist " + this.name );
}
}
class Hund extends Tier {
private String rasse;
public Hund( String pName, String pRasse ) {
super(pName);
this.rasse = pRasse;
}
// Fehler
public void ausgabe() {
System.out.println( "Ich bin ein " + this.rasse );
super.ausgabe();
}
}
Typen von Unterklassen
Vererbung wirkt sich auch auf den Typ von Objekten einer Unterklasse aus. Eine Unterklasse Quadrat aus dem Beispiel ganz oben hat nicht mehr nur den Datentyp Quadrat, sondern auch den Typ aller ihrer Oberklassen. Ein Quadrat kann somit auch als Rechteck deklariert werden.
// Bekannte Deklaration vom Typ Quadrat
Quadrat q = new Quadrat(10);
// Deklaration als Rechteck
Rechteck q = new Quadrat(10);
// Deklaration als Form
Form q = new Quadrat(10);
Speichert man ein Quadrat aber als Form ab, dann verliert es alle Methoden, die vom Rechteck und Quadrat implementiert wurden. Es sind nur noch die Methoden der Form aufrufbar.
Allgemeiner: Eine Objektreferenz hat nur diejenigen Methoden verfügbar, die von ihrem deklarierten Typ oder dessen Oberklassen implementiert werden.
Klassen und Objekte | Compiler und Interpreter | BlueJ | Syntax und Semantik | Datentypen und Variablen | Objektvariablen | Methoden | Parameter und Rückgaben | Objekte erstellen | Der Konstruktor | Referenzen | Klassenmethoden | Klassen der Java-Bibliothek | Systematisch Fehler suchen | Arrays | Komplexe Arrays | Vererbung | Abstrakte Klassen | Interfaces | Generische Typen | Fehlerbehandlung


