Objektorientierte Programmierung
Willemers Informatik-Ecke
Dies ist ein Auszug aus meiner Vorlesung "Objektorientierte Programmierung" für den Studiengang Medieninformatik an der Hochschule Flensburg.

Inhalte basieren auf meinem Buch Java (Alles in einem Band) für Dummies.

Die Themen werden anhand der GUI-Programmierung vertieft. In diesem Fall wurde Java Swing wegen seiner einfach gehaltenen API verwendet.

Die Klasse als zentraler Baustein der OOP

Weitere sprachspezifische Informationen:

Die Klasse und ihre Attribute

Die Datenanteile einer Klasse nennt man Attribute. Diese Attribute bilden ein Objekt der realen Welt nach. Das Auto kann viel mehr Attribute haben, als eine Klasse sinnvollerweise fassen kann. Der Programmierer wird nur jene Attribute implementieren, die sein Programm benötigt.

Ein Händlerprogramm wird vor allem Einkaufs- und Verkauspreis speichern. Das Programm des Finanzamts errechnet die Steuer aus Hubraum und CO2-Ausstoß. Der Veranstalter von Autorennen interessiert sich für die Höchstgeschwindigkeit. Der Familienvater für die Anzahl der Sitzplätze.

Dasselbe reale Objekt kann in verschiedenen Programmen zu unterschiedlichen Attributen führen.

Die Attribute speichern die Eigenschaften einer Klasse. Man kann auch vom Status sprechen.

Die Kasse und ihre Methoden

Die Klasse wird nicht nur durch ihre Attribute definiert, sondern in besonderem Maß durch ihre Methoden. So bestehen die Koordinaten genauso wie ein Bruch aus zwei Zahlen. Aber für einen Bruch werden alle mathematischen Grundrechenarten sinnvol definiert, während Koordinaten bestenfalls addiert werden können.

Beispiele

Datum
Attribute: Ein Datum besteht aus den drei int-Werten für Tag, Monat und Jahr.

Methoden: Eine typische Methode für ein Datum ist die Berechnung des Wochentags.

Bruchrechnung
Attribute: Ein Bruch besteht aus zwei int-Werten je für Zähler und Nenner.

Methoden: Die Klasse Bruch muss Methoden für die Grundrechenarten zur Verfügung stehen, weil Java nicht weiß, wie diese auf Brüche wirken.

Koordinate
Attribute: Eine Koordinate besteht - wie ein Bruch - aus zwei Zahlenwerten.

Methoden: Der entscheidende Unterschied zu Bruch liegt weniger in den gespeicherten Daten als in den Methoden. Während ein Bruch dividiert werden kann, wird bei Koordinaten vielleicht der Winkel und die Entfernung zueinander bestimmt.

Klassendefinition

Die Klasse Bruch enthält einen Zähler und einen Nenner:
class Bruch {
   int zaehler, nenner;
}
In Java muss jede Klasse in einer eigenen Datei stehen, die den Namen der Klasse trägt. Die Klasse Bruch steht also in einer Datei namens Bruch.java. In anderen Sprachen ist das zwar empfehlenswert, aber nicht zwingend.

Anlegen eines Objekts

public static void main(String[] args) {
     Bruch meinAnteil;
     meinAnteil = new Bruch();
     meinAnteil.zaehler = 3;
     meinAnteil.nenner = 4;
}

Die Methode zur Multiplikation

Da Java keinen Bruch mit * multiplizieren kann, muss die Klasse Bruch eine Multiplikation liefern.

Das Bruch-Objekt, über das multipliziert wird, soll sich durch die Multiplikation nicht ändern. (Der Anteil soll immer gleich bleiben, egal welche Beute darüber multipliziert wird.) Also muss das Ergebnis der Multiplikationsmethode zurückgegeben werden.

public class Bruch {

   int zaehler, nenner;

   public Bruch mul(int wert) {
       Bruch ergebnis = new Bruch();
       ergebnis.zaehler = this.zaehler * wert;
       ergebnis.nenner = this.nenner;
       return ergebnis;
   }
}
Wenn aber die Beute ein halbes Schwein ist, muss die Klasse Bruch auch Brüche multiplizieren können.
public class Bruch
{
   int zaehler, nenner;
   public Bruch mul(int wert) {
       Bruch ergebnis = new Bruch();
       ergebnis.zaehler = this.zaehler * wert;
       ergebnis.nenner = this.nenner;
       return ergebnis;
   }
   public Bruch mul(Bruch wert) {
       Bruch ergebnis = new Bruch();
       ergebnis.zaehler = this.zaehler * wert.zaehler;
       ergebnis.nenner = this.nenner * wert.nenner;
       return ergebnis;
   }
}

