Letztes Update: 25.09.2018
Behandelte Befehle: Oberklasse/Unterklasse, Subtypen, Klassendiagramm, Objektdiagramm, extends, super(), UTF-8, toString()

Lernziele

  • Klassen in eine Ober-/Unterklassen-Beziehung setzen, um Vererbung zu nutzen
  • Konstruktor der Oberklasse mit super() einbinden
  • Den ternären Operator gezielt einsetzen, um den Code zu verschlanken

In diesem Kapitel lernen wir das Konzept von Ober- und Unterklasse kennen, um so Klassenhierarchie zu erstellen. Dazu entwerfen wir eine kleine Software, die Musiktitel und Filme verwalten kann.

18.1 MyTunes: Eine Musik- und Filmverwaltung

Unsere Software soll unsere Bibliothek an Musik- und Filmdateien verwalten. Das heißt konkret: wir können Titel suchen, abspielen und vielleicht auch Playlists anlegen.

Erster Ansatz

Da wir objektorientiert arbeiten, entwerfen wir je eine Klasse für (1) Songs und (2) Filme. Ein konkreter Song wird dann durch eine Instanz (Objekt) repräsentiert. Zu jedem Song wollen wir Titel, Künstler und Dateinamen auf der Festplatte speichern. Bei Filmen speichern wir Titel, Regisseur und Dateinamen.

Legen Sie zunächst ein neues Package an z.B.

de.hsa.mytunes

Jetzt schreiben wir unsere Klasse Song:

package de.hsa.mytunes;

public class Song {

  // Instanzvariablen
  private String title;
  private String artist;
  private String file;

  // Konstruktor
  public Song(String aTitle, String aArtist, String aFile) {
    title = aTitle;
    artist = aArtist;
    file = aFile;
  }
}

Für Filme schreiben wir eine Klasse Movie:

package de.hsa.mytunes;

public class Movie {

  private String title;
  private String director;
  private String file;

  public Movie(String aTitle, String aDirector, String aFile) {
    title = aTitle;
    director = aDirector;
    file = aFile;
  }
}

Um unsere Klassen zu testen, müssen wir Testobjekte erzeugen, die wir in Listen speichern. Wir machen das in einer statischen main-Methode in MyTunes. Denken Sie daran, die ArrayList zu importieren!

package de.hsa.mytunes;

import java.util.ArrayList;

public class MyTunes {}

  public static void main(String[] args) {
    // Liste mit Test-Filmen
    ArrayList<Movie> movies = new ArrayList<Movie>();
    movies.add(new Movie("Alien", "Ridley Scott", "alien.mov"));
    movies.add(new Movie("Der Pate", "Francis Ford Coppola", "pate.mov"));
    movies.add(new Movie("Gravity", "Alfonso Cuarón", "gravity.avi"));

    // Ausgeben
    for (Movie m: movies) {
      System.out.println(m);
    }

    // Liste mit Test-Songs
    ArrayList<Song> songs = new ArrayList<Song>();
    songs.add(new Song("OMG!", "Marteria", "omg.wav"));
    songs.add(new Song("Happy", "Pharrell Williams", "happy.wav"));

    // Ausgeben
    for (Song s: songs) {
      System.out.println(s);
    }
  }
}
Als Ausgabe sehen Sie:
MyTunes$Movie@3fa3e565
MyTunes$Movie@7aa16cf5
MyTunes$Movie@13e36e92
MyTunes$Song@6b398cec
MyTunes$Song@1a8e93ba

So sieht es aus, wenn Java (und Processing) Objekte ausgibt.

Objektausgabe mit toString()

Die Ausgabe ist leider nicht sehr aussagekräftig. Deshalb bedienen wir uns einer Technik, um die Ausgabe von Objekten schöner zu machen: wenn Sie in einer Klasse die Methode toString() hinzufügen, dann wird diese Methode aufgerufen, sobald das Objekt mit print() ausgegeben wird.

public class Movie {
  ...

  // Rückgabetyp muss String sein!
  public String toString() {
    return "MOVIE \"" + title + "\" von " + director +
           ", zu finden unter: " + file;
  }
}

public class Song {
  ...

  // Rückgabetyp muss String sein!
  public String toString() {
    return "SONG \"" + title + "\" von " + artist +
           ", zu finden unter: " + file;
  }
}

Falls Sie sich über das "MOVIE \"" wundern, das wird gleich erklärt. Die Ausgabe ist jetzt schöner:

