Java: Vererbungslehre

Willemers Informatik-Ecke

Die Vererbung wird in Java über das Schlüsselwort extends angezeigt und das sagt auch sehr präzise aus, was Vererbung in der objektorientierten Programmierung ist: Die Erweiterung einer Basisklasse.

Gibt es in einem Software-Projekt verschiedene Klassen, die gewisse Gemeinsamkeiten haben, so ist es sinnvoll, zunächst eine Basisklasse zu schaffen.

Typische Anwendungen sind die Verwaltung von Kunden, Personal und Lieferanten. Alle sind Menschen 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.

Basisklasse und Erweiterung

Betrachtet unser 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 Beipiel 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 {
  protected int plaetze;

  public int getPlaetze() {
    return plaetze;
  }
  public void setPlaetze(int paraPlaetze) {
    plaetze = paraPlaetze;
  }
}
protected heißt in diesem Zusammenhang, dass eine Klasse, die Fahrzeug erweitert, direkt auf das Attribut zugreifen kann. Für alle anderen Klassen ist das Attribut privat.

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 {
  protected int kW;
  ...
}
Nun kann ein Objekt Motorrad von der Klasse Kraftfahrzeug und ein Objekt Fahrrad von der Klasse Fahrzeug definiert werden. Beide haben dann Platze, aber nur das Motorrad 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;
    }
}
Einer Referenz einer Basisklasse kann jederzeit die Referenz auf eine davon erweiterte Klasse zugewiesen werden (fahrzeug = motorrad). In umgekehrter Richtung muss allerdings ein Casting stattfinden. Und wie überall in der Welt ist ein Casting eine möglichst zu vermeidende Sache.

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.

Abstrakte Klassen

Nehmen wir an, dass die kW-Leistung eines Fahrzeugs unterschiedlich ermittelt würde, je nachdem, ob es sich um einen Bus, einen PKW oder um ein Motorrad handelt. In diesem Fall wäre die Basisklasse nicht in der Lage, das Attribut kW zu besetzen. Eine Methode setKw wäre also erst durch erweiterte Klassen zu implementieren. getKw stellt kein Problem dar, weil es nur das Attribut zurückliefert.

Die Methode setKw kann nicht implementiert werden. Aber sie kann als Vorgabe deklariert werden, indem sie als abstrakte Methode vorgegeben wird. Eine abstrakte Methode muss dann von einer erweiterten Klasse implementiert werden. Enthält eine Klasse eine abstrakte Methode, muss sie als abstrakte Klasse deklariert werden.

public abstract class Kraftfahrzeug extends Fahrzeug {
    protected int kW;
    public int getKw() {
       return kW;
    }
    abstract void setKw(int kW);
}
Von einer abstrakten Klasse kann keine Instanz gebildet, also kein Objekt erzeugt werden. Es wird also noch eine erweiterte Klassse benötigt, die die abstrakte Methode implementiert. Wir erstellen dazu die Klasse Motorrad.
public class Motorrad extends Kraftfahrzeug {
    public void setKw(int paraKw) {
        kW = paraKw;
    }
}
Die Methode getKw muss nicht implementiert werden, da sie ja bereits von der Basisklasse implementiert wird. Das Attribut kW ist in der Basisklasse als protected definiert worden, kann also von Motorrad aus frei zugegriffen werden. Schauen wir noch einmal auf das Programm TestFahrzeug:
class TestFahrzeug {
    static public void main(String[] args) {
        Fahrzeug fahrrad = new Fahrzeug();
        Kraftfahrzeug motorrad = new Motorrad();
        motorrad.setKw(40);
        Fahrzeug fahrzeug;
        fahrzeug = fahrrad;
        fahrzeug = motorrad;
        motorrad = (Kraftfahrzeug) fahrzeug;
        System.out.println(motorrad.getKw());
    }
}
Die Variable motorrad ist zwar vom Typ Kraftfahrzeug, aber motorrad ist ja auch nur eine Referenz. Ein Objekt wurde durch den Befehl new erzeugt. Und dort wurde eine Instanz von Motorrad angefordert. Es ist also möglich, eine Referenzvariable einer abstrakten Klasse anzulegen. Allerdings darf hinter dem new keine abstrakte Klasse stehen.

Interface

Wenn man den Weg einer abstrakten Klasse weitergeht, dann enthält diese keinerlei Implementation, sondern nur abstrakte Methoden, die vorgeben, wie eine Ableitung dieser Klasse zu implementieren ist.

Besteht die Klasse nur aus abstrakten Methoden und Konstanten, kann man sie als Interface deklarieren. Natürlich kann ein Interface nicht die Erweiterung einer implementierenden Klasse sein. Darum fällt die Klasse Kraftfahrzeug als Kandidat für ein Interface solange aus, wie nicht auch Fahrzeug als Interface definiert ist.

Mehrfachvererbung per Interface

Der Grund für die Einrichtung eines Interfaces ist kein Selbstzweck. Es geht nicht um die wahre, reine abstrakte Klasse, sondern es geht um den Fall, dass eine Klasse von zwei Basisklassen erben möchte.

Dies ist in Java ausgeschlossen. Zu groß erschien die Gefahr, dass bei gleichen Methodennamen die Erbschaft zur Last werden könnte.

Dafür ist es möglich, zwei Interfaces zu implementieren. Hier ist das Risiko gering, weil die Klasse selbst für die vollständige Implementierung zuständig ist und insofern keine Doppeldeutigkeiten entstehen können, die dazu führen könnten, den falschen Code aufzurufen..

Interfaces werden implementiert

Wir definieren nun Fahrzeug als Interface. Es ist analog zu der Klasse Fahrzeug, nur dass das Interface keinerlei Implementierung besitzt.
interface Fahrzeug {
    int getPlaetze() ;
    void setPlaetze(int paraPlaetze) ;
}
Damit wir auch einmal eine Mehrfachvererbung sehen können, definieren wir ein Interface Zweirad, das nur eine boolesche Methode besitzt, die hatBalance heißt.
interface Zweirad {
    boolean hatBalance() ;
}
Diese beiden Interfaces werden nun von der Klasse Fahrrad implementiert. Dazu müssen sowohl die Methoden von Fahrzeug als auch die Methode von Zweirad implementiert werden.
class Fahrrad implements Fahrzeug,Zweirad {
    protected int plaetze;

  public int getPlaetze() {
    return plaetze;
  }
  public void setPlaetze(int paraPlaetze) {
    plaetze = paraPlaetze;
  }
  public boolean hatBalance() {
    return true;
  }
}
Im Testprogramm wird ein Objekt Fahrrad erzeugt und einer Referenzvariablen vom Typ Fahrzeug zugewiesen. Das funktioniert auch mit Interfaces und sogar dann, wenn Fahrrad mehrere Interfaces implementiert.
class TestFahrrad {
    static public void main(String[] args) {
        Fahrzeug fahrrad = new Fahrrad();
    }
}


Homepage (C) Copyright 2011 Arnold Willemer