Kurzeinstieg Java: Vererbung

Was ist Vererbung?

Bearbeiten

Vererbung ist ein Mechanismus in der objektorientierten Programmierung (OOP), bei dem man aus allgemeinen Klassen spezialisierte oder erweiterte Klassen herstellt. Das hat nichts mit "Vererbung" in einem biologischen Sinn zu tun. Aus einem "geometrischen Objekt" (allgemeine Klasse) wird ein Kreis (spezialisiert) und aus einer Pflanze eine Kartoffel. In jedem Fall hat die Kartoffel alle Eigenschaften einer Pflanze und ein Kreis alle Eigenschaften eines geometrischen Objekts. Wir können aus einem "Skywalker" zwar einen "Anakin Skywalker" herstellen, aber aus Anakin leider nicht Luke Skywalker. Im Sinne der OOP wäre dann nämlich ein Luke auch ein Anakin, und das würde keinen Sinn ergeben.

Aus diesen Gründen spricht man häufig von "Ableitung", abgeleitete Klassen sind Subklassen oder Unterklassen. Diejenigen Klassen, von denen abgeleitet wird, nennt man Basisklassen oder Superklassen. In diesem Bereich hat man mit vielen Begriffspaaren zu tun, die aus der Zeit stammten, als OOP etwas völlig Neues war. Hier müssen Sie ein wenig flexibel bleiben. In diesem Buch sprechen wir von Vererbung, Super- und Subklasse, manchmal auch von Basisklassen.

Wie sieht Vererbung aus?

Bearbeiten

Wir bleiben vorerst bei der Familie Skywalker. Ein beliebiges Familienmitglied kann offenbar ein Jedi sein. Die Methoden setzeJedi() und istJedi() sind können die Eigenschaft setzen oder abfragen. Das eigentliche Attribut ist privat. Allgemeine Skywalker sind natürlich keine Jedis:

class Skywalker {
    private boolean istEinJedi;
    
    public Skywalker() {
        System.out.println("Ich bin ein Skywalker");
        setzeJedi(false);
    }
    
    // setter
    protected void setzeJedi(final boolean jedi) {
        istEinJedi = jedi;
    }
    
    // getter
    protected boolean istJedi() {
        return istEinJedi;
    }
}


class AnakinSkywalker extends Skywalker {
    private boolean hatEinLichtschwert;
    
    public AnakinSkywalker() {
        System.out.println("Ich bin Anakin Skywalker");
        setzeJedi(false);  // war ein Jedi
        hatEinLichtschwert = true;
    }
    
    public boolean hatLichtschwert() {
        return hatEinLichtschwert;
    }

}


public class Todesstern {
    public static void main(String[] args) {
        
        AnakinSkywalker darthVader = new AnakinSkywalker();
        if(!darthVader.istJedi())
            System.out.println("war ein Jedi");
        
        if(darthVader.hatLichtschwert())
            System.out.println("hat ein Lichtschwert");

    }
}

Nicht anders sieht es bei AnakinSkywalker aus. Der war früher mal ein Jedi, heute ist er keiner mehr. Dafür hat er ein Lichtschwert, was eine neue Eigenschaft ist. AnakinSkywalker erbt alle Eigenschaften eines Skywalkers. Dafür sorgt das Schlüsselwort extends. Anakin wird dadurch spezieller, in dem er neue Eigenschaften und Methoden bekommt. Man könnte auch sagen, seine Fähigkeiten werden erweitert.

Ist das nicht super?

Bearbeiten

Nun tritt Luke auf den Plan. Für Luke ist es selbstverständlich, ein Jedi zu sein. So selbstverständlich, dass er seine eigenen Methode istJedi() implementiert. Beachten Sie, dass wir den Rückgabetyp von istJedi() von boolean auf String geändert haben. In manchen Fällen möchte er gerne explizit auf die istJedi()-Implementierung der Superklasse zugreifen, und in manchen Fällen eben auf seine eigene.

