Java: Vererbungslehre
Willemers Informatik-Ecke
Objektorientierte Programmierung Polymorphie

Weiterführende Themen:

Die Vererbung oder Ableitung bedeutet, dass alle Attribute und Methoden einer Basisklasse von einer anderen Klasse verwendet werden, ohne sie neu zu deklarieren und diese um andere Attribute oder Methoden erweitert.

Die Vererbung wird in Java über das Schlüsselwort extends deklariert und sagt damit aus, was Vererbung in der objektorientierten Programmierung eigentlich ist: Die Erweiterung einer Basisklasse.

Typische Anwendungen sind die Verwaltung von Kunden, Personal und Lieferanten. Alle sind Personen und haben eine Adresse und eine Telefonnummer. Damit bietet sich eine Klasse Person an, die sich um diese Eigenschaften kümmert. Ein Kunde erweitert eine Person um die Eigenschaft, dass er kauft. Ein Lieferant ist eine Person, die Rechnungen stellt und der Mitarbeiter ist eine Person, die in einer Abteilung arbeitet und dafür das Anrecht auf Bezahlung und Urlaub erwirbt.

Gibt es in einem Software-Projekt verschiedene Klassen, die gewisse Gemeinsamkeiten haben, so ist es sinnvoll, nach einer Basisklasse zu suchen, die die gemeinsamen Eigenschaften darstellt.

Beispiel Hausbau

Als Beispiel für eine Klasse und ihre Objekte haben wir das Beispiel eines Hausbaus bereits bemüht. Der Bauplan ist die Klasse, das Objekt bzw. die Instanz wurde durch die Baufirma new konstruiert. Die Referenzvariable variable verweist als Hinweisschild auf das gebaute Objekt.

Das Haus steht. Die Maurer sind fertig. Nun soll die Sanitäreinrichtung gebaut werden. Die Sanitärfirma interessiert sich aber nicht für den Bauplan der Maurer. Sie will nur wissen, wie das bestehende Haus um die Rohre und Wasserhähne zu erweitern ist.

Basisklasse und Erweiterung

Betrachtet ein Programm Busse, LKWs, PKWs und Motorräder, so kann es sinnvoll sein, eine Basisklasse Fahrzeug zu schaffen. Je nachdem, was die Fahrzeuge für das Programm interessant macht, kann die Basisklasse diejenigen Bestandteile implementieren, die allen gleich ist. Im Beispiel ist die Anzahl der mitfahrenden Personen relevant. Darum wird in der Basisklasse Fahrzeug ein Attribut angelegt, das gesetzt und gelesen werden kann. Dieses muss von einer erweiterten Klasse nicht mehr implementiert werden.
public class Fahrzeug {
    private int plaetze;
    public int getPlaetze() {
        return plaetze;
    }
    public void setPlaetze(int paraPlaetze) {
        plaetze = paraPlaetze;
    }
}
Die Klasse Fahrzeug könnte von einer Klasse Kraftfahrzeug erweitert werden. Das Kraftfahrzeug hat alle Eigenschaften eines Fahrzeuges, hat aber zusätzlich einen Motor. Dieser wird hier durch seine Leistung in kW repräsentiert.
public class Kraftfahrzeug extends Fahrzeug {
    private int kW;
    public int getKw() {
        return kW;
    }
    public void setKw(int kW) {
        this.kW = kW;
    }

}
Nun kann ein Objekt Lastkraftwagen von der Klasse Kraftfahrzeug und ein Objekt Fahrrad von der Klasse Fahrzeug definiert werden. Beide haben dann Plätze, aber nur der Lastkraftwagen hat das Attribut kW.

class TestFahrzeug {
    static public void main(String[] args) {
        Fahrzeug fahrrad = new Fahrzeug();
        Kraftfahrzeug motorrad = new Kraftfahrzeug();
        Fahrzeug fahrzeug;
        fahrzeug = fahrrad;
        fahrzeug = motorrad;
        ...
        motorrad = (Kraftfahrzeug) fahrzeug;
    }
}

Zuweisungskompatibilität

Einer Referenz einer Basisklasse kann jederzeit die Referenz auf eine davon erweiterte Klasse zugewiesen werden. So funktioniert die Zuweisung fahrzeug = motorrad; weil die Klasse Kraftfahrzeug die Klasse Fahrzeug erweitert.

Fahrzeug fahrrad = new Fahrzeug();
Kraftfahrzeug motorrad = new Kraftfahrzeug();
fahrzeug = motorrad;

Man kann auch sagen, dass ein Motorrad ein Fahrzeug IST und darum alle Bedingungen eines Fahrzeugs erfüllt. Verwendet man also eine (Referenz-)Variable vom Typ Fahrzeug, können alle Eigenschaften von Fahrzeug von einem Motorrad erfüllt werden.

In umgekehrter Richtung gilt das nicht unbedingt, denn nicht jedes Fahrzeug ist auch ein Kraftfahrzeug. So hat ein Segelboot keinen Motor. Würde man auf ein Segelboot also über eine Referenz vom Typ Kraftfahrzeug zugreifen, würden die Zugriffe, die den Motor betreffen, wie etwa die Zugriffe auf die Leistung (kW) ins Leere weisen, weil ein Motor nicht vorhanden ist.