Überladen

Es dürfen mehrere Methoden gleichen Namens in einer Klasse existieren, sofern sie sich durch ihre Parameter unterscheiden. Der Rückgabewert ist diesbezüglich kein Unterscheidungsmerkmal!

Java-Klassen dürfen keine Operatoren überladen, also nicht den * der Multiplikation oder das + der Addition, weil man den Programmierer vor sich selbst schützen möchte. Andere Sprachen wie C++ trauen ihren Programmierern mehr Verantwortungsgefühl zu.

Konstruktoren

Hinter dem Befehl new steht der Aufruf des Konstruktors.

Beispiel: Konstruktor für Bruch

Für die Klasse Bruch bietet sich ein Konstruktor mit zwei Parametern an. So wird bei der Erstellung bereits der Wert vorgeben.

public class Bruch
{
   int zaehler, nenner;
   public Bruch(int zaehler, int nenner) {
       this.zaehler = zaehler;
       this.nenner  = nenner;
   }
}
Beim Erstellen eines Bruches kann der Wert übergeben werden:
Bruch einHalb = new Bruch(1, 2);
Danach funktioniert die folgende Zeile nicht mehr. Warum?
Bruch einHalb = new Bruch(); // führt zu einem Fehler

Standard-Konstruktor

Der Standard-Konstruktor ist ein Konstruktor ohne Parameter

Noch einmal das Anlegen eines Objekts

Kehren wir noch einmal zur Erzeugung eines Objekts zurück und schauen uns die Zeile genauer an:
Bruch meinAnteil = new Bruch();

Zur Einübung: Aufgaben zur Bruchrechnung

Vererbung, Ableitung, Erweiterung

Alle drei Begriffe stehen für den gleichen Inhalt. Es geht darum, dass Klassen oft gleiche Attribute und Methoden haben, weil sie einander ähnlich sind.

In solchen Fällen greift die Regel, dass ein Programm möglichst keinen wiederholenden Code haben sollte, weil ansonsten Fehler mitkopiert werden und sich gleicher Code im Zuge von Korrekturen auseinander entwickelt. In jedem Fall werden Programme mit überflüssigen Codezeilen unübersichtlicher.

Eine abgeleitete bzw. erweiternde Klasse wird in Java mit dem Schlüsselwort extends gekennzeichnet. C++ setzt hinter einem Doppelpunkt den Namen der Basisklasse. Python nennt die Basisklasse in Klammern.

Beispiel: Student und Professor

Beispielsweise hat ein Student und ein Professor eine Telefonnummer, eine Adresse, einen Namen und einen Geburtstag gemeinsam. Darüber hinaus gehören beide einer Hochschule an.

Sie unterscheiden sich darn, dass der Student eine Matrikelnummer besitzt, während der Professor einen Lehrstuhl innehat.

Es ist hochgradig ärgerlich, wenn man für die Klasse Student und die Klasse Professor die Gemeinsamkeiten jedes Mal neu implementiert. Die Idee bei der Vererbung ist es, eine Klasse zu schaffen, die die gemeinsamen Eigenschaften realisiert. Die Klassen Student und Professor beziehen sich auf diese Basisklasse und definieren nur noch, was sie von der Basisklasse unterscheidet.

class Hochschulangehoeriger {
    String name;
    String telefon;
    Date geburtsdatum;
    String hochschule;
    // ...
}

class Student extends Hochschulangehoeriger {
    int martikelnr;
}

class Professor extends Hochschulangehoeriger {
    String lehrstuhl;
}

Wenn Sie nun ein Objekt der Klasse Student erzeugen, hat dieses neben der Matrikelnummer auch eine Adresse und einen Namen, obwohl das in der Klasse Student nicht explizit aufgeführt ist. Das Objekt enthält die Eigenschaften von Student und alle Eigenschaften der Klasse Hochschulangehoeriger.

Beispiel: Personen

Die Welt besteht aber nicht nur aus Hochschulangehörigen. Es gibt Zahnärzte, Kaufleute, Rentner und Kinder. Diese haben alle einen Namen und Adresse, gehören aber keiner Hochschule an. Also bauen Sie eine neue Klasse Person, die alle Gemeinsamkeiten enthält und leiten davon die Klasse Hochschulangehoeriger ab.
class Person {
    String name;
    String telefon;
    Date geburtsdatum;
    // ...
}