class Skywalker {
    private boolean istEinJedi;
        
    public Skywalker() {
        System.out.println("Ich bin ein Skywalker");
        setzeJedi(false);
    }
                                    
    // setter                    
    protected void setzeJedi(final boolean jedi) {
        istEinJedi = jedi;
    }
                                                            
    // getter
    protected String istJedi() {
        return istEinJedi ? "ja" : "nein";
    }
                                                                                }
                                                                                 
class LukeSkywalker extends Skywalker {
    
    public LukeSkywalker() {
        setzeJedi(true);        // natürlich ein Jedi
        System.out.println("Ich bin Luke, ich werde niemals zur dunklen Seite wechseln!");
        System.out.println("Jedistatus: " + super.istJedi());  
    }
     
    // neue Rückgabe
    protected String istJedi() {
        return "Selbstverständlich bin ich ein Jedi!";
    }
}
 

class Todesstern
{
    public static void main(String[] args) {
        LukeSkywalker luke = new LukeSkywalker();
    }
}

Und genau das macht das Schlüsselwort super möglich. super.istJedi() ruft die Methode der Superklasse auf, damit können Subklassenimplementierungen von Superklassenimplementierungen unterschieden werden.

Nehmen Sie hier folgendes mit:

  1. Sie dürfen in einer Subklasse Methoden der Superklasse überschreiben. Das ist dann eine neue Implementation, und wann auch immer Sie die überschriebene Methode aufrufen, es wird diejenige Methode in ihrer Klasse gewählt.
  2. Um auf Methoden und Attribute der Superklasse zuzugreifen, benutzen Sie super.

Wie kann man das Überschreiben verhindern?

Bearbeiten

Nun kann selbstverständlich jeder Skywalker eine eigene Implementierung von istJedi() haben und sich so unter Umständen fälschlich als Jedi ausgeben. Das sollte verhindert werden. Benutzen Sie in einer Superklasse das Schlüsselwort final, dann darf nichts und niemand ihre Methode überschreiben. Schreiben Sie in der Klasse Skywalker einfach:

class Skywalker {
...
    final protected String istJedi() {
        return istEinJedi ? "ja" : "nein";
    }
}

So scheitert jeder Versuch, sich in Subklassen fälschlich als Jedi auszugeben.

Wie kann man Vererbung verhindern?

Bearbeiten

Wie im vorangegangenen Abschnitt, hilft final weiter. Diesmal setzen Sie final nicht vor eine oder mehrere Methoden, sondern vor die ganze Klasse. Es ergibt beispielsweise wenig Sinn, von einer Klasse "Todesstern" Eigenschaften zu erben, denn er ist schon ein Spezialfall eines Zwergmondes[1] und der Einzige seiner Art:

final class Todesstern {
...
}


  1. vergl. https://xkcd.com/1458/

Wie fordert man zum Implementieren auf?

Bearbeiten

Man kann Klassen so schreiben, dass sie nichts oder nur einen Teil ihrer Funktionalität selber implementieren. Die Implementierung muss dann in der Subklasse erfolgen. Methoden, die nur aus dem Methodenkopf ohne Implementierung bestehen, nennt man abstrakte Methoden. Ist mindestens eine Methode einer Klasse abstrakt, dann ist es auch gleich die ganze Klasse. Von solchen Klassen kann man kein Objekt erzeugen. Was sollte ein Aufruf der nicht implementierten Methode auch tun? Statische Methoden und Eigenschaften einer abstrakten Klasse können Sie wie gewohnt nutzen. Das Schlüsselwort für abstrakte Klassen und Methoden ist, Sie erraten es, abstract.

abstract class Stormtrooper {
    
    static boolean hatGewehr = true;
    
    Stormtrooper() {
        System.out.println("Ich bin ein Stormtrooper");
    }
    
    // Abstrakte Methode, muss in Subklasse implementiert werden
    abstract public void marschiereLos();    
    