MOVIE "Alien" von Ridley Scott, zu finden unter: alien.mov
MOVIE "Der Pate" von Francis Ford Coppola, zu finden unter: pate.mov
MOVIE "Gravity" von Alfonso Cuarón, zu finden unter: gravity.avi
SONG "OMG!" von Marteria, zu finden unter: omg.wav
SONG "Happy" von Pharrell Williams, zu finden unter: happy.mp3

Escape-Sequenzen in Strings

Wenn Sie in einem String Anführungszeichen einbauen wollen, dann haben Sie ein Problem: Wie verhindern Sie, dass Processing denkt, dass an dieser Stelle der String zu Ende ist:

String foo = "dies ist ein Anführungszeichen: ""; // Fehler

Sie müssen das vorletzte Anführungszeichen kennzeichnen, so dass Processing weiß, dass es im String ist und nicht die Begrenzung darstellt. Dieses Kennzeichnen funktioniert mit einem vorangestelltem Backslash:

String foo = "dies ist ein Anführungszeichen: \""; // OK

Analog funktioniert das für "unsichtbare" Zeichen wie Tab (\t), Zeilenumbruch (\n) und den Backslash selbst (\\).

Probleme und Code-Duplizierung

Unser Programm hat einige unschöne Aspekte:

  • Die Klassen Movie und Song sehen fast gleich aus. Das bedeutet, dass Code doppelt vorhanden ist. Man nennt das Code-Duplizierung. Das gilt als schlechte Programmierpraxis, weil ich bei Änderungen in einem Code (z.B. einen Fehler beheben oder den Code schneller/eleganter gestalten) auch den anderen ändern müsste.
  • Wir müssen die Medien in zwei Listen verwalten, besser wäre alles in einer Datenstruktur.

Ein wichtiges Designprinzip vom Erstellen von Klassen ist also die Vermeidung von Code-Duplizierung, im Englischen spricht man auch von DRY: Don't Repeat Yourself. Im nächsten Kapitel lernen wir eine Möglichkeit kennen, Code-Duplizierung zu vermeiden.

Übungsaufgaben

18.1 a) (a) toString

Schreiben Sie eine Klasse Ausflug für eine Ausflugs-Planungsapp. Die Klasse soll das Ziel, die Anzahl der Personen und das Datum speichern (für das Datum einfach einen String verwenden). Achten Sie auf die korrekten Zugriffsmodifikatoren.

Fügen Sie eine toString Methode hinzu, so dass Sie eine "schöne" Ausgabe bekommen, z.B.

Ausflug nach "Königsbrunn" am 24.06.2020 mit 6 Personen
Ausflug nach "Ulm" am 1.04.2020 mit 12 Personen
Ausflug nach "Innsbruck" am 12.10.2022 mit 20 Personen

Zusammenfassung

Wir haben Klassen für Lieder (Klasse Song) und Filme (Klasse Movie) geschrieben. Wenn wir in einer Klasse eine Methode namens toString() implementieren (mit einem String als Rückgabe), können wir die Objekte mit System.out.println() in verständlicher Form ausgeben.

Wir haben ein Problem identifiziert: Beide Klassen beinhalten ähnlichen Code, d.h. wir haben es hier mit Code-Duplizierung zu tun. Dies sollte vermieden werden.

18.2 Klassenhierarchie und Vererbung

In objektorientierten Sprachen kann man zwei Klassen A und B zueinander in Beziehung setzen, so dass A die Oberklasse von B ist. Die Konsequenz lehnt sich an die Eltern-Kind-Beziehung beim Menschen an: Die Unterklasse "erbt" alle Instanzvariablen und Methoden der Oberklasse. Das bedeutet technisch, dass die Unterklasse zunächst mal alle Instanzvariablen und Methoden der Oberklasse benutzen kann, als wären sie in der eigenen Klasse definiert worden.

Man kann sich das anhand verschiedener Typologien verdeutlichen. Zum Beispiel: ein Auto (Klasse) ist ein Fahrzeug (Klasse). Auto erbt von Fahrzeug die Eigenschaft, dass es sich fortbewegen kann. Ein Auto hat aber auch Eigenschaften, die die Oberklasse (Fahrzeug) nicht hat, z.B. dass es vier Räder hat. Ein Fahrrad (Klasse) könnte eine weitere Unterklasse von Fahrzeug sein und hat die Eigenschaft, zwei Räder zu besitzen. Das ganze kann man in einem Klassendiagramm festhalten.

