Objektorientierte Programmierung mit Java
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

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 wird, wird bei Koordinaten der Winkel und die Entfernung zueinander bestimmt.

Klassendefinition

Die Klasse Bruch enthält einen Zähler und einen Nenner:
class Bruch {
   int zaehler, nenner;
}
Jede Klasse in Java muss in einer eigenen Datei stehen, die den Namen der Klasse trägt. Die Klasse Bruch steht also in einer Datei namens Bruch.java.

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();

Vererbung, Ableitung, Erweiterung

Alle drei Begriffe stehen für den gleichen Inhalt.

Modellieren wir ein Tier!

class Tier {
    int anzahlExtremitaeten, umfangBauch, umfangKopf;
    String name;
}
Auch wenn wir Menschen es als kränkend empfinden, ist es für den Philosophen nicht einfach, den Unterschied zwischen Mensch und Tier zu definieren. Die Informatiker haben es da einfacher: Ein Mensch ist ein Tier mit einem Bankkonto.
class Mensch extends Tier {
     Konto bankkonto;
}
Mensch erbt alle Attribute und Methoden von Tier. Darum werden in der Klasse Mensch nur die Attribute und Methoden aufgeführt, zu den Menschen vom Tier unterscheiden.

Häufig kommt es in der Praxis vor, dass man Personen verwaltet.

Es entspannt sich eine baumartige Abhängigkeit von Vererbungen.

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 EIN Mensch)
  • 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:

Als Beispiel wurden JFrame und JPanel verwendet. Für das Überschreiben wurde die Methode paint überschrieben, um eine eigene Grafik zu erstellen.

Namensraum durch 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();
Ü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();

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.

Polymorphie

Polymorphie bedeutet, dass das Objekt weiß, welchen Typs es ist und die richtige Methode verwendet.

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. Da 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.

abstract class Suppe {
    private double preis = 0.70;
    public double kostet() {
        return preis;
    }
    abstract public String getName();
}
Eine abstrakte Methode erhält das Schlüsselwort abstract. Sie implementiert nicht, sondern deklariert die Schnittstelle. Statt des Methodenrumpfs hat die abstrakte Methode ein Semikolon.

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.