Weiß der Programmierer allerdings, dass das Objekt, das auf das fahrzeug verweist, ursprünglich als Kraftfahrzeug erzeugt worden ist, kann auch eine Rückzuweisung erfolgen. Allerdings muss man den Compiler dazu mit Gewalt überzeugen. Es muss ein Casting stattfinden. Und wie überall in der Welt ist ein Casting immer grausam und sollte vermieden werden.

Fahrzeug fahrrad = new Fahrzeug();
Kraftfahrzeug motorrad = new Kraftfahrzeug();
fahrzeug = motorrad;
Kraffahrzeug kfz = (Kraftfahrzeug)fahrzeug; // Der Programmierer übernimmt die Verantwortung

Object

Jede Klasse in Java ist mindestens indirekt von der Klasse Object abgeleitet. Aus diesem Grund kann eine Variable vom Typ Object jede andere Referenz aufnehmen. In umgekehrter Richtung muss wiederum ein Casting erfolgen.

Überschreiben von Methoden

Beim Erweitern einer Klasse kann es passieren, dass eine Methode auf die Erweiterung angepasst werden muss. In diesem Fall wird die Methode der Basisklasse überschrieben.
public class Basis {

    private int zahl = 0;
    
    void setZahl(int neu) {
        zahl = neu;
    }
}
Wenn in der erweiterten Klasse auch die Methode setZahl zur Verfügung stehen soll, in jener Klasse allerdings auf keinen Fall negative Zahlen möglich sein sollen, überschreibt man die Methode. Für das Überschreiben wird in der erweiternden Klasse eine Methode mit dem gleichen Namen und gleichen Parametern geschrieben.
public class Erweiterung extends Basis {
    @Override
    void setZahl(int wert) {
        if  (wert < 0) {
            wert = 0;
        }
        super.setZahl(wert);
    }
}
Die Annotation @Override lässt den Compiler prüfen, ob es eine gleichnamige Methode mit diesen Parametern in der Basisklasse gibt. Die Annotation ist für das Überschreiben nicht erforderlich, verhindert aber Tippfehler, da der Compiler die Übereinstimmung überprüft. Soll innerhalb der überschriebenen Methode die Methode der Basisklasse aufgerufen werden, kann man diese über die Referenz super erreichen.

Erweiterung und Konstruktoren

Bei der Erzeugung der Instanz einer erweiternden Klasse wird natürlich der Konstruktor dieser Klasse aufgerufen. Das geschieht auch, wenn die erweiternde Klasse keinen expliziten Konstruktor hat, sondern dieser von Java automatisch generiert wird.
public class Basis {
    public Basis() {
        System.out.println("Hallo");
    }
}

public class Erweitert extends Basis {
}

public class Test {
    public static void main(String[] args) {
        new Erweitert();
    }
}
Tatsächlich erscheint die Ausgabe Hallo auf dem Bildschirm.

Das geschieht auch, wenn die Klasse Erweitert einen expliziten Konstruktor erhält. Um diesen Vorgang deutlicher zu machen, kann der Konstruktor der Basisklasse explizit über den Aufruf von super erfolgen.

public class Erweitert extends Basis {
    public Erweitert() {
        super();
    }
}
Der Aufruf des Konstruktors der Basisklasse erfolgt immer als erste Anweisung im Konstruktor. Spätere Aufrufe werden abgelehnt.

Konstruktoren mit Parametern

Hat die Basisklasse nur Konstruktoren mit Parametern und damit keinen Standardkonstruktor, so muss die erweiterte Klasse einen Konstruktor zur Verfügung stellen, der explizit den Konstruktor der Basisklasse aufruft.
public class Basis {
    public Basis(int zahl) {
        System.out.println("Hallo "+zahl);
    }
}

public class Erweitert extends Basis {
    public Erweitert() {
        super(12);
    }
}
Etwas schwierig wird es, wenn der Parameter für den Konstruktor der Basisklasse erst errechnet werden soll, da der Aufruf von super unbedingt als erste Anweisung erfolgen muss. Der folgende Versuch, eine zufällige Zahl zwischen 0 und 11 als Parameter zu übergeben, wird also scheitern.
public class Erweitert extends Basis {
    public Erweitert() {
        // Das lässt der Compiler nicht zu, weil diese Anweisung ...
        java.util.Random zufall = new java.util.Random();
        // ... vor dem super erfolgt.
        super(zufall.nextInt(12));
    }
}
Wird die Erzeugung der Variablen zufall aus dem Konstruktor herausgenommen, könnte es ja vielleicht klappen?
public class Erweitert extends Basis {
    java.util.Random zufall = new java.util.Random();
    public Erweitert() {
        super(zufall.nextInt(12)); // Compiler meckert
    }
}
Tatsächlich ist der Compiler unzufrieden, da die Variable zufall noch nicht existiert, bevor der Konstruktor aufgerufen wird. Sobald die Variable allerdings auf static gesetzt wird, funktioniert es. Der Grund liegt darin, dass eine static-Variable bereits vor dem Aufruf des Konstruktors aufgerufen wird.
public class Erweitert extends Basis {
    static java.util.Random zufall = new java.util.Random();
    public Erweitert() {
        super(zufall.nextInt(12));
    }
}
Eine andere Variante wäre, dass man im Parameteraufruf von super die Variable anonym erzeugt und direkt deren Methode nextInt aufruft.
public class Erweitert extends Basis {
    public Erweitert() {
        super(new java.util.Random().nextInt(12));
    }
}

Video


Objektorientierte Programmierung Polymorphie