Video: Klassendiagramm/Objektdiagramm (8:43)

Die Pfeile bedeuten "ist Unterklasse von" und müssen genau so gezeichnet werden (d.h. offene Spitze, durchgezogener Strich), denn diese Diagramme sind standardisiert.

Ist-ein

Eine Klassenhierarchie zu erstellen ist ein Designproblem, d.h. es gibt nicht immer ein richtig oder falsch. Eine gute Daumenregel, um herauszufinden, ob die Klassenbeziehungen stimmen ist die "ist-ein"-Regel. Man sollte immer sagen können "<Unterklasse> ist ein <Oberklasse>". Im obigen Beispiel also "Auto ist ein Fahrzeug" und "Fahrrad ist ein Fahrzeug".

Einfaches Beispiel

Definieren wir mal eine einfache Klasse A mit einer Eigenschaft und einer Methode.

public class A {
  protected int anum = 10;

  public void afun() {
    System.out.println("a " + anum);
  }
}

Wir verwenden hier einen neuen Zugriffsmodifikator namens "protected". Warum wir das tun, wird gleich klar.

Um Processing zu sagen, dass Klasse B die Unterklasse von A ist, benutzen wir das Schlüsselwort extends (engl. erweitert):

public class B extends A {
  private int bnum = -99;

  public void bfun() {
    System.out.println("b " + anum + " " + bnum); // Zugriff auf anum
  }
}

Sie sehen hier, dass die Klasse B auf die Instanzvariable anum zugreifen kann, obwohl diese Variable doch eigentlich zu A gehört! Der Grund ist, dass B diese Variable geerbt hat, d.h. in B gibt es genauso eine Variable anum. Damit dieser Zugriff wirklich funktioniert, mussten wir allerdings den Zugriffsmodifikator von "private" auf das schwächere "protected" umstellen, sonst dürfte selbst die Unterklasse B nicht auf die Variable zugreifen.

public static void main(String[] args) {
  A a = new A();
  B b = new B();
  a.afun();
  b.bfun();
  b.afun();
}

Beachten Sie, dass man auf b auch die Methoden der Oberklasse aufrufen kann, in diesem Fall afun . Auch die Methoden vererbt A an B.

a 10
b 10 -99
a 10

Typen und Subtypen

Sobald Sie eine Klasse A schreiben, erzeugen Sie auch einen neuen Datentypen A, denn Sie können natürlich dann Variablen herstellen, die Instanzen von A speichern sollen.

Wenn Klasse B Unterklasse von A ist, dann ist der Datentyp B automatisch Subtyp von A. Das bedeutet, dass eine Variable von Typ A auch Objekte von Typ B speichern kann.

Beispiel 1:

A a = new A();
B b = new B();
A a2 = b; // korrekt!

Beispiel 2: Nehmen wir an, es gäbe die Klassen Fahrzeug, Auto und Fahrrad, wie oben skizziert.

Fahrzeug f;
Auto a = new Auto();
f = a; // korrekt

Fahrzeug f2 = new Fahrrad(); // ebenso!

Das ist korrekt, denn: ein Auto ist ein Fahrzeug und ein Fahrrad ist ein Fahrzeug. Umgekehrt geht es nicht:

Auto a;
a = new Fahrzeug(); // falsch!

denn ein Fahrzeug ist nicht (immer) ein Auto.

Video: Klassenhierarchie und Vererbung (12:55)

MyTunes 2: Vererbung nutzen

Mit unserem neuen Wissen verbessern wir unsere Datenbank. Da die Instanzvariablen vererbt werden, können wir die gemeinsamen Variablen in eine neue Oberklasse "Medium" auslagern:

Was wird vererbt? Die Klasse Movie erbt die Eigenschaften title und file, hat also insgesamt die drei Eigenschaften:

  • title
  • file
  • director

Die Klasse Song erbt ebenfalls die Eigenschaften title und file, hat also insgesamt die drei Eigenschaften:

  • title
  • file
  • artist

Daher kann man auch sagen, dass die Unterklasse Song ihre Oberklasse erweitert (engl. to extend).

Der Klasse Medium geben wir einen eigenen Konstruktor:

public class Medium {
  private String title;
  private String file;

  public Medium(String aTitle, String aFile) {
    title = aTitle;
    file = aFile;
  }
}

Verwendung von super()