    static void schiesse() {
        System.out.println("peng!");
    }
}


class WeisserStormtrooper extends Stormtrooper {
 
    WeisserStormtrooper() {
        System.out.println("Ich bin ein weißer Stormtrooper");
    }
    
    public void marschiereLos() {
        System.out.println("Auf geht's!");
    }    
}


public class Angriff {
    public static void main(String[] args) {
        
        if(Stormtrooper.hatGewehr) {            
            WeisserStormtrooper s = new WeisserStormtrooper();
            s.marschiereLos();
            Stormtrooper.schiesse();
        }
    }
}

Es muss jede Klasse, die mindestens eine abstrakte Methode enthält, selber abstrakt deklariert werden. Abstrakte Klassen haben Ähnlichkeiten zu Interfaces, die wir später behandeln.

Wie verwendet man Modifizierer in Zusammenhang mit Vererbung?

Bearbeiten

Sowohl public-, protected- wie auch 'package-private'-Elemente von Klassen werden vererbt. In Subklassen bleibt diese Zugriffsmodifizierung erhalten. Auf private-Elemente können Sie in Subklassen nicht zugreifen. Versuchen Sie, in ihrem Vererbungsbaum so private wie möglich zu bleiben. Verwenden Sie final für alle Methoden und Attribute, die nicht weitervererbt werden sollen. Die Kombination von final und private ergibt bei Methoden keinen Sinn, hier reicht private schon aus. Bei private Konstanten können Sie static verwenden, wenn diese Konstante in allen Instanzen denselben Wert haben soll.

Wer bin ich?

Bearbeiten

In diesem Buch können wir Ihnen nicht auf alle Fragen eine Antwort geben, aber soviel sei verraten: Wenn Sie ein Objekt im Sinne der OOP von Java sind und sich in ein bestimmtes anderes Objekt umwandeln (casten) lassen, dann können wir prüfen, ob Sie von letztgenanntem Objekt abstammen. Die 'Umwandeln-Bedingung' ist leider eine sehr harte Bedingung, die man nicht umgehen kann. Man kann also nicht prüfen, ob ein Frosch eine Instanz eines Prinzen ist. Weniger märchenhaft ist das Beispiel das zeigt, wie es funktioniert. Drei Klassen stehen durch Vererbung miteinander in Verbindung. Mit instanceof lässt sich nun prüfen, ob Objekte a und c Instanzen der Beispielklassen sind.

class A {
    public A() {}
}

class B extends A {
    public B() {}
}

class C extends B {
    public C() {}
}

class Instanzentest
{
    public static void main(String[] args) {
        A a = new A();
        C c = new C();

        System.out.println(c instanceof A);
        System.out.println(a instanceof A);
        System.out.println(a instanceof C);
        System.out.println(""  instanceof String);
    }
}

Ein Objekt, das sich in ein Objekt eines jeden Typs umwandeln lässt, ist null. null instanceof WasAuchImmer ergibt immer false, weil null niemals Instanz von etwas ist. Das Objekt a kann man nicht gegen zum Beispiel 'String' testen, weil sich a nicht in einen 'String' umwandeln lässt.

Welche Fallen gibt es im Zusammenhang mit OOP?

Bearbeiten

Im Rahmen der OOP werden Fragen aufgeworfen, die sich mit der Vererbung als Theorie befassen. Im Rahmen der Ersetzbarkeitstheorie[1] ist ein Problem bekannt, das so genannte "Kreis-Ellipsen-Problem", das, bezogen auf unsere Java-Terminologie, wie folgt lautet: Für eine beliebige Superklasse gilt, dass seine Subklasse dieselben Eigenschaften hat. Bei einer Ellipse können Sie beide Radien unabhängig voneinander ändern. Ein Kreis ist bekanntermaßen ein Spezialfall einer Ellipse. Wenn Sie nun den Kreis (Subklasse) von Ellipse (Superklasse) ableiten, dann erhalten Sie einen Kreis mit zwei getrennt voneinander änderbaren Radien. Wenn Sie es nun umgekehrt probieren, also Ellipse von Kreis ableiten, dann stellen Sie fest, dass die Ellipse nichts mit dem einzelnen Radius anfangen kann.

  1. vergl. w:Liskovsches Substitutionsprinzip