class Hochschulangehoeriger extends {
    String hochschule;
}
Spinnen wir weiter: Es entspannt sich eine baumartige Abhängigkeit von Vererbungen.

Die Ist-Ein-Beziehung

Wenn eine Klasse A alle Elemente einer anderen Klasse B besitzt und zusätzliche Attribute kennt, sagt man
  • A erweitert B
  • A IST EIN B (Ein Professor IST EINE Person)
  • C ist Attribut von D
  • D HAT EIN C (Ein Professor HAT EINEN Lehrstuhl)

In vielen Fällen hat man Wahlmöglichkeiten. Hier ist das gestalterische Geschick und die Übung beim Modellieren gefragt. Beispielsweise:

Namensräume

Namensräume werden geschaffen, damit in einem Programm zwei Klassen gleichen Namens nicht in Konflikt geraten. Jedes Team bzw. jeder Arbeitsbereich bekommt einen eigenen Namensraum und wird so frei in der Namenswahl.

Java Packages

Ein Package ist eine Gliederungsstruktur für Klassen, die oft verwendet wird, um Bibliotheken zusammenzufassen.
package verkauf;      // Muss immer der erste Befehl sein!

public class Mitarbeiter {
Beim Aufruf wird der Package-Name vor die Klasse gestellt, um die Klasse zu identifizieren, die zu einem anderen Namensraum gehört.
verkauf.Mitarbeiter erwin = new verkauf.Mitarbeiter();

import

Überschneidet sich der Klassenname nicht mit anderen verwendeten Klassen, kann ein import die Wiederholung des Package-Namen sparen.
import verkauf.Mitarbeiter;
...
Mitarbeiter erwin = new Mitarbeiter();

Namensräume unter C++

C++ verwendet das Schlüsselwort namespace, um Namensräume zu deklarieren. Mit dem Befehl using namespace kann man die Namen eines fremden Namensraums in den eigenen integrieren, was kein Problem bereitet, so lange sich keine gleichen Namen überdecken.

Sichtbarkeit: Wir haben etwas zu verbergen

Eine Klasse sollte nur so viel offen darlegen, wie für die Nutzer der Klasse unbedingt erforderlich. Alles was in der Klasse verborgen ist, kann später ohne Auswirkung auf andere Klassen verändert werden.

Für die Sichtbarkeit und den Zugriff werden Attributen, Methoden und Klassen Modifizierer vorangestellt:

Die folgende Klasse Datum berechnet den Wochentag nur dann neu, wenn es wirklich erforderlich ist:
public class Datum {
    private int tag, monat, jahr, wochentag=-1;