Um Code-Duplizierung zu vermeiden, können Sie im Konstruktor einer Klasse den Konstruktor der Oberklasse aufrufen. In unserem Beispiel werden die Variablen title und file im Konstruktor von Medium initialisiert. Wir wollen diesen Code nicht im Konstruktor von Movie duplizieren.

Also rufen wir im Konstruktor von Movie den Konstruktor der Oberklasse mit super() auf. super() hat immer die gleiche Anzahl von Parametern wie der Konstruktor (bzw. einer der Konstruktoren) der Oberklasse. In diesem Fall sind das zwei Parameter:

public class Movie extends Medium {
  private String director;

  public Movie(String aTitle, String aDirector, String aFile) {
    super(aTitle, aFile);
    director = aDirector;
  }

  public String toString() {
    return "MOVIE \"" + title + "\" von " + director +
           ", zu finden unter: " + file;
  }
}

Wichtig: super() muss immer die erste Anweisung im Konstruktor sein. Es gibt übrigens noch eine weitere Verwendung von super, um Methoden der Oberklasse aufzurufen. Letzteres wird aber ohne Klammern geschrieben und behandeln wir später. Nicht verwechseln!

Analog für die Klasse Song:

public class Song extends Medium {
  private String artist;

  public Song(String aTitle, String aArtist, String aFile) {
    super(aTitle, aFile);
    artist = aArtist;
  }

  public String toString() {
    return "SONG \"" + title + "\" von " + artist +
           ", zu finden unter: " + file;
  }
}

Wir passen den Teil an, wo wir die Listen erzeugen:

Jetzt können wir alle Objekte in einer Liste vom Typ Medium ablegen, da Movie und Song Subtypen von Medium sind. Die Verwaltung wird so wesentlich übersichtlicher:

public static void main(String[] args) {

  // Nur noch eine Liste für beide Medientypen
  ArrayList<Medium> media = new ArrayList<Medium>();

  media.add(new Movie("Alien", "Ridley Scott", "alien.mov"));
  media.add(new Movie("Der Pate", "Francis Ford Coppola", "pate.mov"));
  media.add(new Movie("Gravity", "Alfonso Cuarón", "gravity.avi"));
  media.add(new Song("OMG!", "Marteria", "omg.wav"));
  media.add(new Song("Happy", "Pharrell Williams", "happy.wav"));

  // Nur noch eine For-Schleife zur Ausgabe
  for (Medium m: media) {
    System.out.println(m);
  }
}

Einschub: Der ternäre Operator statt If-Else

Wir benötigen den sogenannten ternären Operator für einige Übungsaufgaben. Außerdem ist es gut, ihn zu kennen, weil es ihn in vielen Programmiersprachen gibt.

Es gibt Fälle, wo das If-Else etwas sperrig ist. Nehmen wir an, Ihr Programm soll ausgeben, ob jemand jung (< 40 Jahre) oder alt (>= 40 Jahre) ist:

public String altersklasse(int alter) {
  if (alter < 40) {
    return "jung";
  } else {
    return "alt";
  }
}

Der sogenannte ternäre Operator erlaubt es Ihnen, diese drei Bestandteile (Bedingung, Fall 1, Fall 2) sehr kompakt in folgender Form zu schreiben:

BEDINGUNG ? FALL1 : FALL2

In unserem Beispiel wäre dies:

public String altersklasse(int alter) {
  return alter < 40 ? "jung" : "alt";
}

Der ternäre Operator wird auch häufig verwendet, um Strings zusammenzusetzen. Dann müssen Sie den ganzen Ausdruck einklammern:

int alter = 50;
System.out.println("Sie sind " + (alter < 40 ? "jung" : "alt"));

Übungsaufgaben

18.2 a) (a) Ternärer Operator

Schreiben Sie die Klasse Diplom, um die guten alten Diplome auszudrucken. Dabei unterscheiden wir zwischen Fachhochschul-Diploma (FH) und Universitäts-Diploma (Univ). Die Klasse hat folgende Eigenschaften (unbedingt die Typen beachten!):

  • person: ein String mit dem Namen des/der Absolventen/in
  • mitAuszeichnung: boolean
  • fachhochschule: boolean

Schreiben Sie einen Konstruktor mit einem Parameter (für die Person) und zwei Setter-Methoden für die booleschen Eigenschaften.

Schließlich schreiben Sie eine toString-Methode, so dass eine Ausgabe in folgender Art herauskommt:

FH-Diplom für Lisa Schmidt
Univ-Diplom für Donald Duck mit Auszeichnung
FH-Diplom für Daisy Duck mit Auszeichnung

Sie sollten mit folgendem Testcode (in der statischen main-Methode) den obigen Output erzeugen können:

Diplom d1 = new Diplom("Lisa Schmidt");
d1.setFachhochschule(true);

Diplom d2 = new Diplom("Donald Duck");
d2.setMitAuszeichnung(true);

Diplom d3 = new Diplom("Daisy Duck");
d3.setFachhochschule(true);
d3.setMitAuszeichnung(true);

System.out.println(d1);
System.out.println(d2);
System.out.println(d3);

18.2 b) (b) Eine Unterklasse

Schreiben Sie die Klasse Person mit den Instanzvariablen name und isMale (boolean). Schreiben Sie die Klasse Student als Unterklasse von Person mit der Instanzvariablen matrikelnummer.

Schreiben Sie Konstruktoren für beide Klassen. Schreiben Sie für die Klasse Student eine aussagekräftige toString-Methode.

Um Ihre Klassen zu testen, erzeugen Sie zwei Variablen a und b vom Typ Person (nicht vom Typ Student). In jeder Variable speichern Sie ein neues Student-Objekt. Anschließend geben Sie a und b mit System.out.println() aus. Die Ausgabe könnte so aussehen:

Lisa Müller, Studentin, Matrikelnr. 809221
Augustin Burg, Student, Matrikelnr. 007007
Tipp
Wenn Sie sich wundern, dass 007007 nicht so aussieht, wie es sein sollte, lesen Sie Kap. 2.3.7 in Java ist auch eine Insel .

18.2 c) (c) Mehrere Unterklassen

Nehmen Sie den Code aus (b) und erweitern Sie ihn. Schreiben Sie die neue Klasse Professor (ebenfalls Unterklasse von Person) mit der Instanzvariablen hasDrTitle (boolean), mit Konstruktor und mit toString-Methode.

Um alle Klassen zu testen, erzeugen Sie vier Variablen a, b, c, d - jeweils vom Typ Person (nicht vom Typ Student). In jeder Variable speichern Sie ein neues Objekt (je Student oder Professor). Anschließend geben Sie a, b, c und d jeweils mit System.out.println() aus. Die Ausgaben für vier Objekte könnten so aussehen:

Lisa Müller, Studentin, Matrikelnr. 809221
Professor Dr. Rainer Unsinn
Augustin Burg, Student, Matrikelnr. 007007
Professorin Hilde Brün
Tipp
Der Professoren- und Doktortitel sollte nicht in der Variablen name gespeichert werden, sondern im toString() erzeugt werden.
Tipp
Schauen Sie sich den ternären Operator im obigen Kapitel 17.2 an.

Jetzt legen Sie im Hauptprogramm eine ArrayList an (von welchem Typ?) und fügen Sie ihr Objekte hinzu. Verwenden Sie anschließend eine Foreach-Schleife, um die Objekte mit System.out.println() auszugeben. Warum können Sie sowohl Student- als auch Professor-Objekte in dieser Liste speichern? Argumentieren Sie mit dem Subtyp.

18.2 d) (d) Restaurant-Klassen

Sie sollen für ein italienisches Restaurant folgende Klassen programmieren (in Klammern stehen die Instanzvariablen, finden Sie jeweils einen sinnvollen Typ):

  • Pizza (preis, titel, belag, vegetarisch)
  • Gericht (preis, titel)
  • Pasta (preis, titel, sosse)

Bringen Sie die Klassen in eine sinnvolle Ober-/Unterklassenbeziehung und verteilen Sie entsprechend die Eigenschaften auf die Klassen.

Programmieren Sie die drei Klassen. Fügen Sie eine aussagekräftige toString-Methode hinzu und testen Sie Ihre Klassen im Hauptprogramm, indem Sie einige Gerichte anlegen und auf der Konsole ausgeben.

Zusammenfassung

Zwei Klassen A und B können in einer Beziehung zueinander stehen. Wenn A Oberklasse von B ist, dann erbt Klasse B alle Instanzvariablen und Methoden von A. Man sagt auch, dass B Unterklasse von A ist. Im Englischen spricht man von superclass und subclass.

Im Code kennzeichnet man dies nur in der Unterklasse mit dem Stichwort extends:

class A {
...
}

class B extends A {
...
}

