Generische Klassen & Methoden

TL;DR

Generische Klassen und Methoden sind ein wichtiger Baustein in der Programmierung mit Java. Dabei werden Typ-Variablen eingeführt, die dann bei der Instantiierung der generischen Klassen oder beim Aufruf von generischen Methoden mit existierenden Typen konkretisiert werden ("Typ-Parameter").

Syntaktisch definiert man die Typ-Variablen in spitzen Klammern hinter dem Klassennamen bzw. vor dem Rückgabetyp einer Methode: public class Stack<E> { } und public <T> T foo(T m) { }.

Videos (HSBI-Medienportal)
Lernziele
  • (K1) Begriffe generischer Typ, parametrisierter Typ, formaler Typ-Parameter, Typ-Parameter
  • (K3) Erstellen und Nutzen von generischen Klassen und Interfaces
  • (K3) Erstellen und Nutzen von generischen Methoden

Generische Strukturen

Vector speicher = new Vector();
speicher.add(1); speicher.add(2); speicher.add(3);
speicher.add("huhu");

int summe = 0;
for (Object i : speicher) { summe += (Integer)i; }

Problem: Nutzung des "raw" Typs Vector ist nicht typsicher!

  • Mögliche Fehler fallen erst zur Laufzeit und u.U. erst sehr spät auf: Offenbar werden im obigen Beispiel int-Werte erwartet, d.h. das Hinzufügen von "huhu" ist vermutlich ein Versehen (wird vom Compiler aber nicht bemerkt)
  • Die Iteration über speicher kann nur allgemein als Object erfolgen, d.h. in der Schleife muss auf den vermuteten/gewünschten Typ gecastet werden: Hier würde dann der String "huhu" Probleme zur Laufzeit machen
Vector<Integer> speicher = new Vector<Integer>();
speicher.add(1); speicher.add(2); speicher.add(3);
speicher.add("huhu");

int summe = 0;
for (Integer i : speicher) { summe += i; }

Vorteile beim Einsatz von Generics:

  • Datenstrukturen/Algorithmen nur einmal implementieren, aber für unterschiedliche Typen nutzen
  • Keine Vererbungshierarchie nötig
  • Nutzung ist typsicher, Casting unnötig
  • Geht nur für Referenztypen
  • Beispiel: Collections-API

Generische Klassen/Interfaces definieren

  • Definition: "<Typ>" hinter Klassennamen

    public class Stack<E> {
        public E push(E item) {
            addElement(item);
            return item;
        }
    }
    • Stack<E> => Generische (parametrisierte) Klasse (auch: "generischer Typ")
    • E => Formaler Typ-Parameter (auch: "Typ-Variable")
  • Einsatz:

    Stack<Integer> stack = new Stack<Integer>();
    • Integer => Typ-Parameter
    • Stack<Integer> => Parametrisierter Typ

Generische Klassen instantiieren

  • Typ-Parameter in spitzen Klammern hinter Klasse bzw. Interface

    ArrayList<Integer> il = new ArrayList<Integer>();
    ArrayList<Double>  dl = new ArrayList<Double>();

Beispiel I: Einfache generische Klassen

class Tutor<T> {
    // T kann in Tutor *fast* wie Klassenname verwendet werden
    private T x;
    public T foo(T t) { ... }
}
Tutor<String>  a = new Tutor<String>();
Tutor<Integer> b = new Tutor<>();  // ab Java7: "Diamond Operator"

a.foo("wuppie");
b.foo(1);
b.foo("huhu");  // Fehlermeldung vom Compiler
Beispiel: classes.GenericClasses

Typ-Inferenz

Typ-Parameter kann bei new() auf der rechten Seite oft weggelassen werden => Typ-Inferenz

Tutor<String> x = new Tutor<>();  // <>: "Diamantoperator"

(gilt seit Java 1.7)

Beispiel II: Vererbung mit Typparametern

interface Fach<T1, T2> {
    public void machWas(T1 a, T2 b);
}

class SHK<T> extends Tutor<T> { ... }

class PM<X, Y, Z> implements Fach<X, Z> {
    public void machWas(X a, Z b) { ... }
    public Y getBla() { ... }
}