    public int getWochentag() {
        if (wochentag<0) {
            wochentag = berechneWochentag();
        }
        return wochentag;
    }
    ...
    public setTag(int tag) {
        this.tag = tag;
        wochentag = -1;
    }
}
Der bisher errechnete Wochentag kann so lange geliefert werden, solange weder Tag, noch Monat, noch Jahr verändert wurden. Kontrolliert wird dies dadurch, dass diese Attribute private sind. Nur die set-Methoden (sogenannte Setter) können die Attribute ändern. Geschieht dies, wird wochentag auf -1 gesetzt, wodurch getWochentag bei seinem nächsten Aufruf eine Neuberechnung veranlasst.

Oftmals werden Klassen zur Modellierung und zum Transport von Daten verwendet. Dann werden alle Attribute private definiert und für jedes Attribut eine set- und eine get-Methode erstellt. So können Attribute, die nicht von außen verändert werden sollen, leicht schreibgeschützt werden, indem man keine set-Methode zur Verfügung stellt. Eine solche Klasse wird bei Java Java Beans genannt.

Grundsätzlich sollten die Attribute jeder Klasse möglichst als private definiert werden. Für den Zugriff sollte eine set-Methode zum Schreiben und eine get-Methode zum Lesen erstellt werden.

Polymorphie

Die Polymorphie setzt Vererbung voraus. Folgende Effekte werden benutzt: Trotz der zunächst etwas kompliziert erscheinenden Beschreibung ahmt dieses Verhalten die natürliche Erwartung nach.

Die Figur

Betrachten wir eine Figur. Die Basisfigur hat beispielsweise nur die Attribute x und y, die besagen, wo die Figur ihren Ursprung hat.

Die Figur hat auch eine Methode zeichne.

Es gibt als Erweiterungen Rechtecke, Dreiecke und Kreise. Als Sonderform der Kreise vielleicht noch Ellipsen und Kreisausschnitte. Alle diese erweiternden Klassen implementieren die Methode zeichne und stellen die Ellipsen, Quadrate und Kreisausschnitte dar.

Wir können eine Methode schreiben, die eine Referenz auf eine Figur als Parameter hat. Dieser Methode können wir einen Kreis übergeben, weil ein Kreis durch die Vererbung eben eine Figur ist. Wenn die Methode nun auf das übergebene Objekt die Methode zeichne anwendet, werden wir erwarten, dass ein Kreis gezeichnet wird. Genau dies erfolgt bei der Polymorphie.

Polymorphie bedeutet, dass das Objekt weiß, welchen Typs es ist und zur Laufzeit erkennt, welche Methode im Vererbungsbaum aufgerufen werden muss.

Späte Bindung

Die Entscheidung, die Methode welcher Klasse gewählt wird, fällt zur Laufzeit, weil erst dann entschieden werden kann, welcher Klasse das Objekt, über das die Methode aufgerufen wird, überhaupt ist. Darum spricht man hier auch von später Bindung.

Da der Compiler nicht festlegen kann, welche Methode aufgerufen werden soll, muss das Laufzeitsystem diese Information irgendwo finden.

Java
Eine Referenz enthält in Java immer zwei Informationen. Einmal die Position des Objekts im Speicher. Dazu kommt der Name der Klasse. Beides kann man sehen, wenn man eine Referenz direkt per System.out.print auf dem Bildschirm ausgibt.
C++
In C++ wird diese Information in einer V-Table im Objekt gespeichert. Da das dazu führt, dass das Objekt größer wird als die reine Zusammensetzung der Attribute, muss dies in C++ explizit in der Basisklasse durch das Schlüsselwort virtual angekündigt werden.

Beispiel: Die Mensa der Universität Gintoft

Was heißt das? Ein Beispiel finden Sie in der Mensa der Universität Gintoft. Diese ist sehr klein und hat darum auch einen Aushilfskoch ohne Ambitionen. Er kann eigentlich nur zwei Gerichte: Kartoffelbrei, den er aus einer hellen Pampe anrührt und Suppe, die er aus einem roten Pulver anrührt. Wir sollen eine Speisekarte für die Mensa erstellen. Dazu müssen wir die Mahlzeiten modellieren.

Kartoffelpampe

Fangen wir an mit dem Kartoffelbrei, der dienstags als Kartoffelschnee und donnerstags als Purree de Pomes angeboten werden soll. Zunächst wird die Basisklasse erstellt:
class Kitt {
    private double preis = 0.50;
    public double kostet() {
        return preis;
    }
}
Wir erweitern zu Kartoffelschnee.
class Schnee extends Kitt { 
    private String name = "Kartoffelschnee";
    public String getName() {
        return name;
    }
}
Und dann noch zu Purree de Pommes:
class Purree extends Kitt { 
    private String name = "Purree de Pommes";
    public String getName() {
        return name;
    }
}

Suppe

Als Basisklasse gibt es Suppe:
class Suppe {
    private double preis = 0.70;
    public double kostet() {
        return preis;
    }
}
Das Pulver ist rot, darum wird es Montags als Tomatensuppe verkauft:
class Tomatensuppe extends Suppe {
    private String name = "Tomatensuppe";
    public String getName() {
        return name;
    }
}
Mittwochs sind noch Reste da, die dann eher braun sind und darum als Gulaschsuppe verkauft werden kann. Und schließlich wird die Brühe am Freitag grün und kann als Erbsensuppe angeboten werden. Sie ist dann auch würziger.

Allerding protestiert der AStA, dass nach EU-Vorschrift eine Erbsensuppe auch vereinzelt Erbsen enthalten muss. Der Koch kippt noch eine Dose Erbsen in den Topf. Das verteuert die Suppe um einen Cent auf 71 Cent.

class Erbsensuppe extends Suppe {
    private String name = "Erbsensuppe";
    public String getName() {
        return name;
    }
    @Override
    public double kostet() {
        return 0.71;
    }
}
@Override sorgt dafür, dass der Compiler prüft, ob die Basisklasse wirklich eine Methode mit gleichem Namen und Parametern hat.

Der Haken an der Lösung oben ist, dass die Erbsensuppe plötzlich billiger wird als die Tomatensuppe, wenn der Preis der Suppe auf 75 Cent steigt.

class Erbsensuppe extends Suppe {
    public String getName() {
        return "Erbsensuppe";
    }
    @Override
    public double kostet() {
        return super.kostet() + 0.01;
    }
}
Soll explizit auf eine Methode, einen Konstruktor oder ein Attribut der Basisklasse zugegriffen werden, wird die Referenz super verwendet.

Speiseplanerstellung: Suppe

Wir erstellen einen Speiseplan zunächst nur für Suppen und deren Preis. Als Basis dient ein Array der Basisklasse Suppe Dieses ist zuweisungskompatibel, da Tomaten-, Gulasch- und Erbsensuppe die Basisklasse Suppe erweitern. So kann jede Referenz einem Array-Element vom Typ Suppe zugewiesen werden.
class Speiseplan {
    public static void main(String[] args) {
        Suppe karte[] = new Suppe[3];
        karte[0] = new Tomatensuppe();
        karte[1] = new Gulaschsuppe();
        karte[2] = new Erbsensuppe();
        for (int i=0; i<karte.length; i++) {
            System.out.println("Kosten: " + karte[i].kostet());
        }
    }
}
Der Start des Programms ergibt: Obwohl also karte[2] eine Referenz auf eine Suppe enthält, "weiß" offenbar das referenzierte Objekt, dass es eigentlich eine Erbsensuppe ist und seine Kosten anders berechnet.

Das referenzierte Objekt behält die Information über seine Klasse auch dann, wenn es über eine Referenz einer Basisklasse zugegriffen wird.

Aber: Der Compiler weiß nicht, dass sich hinter der Suppe eine Erbsensuppe verbirgt. Erst zur Laufzeit wird der Unterschied erkannt. Darum spricht man auch von einer späten Bindung.

Problem mit getName

System.out.print("Name: " + karte[i].getName());  // Compilerfehler!!!
System.out.print("Kosten: " + karte[i].kostet());
Die Methode getName wird nur in den Erweiterungen von Suppe implementiert. Darum kennt Suppe diese nicht und kann sie auch nicht aufrufen, obwohl jede Erweiterung von Suppe die Methode getName() implementiert.

Lösung: Die Basisklasse implementiert die Methode getName() und alle abgeleiteten Klassen überschreiben sie. Die Klasse Suppe erhält also eine Methode getName, die aber nichts weiter tut.

class Suppe {
    private preis = 0.70;
    public double kostet() {
        return preis;
    }
    public String getName() {
        return null;
    }
}
Da die Methode getName in Suppe existiert, kann ein Suppenspeiseplan mit Namen und Preis erstellt werden.

Vollständiger Speiseplan

Der vollständige Speiseplan muss sowohl Suppe als auch Kitt aufnehmen können. Das ist zu lösen, indem eine gemeinsame Basisklasse geschaffen wird, die wir einfach Mahlzeit nennen.

class Mahlzeit {
    public double kostet() {
        return 0;
    }
    public String getName() {
        return null;
    }
}
Damit Suppe zur Mahlzeit wird, muss sie sie erweitern.
class Suppe extends Mahlzeit {
    // ...
}
Nun endlich kann die Mensa der Universität Gintoft ihren wöschentlichen Speiseplan ausgeben und wünscht guten Appetit.
Mahlzeit[] speise = new Mahlzeit[5];
speise[0] = new Tomatensuppe();
speise[1] = new Schnee();
speise[2] = new Gulaschsuppe();
speise[3] = new Purree();
speise[4] = new Erbsensuppe();
for (int i=0; i<speise.length; i++) {
    System.out.print(speise[i].getName());
    System.out.print(": ");
    System.out.println(speise[i].kostet());
}
Die Polymorphie wird in der grafischen Oberfläche Swing genutzt, wenn die eigene paint-Methode aufgerufen wird. Denn Swing ruft die paint-Methode der Basisklasse und dennoch wird die selbstgeschriebene paint ausgeführt.

Abstrakte Methoden und Klassen

Wenn Sie sich die Klasse Suppe noch einmal ansehen, hat sie nur darum eine Methode getName, damit man die Methode getName der Erweiterungen erreicht. Wie die Objekte der Klasse Suppe heißen, wollen wir gar nicht wissen.

Mann könnte null zurückgeben und den Fehler bemerken, wenn irgendjemand den Namen der Suppe erreicht, weil irgendein Dödel vergessen hat, bei der Erweiterung getName zu implementieren.

Eine bessere Lösung ist es, für Suppe eine abstrakte Methode einzurichten. Das bedeutet, dass es die Methode gibt, aber keine Implementierung. Die abstrakte Methode getName besagt also, dass die Klasse Suppe selbst keinen Namen hat, aber eine speziele Suppe immer einen Namen haben muss.

Da die Klasse Suppe die Methode nicht implementiert, kann man auch kein Objekt der Klasse Suppe mehr erzeugen, sondern nur Objekte der Erweiterung von Suppe, die auch getName nun implementieren müssen.

Eine Klasse die mindestens eine abstrakte Methode hat, ist selbst eine abstrakte Klasse. Beides wird mit dem Schlüsselwort abstract deklariert. Eine abstrakte Methode Sie implementiert nicht, sondern deklariert die Schnittstelle.

Java
Java kennzeichnet eine abstrakte Methode mit dem Schlüsselwort abstract. Statt des Methodenrumpfs hat steht hinter der abstrakten Methode ein Semikolon.
abstract class Suppe {
    private double preis = 0.70;
    public double kostet() {
        return preis;
    }
    abstract public String getName();
}
C++
In C++ wird eine abstrakte Methode eine Null zugewiesen. Da abstrakte Methoden nur sinnvoll sind, wenn man sie über die Polymorphie aufrufen kann, benötigt eine abstrakte Methode auch das dafür in C++ verwendete Schlüsselwort virtual.
class Suppe {
private:
    double preis = 0.70;
public:
    double kostet() {
        return preis;
    }
    virtual void std::string getName() = 0;
}

Interfaces

Was bei der Suppe so gut geklappt hat, funktioniert bei der Mahlzeit auch.
abstract class Mahlzeit {
    abstract public double kostet();
    abstract public String getName();
}
Die Klasse Mahlzeit hat nur noch abstrakte Methoden. Das ist auch durchaus passend, eine Mahlzeit weiß weder, wie sie heißt, noch, wie teuer sie ist. Dass eine Mahlzeit abstrakt ist, merkt man spätestens, wenn man im Restaurant eine Mahlzeit bestellt.

Abstrakte Klassen, die nur abstrakte Methoden und keine Attribute enthalten, sind ein Spezialfall, den man in Java Interface nennt.

interface Mahlzeit {
    public double kostet();
    public String getName();
}
Da ein Interface nur abstrakte Methoden enthalten darf, werden diese nicht mehr als abstract gekennzeichnet.

Ein Interface kann nichts (hat keine implementierten Methoden), hat nichts (keine Attribute), aber schreibt allen Erweiterungen vor, was sie zu implementieren haben.

Das Interface Mahlzeit gibt die abstrakten Methoden vor. Die abstrakte Klasse Suppe implementiert Mahlzeit.

abstract class Suppe implements Mahlzeit {
    public double kostet() {
        return 0.70;
    }
}
Erst Tomatensuppe implementiert alle abstrakten Methoden.
class Tomatensuppe extends Suppe {
    public String getName() {
        return "Tomatensuppe";
    }
}
Zusammenfassung:

Wochenspeiseplan

Mahlzeit[] speise = new Mahlzeit[5];
speise[0] = new Tomatensuppe();
speise[1] = new Schnee();
speise[2] = new Gulaschsuppe();
speise[3] = new Purree();
speise[4] = new Erbsensuppe();
for (int i=0; i<speise.length; i++) {
    System.out.print(speise[i].getName());
    System.out.print(": ");
    System.out.println(speise[i].kostet());
}
Interfaces werden genutzt, um Implementierungen austauschbar zu machen.
interface Speicher {
     public void saveKunde(Kunde k);
     public void saveBestellung(Bestellung b);
}
Die Klasse SpeicherInDatenbank implementiert das Speichern in einer Datenbank.
class SpeicherInDatenbank implements Speicher {
    // ...
}
Der Aufrufer gibt nur an einer Stelle an, welche Implementierung verwendet wird.
Speicher speicher = new SpeicherInDatenbank();
    // ...
    speicher.saveKunde(kunde);
In Swing werden die Listener als Interfaces angeboten. Dazu ist keine Implementierung von Swing-Seite erforderlich. Die Methoden actionListener und mouseClicked werden einfach aufgerufen. Dagegen muss die Anwendung in der Regel JFrame bzw. JPanel erweitern. Beide können keine Interfaces sein, da sie allein grafisch sichtbar sind und Standardvorgaben realisieren.

Klassenattribute und -methoden

Ein Attribut gehört zum Objekt. Die Klasse Auto beschreibt beispielsweise das Attribut ps. Jedes Auto hat seine eigene PS-Zahl. Ein R4 hat eine andere PS-Zahl als ein Maserati.
class Auto {
    int ps;
}
Der Zugriff auf das Attribut ps erfolgt über ein Objekt der Klasse Auto:
Auto r4 = new Auto();
r4.ps = 34;
Während ps also eine Objekt-Attribut ist, gibt es auch Klassenattribute. Ein Klassensttribut wird mit dem Schlüsselwort static deklariert und existiert einmalig für alle Objekte der Klasse. Ein Beispiel könnte die Anzahl der Zulassungen sein.
class Auto {
    static int zulassungen = 0;
    int ps;
    public Auto() {
        zulassungen++;
    }
}
Bei jedem Konstruktoraufruf von Auto wird die Anzahl der Zulassungen erhöht. Ihr Wert wird in dem Klassenattribut zulassenungn gespeichert.
Auto r4 = new Auto();
Auto a4 = new Auto();
r4.ps = 34;
System.out.println(r4.ps);  // Zugriff auf das Objekt-Attribut
System.out.println(Auto.zulassungen); // Klassen-Attribut
System.out.println(r4.zulassungen); // Auch per Objekt möglich!
System.out.println(a4.zulassungen); // dasselbe wie bei r4
System.out.println(Auto.ps); // Das knallt und gibt Compilerfehler
Ein mit static gekennzeichnet Attribut existiert genau ein Mal pro Klasse. Dabei ist die Existenz eines Objekts dieser Klasse nicht erforderlich.

Das statische Attribut wird typischerweise über den Klassennamen referenziert, kann aber auch über jedes Objekt der Klasse angesprochen werden.

Klassenmethoden

So wie es Klassenattribute gibt, gibt es auch Klassenmethoden. Auch diese existieren genau einmal für jede Klasse und auch dann, wenn kein Objekt der Klasse existiert. Auch sie werden mit static deklariert.

Die bekannteste Klassenmethode ist sicherlich main, die wir schon die ganze Zeit als Startpunkt für unsere Programme verwenden. Da zum Startzeitpunkt des Programms noch kein Objekt existiert, muss die Methode ohne Objekt auskommen und darum ist sie als static deklariert.

public static void main(String[] args) {

Aufgrund ihrer Eigenschaft, dass sie auch ohne Objekt existieren kann, kann eine Klassenmethode nicht auf Objektattribute zugreifen oder Objektmethoden aufrufen, sondern nur auf Klassenattribute oder Klassenmethoden.

Static erzwingt static

Beispiel für Klassenmethoden

Die Klasse Math stellt Methoden zur Verfügung, braucht keine Attribute. Math} enthält mathematische Funktionen, die kein Objekt benötigen.
minimum = Math.min(a, b);

Klassenkonstanten

Konstanten (also Variablen mit dem Modifizierer final werden typischerweise als Klassenvariablen deklariert, also auch mit einem static versehen.
public static final int MINEN = 12;
Der Zugriff kann so direkt über den Klassennamen erfolgen:
private Minen[] minen = new Minen[Spiel.MINEN];

Klassenkonstruktor

Der Vollständigkeit halber: Es gibt auch einen Klassenkonstruktor. Er kommt in der Praxis selten vor. Er entsteht durch die Kennzeichnung eines Blocks durch static, kein Typ, kein Name, gefolgt von einem Block.
class MeineKlasse {
   static {
        // ...
   }
   // ...
}
Ein static-Element wird über den Klassennamen aufgerufen. Da Klassennamen mit einem Großbuchstaben beginnen und Objekte mit Kleinbuchstaben, kann am Aufruf erkannt werden, ob das Element static ist.

Singleton

Soll ein Element exakt einmal vorkommen, gibt es neben static die Möglichkeit, ein Singleton zu verwenden. Ein Singleton ist eine Klasse, von der genau ein Objekt angelegt werden kann. Damit das gewährleistet ist, muss Folgendes umgesetzt werden: Originellerweise wollen wir die Singleton-Klasse Singleton nennen. Natürlich geht jeder andere Name auch.
class Singleton {
    private Singleton() { }
    private static Singleton instance = null;
    static public  Singleton getInstance() {
        if (instance==null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Verwendung eines Singleton

Singleton obj = Singleton.getInstance();
Beispiel eines Singleton: Die Zulassung wird in einem Singleton realisiert.
class Zulassung {
    private Zulassung() { }
    private static Zulassung instance = null;
    static public  Zulassung getInstance() {
        if (instance==null) {
            instance = new Zulassung();
        }
        return instance;
    }
    private int zulassungen = 0;
    public void zulassen() {
        zulassungen++;
    }
}
Das Auto lässt sich nun etwas anders zu:
class Auto {
    int ps;
    public Auto() {
        Zulassung zulassung = Zulassung.getInstance()
        zulassung.zulassen();
    }
}
Oder auch kürzer:
class Auto {
    int ps;
    public Auto() {
        Zulassung.getInstance().zulassen();
    }
}
Natürlich ist es kompakter ein einzelnes statisches Klassenattribut zu verwenden. Wenn allerdings das Einzelobjekt komplexer wird oder von mehreren Klassen zugegriffen wird, ist ein Singleton das Mittel der Wahl.

Die Klasse Object

Die Klasse Object ist die Klasse, von der alle Klassen abgeleitet sind, ob sie es wollen oder nicht. Object ist die Basisklasse aller Klassen.

Daraus folgt, dass alle Methoden, die in Object definiert sind, über jedes beliebige Objekt jeder beliebigen Klasse aufgerufen werden können. Das sind u. a. equals, clone, toString, wait und notify.

Man kann in der Referenz auf Object die Referenz auf jedes Objekt speichern, denn: Jede Klasse IST EIN Object.

Ausnahme sind die primitiven Typen. Das ist manchmal hinderlich.

Wrapper-Klassen für primitive Typen

Manchmal wären auch primitve Typen gern Klasse. Dann könnten sie auch eine Referenz haben. Sie würden auch Nachfahren von Object werden.

Dazu gibt es Wrapper-Klassen. Java wickelt die primitiven Typen in Klassen ein, die extra für sie geschaffen sind.

Autoboxing

Die primitiven Typen und ihre Wrapper-Klassen sind zuweisungskompatibel. Dieses Verhalten nennt man Autoboxing.
int a = 5;
Integer b = a*2;
a = b*2;
Die Wrapper-Klassen liefern einige Klassenattribute und -methoden (static):
Integer.MAX_VALUE ; // Klassenkonstante: 2147483647
Integer.MIN_VALUE ; // Klassenkonstante: -2147483648
int n=Integer.parseInt(txt1.getText()); // String -> Integer

Noch einmal Object

Da es Wrapper gibt, können Arrays der Klasse Object angelegt werden, in der jeder beliebiger Wert abgelegt werden kann. Das Problem ist nur: Man kann eine Kuh einfüllen, aber einen Hund herausholen.

public static void main(String[] args) {
    Object[] tiere = new Object[5];
    for (int i=0; i<5; i++) {
        tiere[i] = new Kuh();
    }
    Kuh kuh = (Kuh)(tiere[0]); // hier ist Casting erforderlich
    Hund hund = (Hund)(tiere[1]); // Das gibt Ärger!
    hund.machMaennchen(); // Das macht keine Kuh mit
}
Schwierig wird es auch, wenn man den Hund melken will.

Merke: Casting ist nicht nur im Fernsehen gruselig.

Generics

Problemstellung: Wir erstellen eine Klasse, die ein Paar gleicher Objekte speichern soll.
class Paar {
    EinTyp wert1, wert2;
    public Paar(EinTyp a1, EinTyp a2) {
        wert1 = a1; wert2 = a2;
    }
}
Wir können statt EinTyp, Integer, String oder ein beliebiges Objekt einsetzen. Wir müssen allerdings alle noch einmal von Hand schreiben. Das ist doof. Lösung: Wir verwenden die Klasse \befehl{Object}. Damit gilt die Klasse für beliebige Paare.
class ObjectPaar {
    Object obj1, obj2;
    public ObjectPaar(Object o1, Object o2) {
        obj1 = o1; obj2 = o2;
    }
}
Problem:
ObjectPaar op = new ObjectPaar("1", 2); // erlaubt
Die Klasse sollte "typisierbar" sein. Genau das macht Generics Es wird für die Klasse in spitzen Klammern eine Typvariable angegeben, die bei der Instanziierung definiert wird. Die Klasse Paar wird für die Typvariable T definiert:
class Paar {
    T obj1, obj2;
    public Paar(T o1, T o2) {
        obj1 = o1; obj2 = o2;
    }
}
Bei der Instanziierung der Klasse \ident{Paar} wird der Typ angegeben:
Paar ip = new Paar(1, 2); // erlaubt
Paar sp = new Paar("1", "2"); // erlaubt
Paar fp = new Paar("1", 2); // nicht erlaubt
An dieser Stelle wurde in der Vorlesung auf das Java Collection Framework vorgeführt. Hier werden Generics ausgiebig verwendet. Daneben ist es auch interessant, die Interfaces Collection und List näher zu betrachten und die Implementierung von List (ArrayList und LinkedList) zu vergleichen.