Wenn B Unterklasse von A ist, dann ist B auch Subtyp von A. Das bedeutet ein Objekt vom Typ B ist gleichzeitig vom Typ A. Wenn die Klasse Animal Oberklasse von Hase ist, dann ist ein Objekt vom Typ Hase gleichzeitig vom Typ Animal. Folgender Code ist also korrekt:

Animal x = new Hase(); // korrekt

Im Konstruktor der Unterklasse kann man mit super() einen beliebigen Konstruktor der Oberklasse aufrufen. Die super-Anweisung muss allerdings die erste Anweisung im Konstruktor sein.

Wir konnten unseren MyTunes-Code erheblich verbessern, indem wir die Oberklasse Medium einführten. Der gemeinsame Code von Song und Movie liegt jetzt in der Oberklasse, die Code-Duplizierung wurde eliminiert.

18.3 Vererbung allgemein

Im obigen Beispiel haben wir eine einfache Klassenhierarchie gesehen. Schauen wir uns eine etwas komplexere an:

In diesem Klassendiagramm sehen Sie für jede Klasse die Instanzvariablen und die Methoden in eigenen Bereichen. Etwas ungewohnt ist die Angabe der Datentypen nach dem Format

<Variablenname> : <Typ>

Das hängt damit zusammen, dass Klassendiagramme auch für andere Programmiersprachen als Java/Processing verwendet werden und jede Programmiersprache eine andere Schreibweise hat.

Zugriffsmodifikatoren 2

Sie wissen bereits, dass public bedeutet: Jeder kann auf die Klasse/Variable zugreifen, insbesondere können andere Objekte darauf zugreifen. Dagegeben heißt private, dass nur ein Objekt dieser Klasse darauf zugreifen kann. Wichtig ist, dass bei private selbst eine Unterklasse nicht auf die Variable/Methode zugreifen kann.

Jetzt gibt es noch einen weiteren Modifikator: protected. Mit diesem Modifikator dürfen auch die Unterklassen auf die Variable/Methode zugreifen. Zusätzlich gilt (und das ist vielleicht etwas kontra-intuitiv): auch alle Klassen innerhalb des gleichen Pakets haben Zugriff.

Nochmal die folgende Tabelle, die zusammefasst, wer Zugriff auf eine(n) Klasse, Konstruktor, Methode oder Instanzvariable hat:

ModifikatorEigene KlasseUnterklassenKlassen im gleichen PaketAlle Klassen
privateX   
protectedXXX 
publicXXXX
(leer)X X 

Sie sehen hier auch, was passiert, wenn Sie den Modifikator ganz weglassen ("leer"). Diesen Fall nennt man package-private. Hier haben die Unterklassen keinen Zugriff, dafür aber alle Klassen im selben Paket. Diese Kombination ist selten sinnvoll, daher sollten Sie immer einen Modifikator hinschreiben.

Oberklassen und direkte Oberklassen

Sie wissen, dass eine Unterklasse C alle Instanzvariablen und Methoden von Ihrer Oberklasse B erbt. Wenn Klasse B wiederum eine Oberklasse A hat, dann erbt C diese Dinge auch von A! Im Beispiel oben erbt Klasse Auto auch die Instanzvariablen und Methoden der Klasse Produkt.

Dies liegt daran, dass Produkt auch eine Oberklasse von Auto ist. Allgemein kann man sagen:

WENN: A ist Oberklasse von B UND B ist Oberklasse von C
DANN GILT: A ist Oberklasse von C

(Diese Eigenschaft nennt man Transitivität.)

Dann gilt allgemein: Eine Klasse erbt die Instanzvariablen und Methoden von all ihren Oberklassen. Im Beispiel oben nennt man übrigens die Klasse Fahrzeug die direkte Oberklasse von Auto, um anzuzeigen, dass keine andere Klasse zwischen den beiden ist.

Jede Klasse hat nur eine direkte Oberklasse

Eine wichtige Regel in Java ist, dass jede Klasse maximal eine direkte Oberklasse haben darf. Das heißt, das hier ist nicht erlaubt (obwohl es im echten Leben durchaus Sinn macht):

Warum nicht? Man könnte doch einfach die Eigenschaften und Methoden von beiden Oberklassen erben. Probleme gibt es aber, wenn die zwei Oberklassen Variablen haben, die genau gleich heißen, oder Methoden haben, die die gleiche Signatur haben. Welche Variable soll Java wählen? In Java hat man dies also verboten, aber in anderen Sprachen wie C++ oder LISP ist es durchaus erlaubt, erfordert aber Zusatzmechanismen (siehe Wikipedia Mehrfachvererbung).