Wie baut man sich eine Ausnahme?

Bearbeiten

Hinter Ausnahmen verbergen sich teilweise recht lange Vererbungsbäume. Die Basisklasse ist 'java.lang.Throwable'. Einer der Konstruktoren benötigt einen String, mit dem die Ausnahme beschrieben wird. Fangen Sie eine solche Ausnahme mit catch ab, dann können Sie mit getMessage() diese Beschreibung erhalten.

Von 'Throwable' werden zwei Klassen abgeleitet namens 'Error' und 'Exception'. Von der Klasse Error sind Ausnahmen abgeleitet, die nicht abgefangen werden sollen, sondern zum Abbruch des Programms führen. Alle Ausnahmen, die abgefangen werden dürfen, erben von der Klasse Exception. Exception ist damit die Basisklasse aller Ausnahmen, mit denen wir uns beschäftigen wollen.

Das Beispiel konstruiert eine Ausnahme, die im Hauptprogramm ausgelöst wird.

class MeineAusnahme extends Exception {

    public MeineAusnahme(String nachricht) {
        super(nachricht);       // Exception-Konstruktor mit Nachricht aufrufen
    }
}

class Ausnahmetest
{
    public static void main(String[] args) {
        // Ausnahme erzeugen
        MeineAusnahme ausnahme = new MeineAusnahme("Hallo, Welt!");
        try {
            throw ausnahme;                     // Ausnahme auslösen        
        }
        catch( MeineAusnahme e ) {
            System.err.println(e.getMessage()); // Nachricht 
            e.printStackTrace();                // Hinweise, wer die Ausnahme wo ausgelöst hat
        }
    }
}

Mit super() wird dabei derjenige Konstruktor der Superklasse Exception ausgewählt, der Nachrichten speichern kann. Das ist ein Vorgriff zu "polymorphen Konstruktoren" aus dem nächsten Kapitel. Die Ausnahme wird erzeugt wie jedes andere Objekt auch, mit throw ausgelöst und mit catch wieder eingefangen. Der "Stacktrace" ist diejenige Programmausgabe, die Sie sehen, wenn ein Programm wegen einer Ausnahme abgebrochen wird. Der Stacktrace enthält den Ort und das Objekt, wo die Ausnahme erzeugt wurde.

Stammen wir wirklich alle von einem gemeinsamen Objekt ab?

Bearbeiten

Alle Objekte stammen vom der Klasse 'Object' ab, selbst leere Klassen. Die Ausnahme bildet allerdings null, wie Sie sicher schon geahnt haben.

class OhneNamen {}

class ObjectTest
{
    public static void main(String[] args) {
        OhneNamen dummy = new OhneNamen();
        System.out.println(dummy instanceof Object);
     
    }
}

Damit ist Object die Superklasse aller Superklassen. Selbst ein 'Object'-Objekt stammt von Object ab. Jedes Objekt erhält dadurch eine Reihe von Methoden mit auf den Weg, von denen einige in diesem Programm vorgestellt werden:

class OhneNamen {}

class ObjectTest
{
    public static void main(String[] args) {
        OhneNamen dummy = new OhneNamen();
        OhneNamen andererDummy = new OhneNamen();

        System.out.println(dummy.toString());   // OhneNamen + '@' + Zahl
        System.out.println(dummy.equals(dummy) + " " + dummy.equals(andererDummy)); // true false
        System.out.println(dummy.getClass());   // "class OhneNamen"
    }
}

Damit gibt es also einen Anfang aller Vererbung und einen Grundstock an geerbten Methoden. Na, wenn das mal kein Zeichen ist.