class Studi<A,B> extends Person { ... }
class Properties extends Hashtable<Object,Object> { ... }

Auch Interfaces und abstrakte Klassen können parametrisierbar sein.

Bei der Vererbung sind alle Varianten bzgl. der Typ-Variablen denkbar. Zu beachten ist dabei vor allem, dass die Typ-Variablen der Oberklasse (gilt analog für Interfaces) entweder durch Typ-Variablen der Unterklasse oder durch konkrete Typen spezifiziert sind. Die Typ-Variablen der Oberklasse dürfen nicht "in der Luft hängen" (siehe auch nächste Folie)!

Beispiel III: Überschreiben/Überladen von Methoden

class Mensch { ... }

class Studi<T extends Mensch> {
    public void f(T t) { ... }
}

class Prof<T> extends Mensch { ... }

class Tutor extends Studi<Mensch> {
    public void f(Mensch t) { ... }      // Ueberschreiben
    public void f(Tutor t) { ... }       // Ueberladen
}

Vorsicht: So geht es nicht!

class Foo<T> extends T { ... }

class Fluppie<T> extends Wuppie<S> { ... }
  • Generische Klasse Foo<T> kann nicht selbst vom Typ-Parameter T ableiten (warum?)
  • Bei Ableiten von generischer Klasse Wuppie<S> muss deren Typ-Parameter S bestimmt sein: etwa durch den Typ-Parameter der ableitenden Klasse, beispielsweise Fluppie<S> (statt Fluppie<T>)

Generische Methoden definieren

  • "<Typ>" vor Rückgabetyp

    public class Mensch {
        public <T> T myst(T m, T n) {
            return Math.random() > 0.5 ? m : n;
        }
    }
  • "Mischen possible":

    public class Mensch<E> {
        public <T> T myst(T m, T n) { ... }
        public String myst(String m, String n) { ... }
    }

Aufruf generischer Methoden

Aufruf

  • Aufruf mit Typ-Parameter vor Methodennamen, oder
  • Inferenz durch Compiler

Finden der richtigen Methode durch den Compiler

  1. Zuerst Suche nach exakt passender Methode,
  2. danach passend mit Konvertierungen => Compiler sucht gemeinsame Oberklasse in Typhierarchie

Beispiel

class Mensch {
    <T> T myst(T m, T n) { ... }
}
Mensch m = new Mensch();


m.<String>myst("Essen", "lecker");  // Angabe Typ-Parameter


m.myst("Essen", 1);          // String, Integer => T: Object
m.myst("Essen", "lecker");   // String, String  => T: String
m.myst(1.0, 1);              // Double, Integer => T: Number
Beispiel methods.GenericMethods

Reihenfolge der Suche nach passender Methode gilt auch für nicht-generisch überladene Methoden

class Mensch {
    public <T> T myst(T m, T n) {
        System.out.println("X#myst: T");
        return m;
    }

    // NICHT gleichzeitig erlaubt wg. Typ-Löschung (s.u.):
/*
    public <T1, T2> T1 myst(T1 m, T2 n) {
        System.out.println("X#myst: T");
        return m;
    }
*/

    public String myst(String m, String n) {
        System.out.println("X#myst: String");
        return m;
    }

    public int myst(int m, int n) {
        System.out.println("X#myst: int");
        return m;
    }
}


public class GenericMethods {
    public static void main(String[] args) {
        Mensch m = new Mensch();

        m.myst("Hello World", "m");
        m.myst("Hello World", 1);
        m.myst(3, 4);
        m.myst(m, m);
        m.<Mensch>myst(m, m);
        m.myst(m, 1);
        m.myst(3.0, 4);
        m.<Double>myst(3, 4);
    }
}

Wrap-Up

  • Begriffe:

    • Generischer Typ: Stack<T>
    • Formaler Typ-Parameter: T
    • Parametrisierter Typ:Stack<Long>
    • Typ-Parameter: Long
    • Raw Type: Stack
  • Generische Klassen: public class Stack<E> { }

    • "<Typ>" hinter Klassennamen
  • Generische Methoden: public <T> T foo(T m) { }

    • "<Typ>" vor Rückgabewert
Quellen