Konstruktoren: Wichtige Regeln

Sie wissen, dass man pro Klasse mehrere Konstruktoren definieren kann. Sie wissen, dass man mit super() einen Konstruktor der Oberklasse aufrufen kann.

Hier noch der Vollständigkeit halber ein paar Regeln.

Regel 1: Wenn in einer Klasse kein Konstruktor definiert ist, wird automatisch ein leerer Konstruktor ohne Parameter definiert.

Sie können also folgendes definieren:

public class Foo {

  // kein Konstruktor!

  // eine Testmethode
  public void hello() {
    System.out.println("servus");
  }

}

Beim Erzeugen einer neuen Instanz wird der Konstruktor ohne Parameter aufgerufen - er wird von Processing hinzugefügt:

public static void main(String[] args) {
  Foo foo = new Foo();
  foo.hello();
}
servus

Regel 2: Sobald mindestens ein eigener Konstruktor definiert wird, wird der Konstruktor ohne Parameter nicht automatisch generiert.

Erweitern Sie die obige Klasse um einen Konstruktor (der Parameter x hat keine Funktion)...

public class Foo {

  public Foo(int x) {
  }

  public void hello() {
    System.out.println("servus");
  }

}

... dann können Sie das Programm nicht mehr ausführen. Sie erhalten die Fehlermeldung:

The constructor ...Foo() is undefined

Regel 3: Hat Ihre Klasse eine Oberklasse, dann wird in allen Konstruktoren automatisch super() aufgerufen, sobald eine Instanz erzeugt wird - sofern nicht ein eigenes super() vom Programmierer eingefügt wurde.

public class Bar {

  public Bar() {
    System.out.println("Konstruktor von Bar");
  }

}

public class Foo extends Bar {

  public Foo() {
    // Java führt hier super() aus
    System.out.println("Konstruktor von Foo");
  }

}

Wenn Sie eine Instanz von Foo erzeugen...

public static void main(String[] args) {
  Foo foo = new Foo();
}

...sehen Sie, dass zuerst der Konstruktor der Oberklasse aufgerufen wird. Erst dann wird der Code des Konstruktors von Foo ausgeführt:

Konstruktor von Bar
Konstruktor von Foo

Ein subtiler Fehler tritt auf, wenn Sie in der Oberklasse einen eigenen Konstruktor mit Parameter/n definieren. Es wird kein Konstruktor ohne Parameter automatisch definiert (Regel 2).

public class Bar {

  // Konstruktor hat jetzt Parameter
  public Bar(int x) {
    System.out.println("Konstruktor von Bar");
  }

}

public class Foo extends Bar {

  public Foo() {
    System.out.println("Konstruktor von Foo");
  }

}

Wenn Sie wieder eine Instanz von Foo anlegen wollen, versucht Processing den Konstruktor ohne Parameter von Bar aufzurufen, der ja nicht existiert. Dies endet mit einem Fehler:

Implicit super constructor ...Bar() is undefined.

Um den Fehler zu beheben, gibt es drei Möglichkeiten: (a) Sie fügen einen leeren Konstruktor zu Bar hinzu, (b) Sie löschen den eigenen Konstruktor, so dass der parameterlose Konstruktor automatisch erzeugt wird oder (c) Sie rufen selbst super() auf, allerdings mit Parameter.

public class Bar {

  public Bar(int x) {
    System.out.println("Konstruktor von Bar");
  }

}

public class Foo extends Bar {

  public Foo() {
    super(5);
    System.out.println("Konstruktor von Foo");
  }

}

18.4 Processing-Beispiel

Die gleichen Techniken funktionieren auch in Processing! Schauen wir uns ein Beispiel aus dem Bereich Animation an.

Wir haben den typischen, sich bewegenden und abprallenden Ball als Klasse:

class Ball {
  PVector location;
  PVector speed;

  // Konstruktor mit zufälligen Anfangswerten
  // für Startpunkt und Geschwindigkeit
  Ball() {
    location =
    new PVector(random(0, width), random(0, height));
    speed =
      new PVector(random(-3, 3), random(-3, 3));
  }

  // Zeichnen
  void render() {
    ellipse(location.x, location.y, 20, 20);
  }

  // Koordinaten anpassen und das Abprallen regeln
  void update() {
    location.add(speed);
    if (location.x > width || location.x < 0) {
      speed.x = -speed.x;
    }

    if (location.y > height || location.y < 0) {
      speed.y = -speed.y;
    }
  }
}

Das Hauptprogramm könnte so aussehen (wichtig ist, dass der Konstruktor in setup aufgerufen wird und nicht im Variablenteil darüber, weil dort height und width noch nicht gesetzt sind).

Ball b;

void setup() {
  size(200, 200);
  b = new Ball();
}

void draw() {
  background(0);
  fill(255);
  noStroke();

  b.update();
  b.render();
}

Jetzt stellen Sie sich vor, Sie möchten ein weiteres Objekt fliegen lassen, ein Quadrat. Im Grunde wäre der Code fast identisch.

Einziger Unterschied: die Methode render() funktioniert bei Quad anders, da die natürlich ein Quadrat zeichnet...

class Quad {
  PVector location;
  PVector speed;

  Quad() {
    location =
    new PVector(random(0, width), random(0, height));
    speed =
      new PVector(random(-3, 3), random(-3, 3));
  }

  void render() {
    rectMode(CENTER);
    rect(location.x, location.y, 20, 20);
  }

  void update() {
    location.add(speed);
    if (location.x > width || location.x < 0) {
      speed.x = -speed.x;
    }

    if (location.y > height || location.y < 0) {
      speed.y = -speed.y;
    }
  }
}

Wieder hilft uns die Klassenhierachie. Wir führen eine neue Klasse ein, die alle gemeinsamen Elemente enthält:

  • Standort (location)
  • Geschwindigkeitsvektor (speed)
  • update-Methode

Wir nennen die neue Oberklasse Mover:

Im Code verschieben wir alle Funktionalität in Mover, bis auf die Methode render natürlich:

class Mover {
  PVector location;
  PVector speed;

  Mover() {
    location =
      new PVector(random(0, width), random(0, height));
    speed =
      new PVector(random(-3, 3), random(-3, 3));
  }

  void update() {
    location.add(speed);
    if (location.x > width || location.x < 0) {
      speed.x = -speed.x;
    }

    if (location.y > height || location.y < 0) {
      speed.y = -speed.y;
    }
  }
}

Die Klassen Ball und Quad sind Unterklassen von Mover und enthalten wesentlich weniger Code:

class Ball extends Mover {
  void render() {
    ellipse(location.x, location.y, 20, 20);
  }
}

class Quad extends Mover {
  void render() {
    rectMode(CENTER);
    rect(location.x, location.y, 20, 20);
  }
}

Mit folgendem Hauptprogramm können Sie den Code testen:

void setup() {
  size(200, 200);
  b = new Ball();
  q = new Quad();
}

void draw() {
  background(0);
  fill(255);
  noStroke();

  b.update();
  q.update();
  b.render();
  q.render();
}

Die obige Klasse Mover könnte in einem Computerspiel eine Menge verschiedener Unterklassen haben: Hinternisse, Bösewichter, Lebensrationen und Waffen... Doch wir haben ein Problem: wie verwalten wir all diese Objekte effizient? Für jeden Bestandteil des Spiels eine eigene Variable anzulegen, wäre kaum machbar. Wir hätten viel lieber eine Liste:

ArrayList<Mover> things = new ArrayList<Mover>();

Dann könnten wir in draw() einfach alle Objekte mit einer For-Schleife updaten und zeichnen:

for (Mover m: things) {
  m.update();
  m.render(); // Fehler: Mover hat kein render
}

Zurzeit geht das nicht, weil die Klasse Mover keine Methode render hat. Muss auch so sein, denn die Klasse Mover hat ja keine bestimmte Form, die sie zeichnen kann. Wir können dieses Problem erst im nächsten Kapitel mit Hilfe von abstrakten Klassen und abstrakten Methoden lösen.

Übungsaufgabe

18.4 a) (a) Spielerfigur

Übernehmen Sie den Code aus dem obigen Abschnitt mit den Klassen Mover, Ball und Quad.

Sie möchten eine Spielerfigur einführen (Klasse Player). Diese wird als Dreieck gezeichnet und soll mit den Cursortasten zu bedienen sein. Das heißt, die Klasse Player kann zwar die Variable location gebrauchen, nicht aber speed (und auch nicht update).

Wie müssen Sie die Klassen umbauen, dass Player nur die Variable location erbt?

Tipp: Führen Sie eine neue Klasse ein.