Subsections of Generics: Umgang mit parametrisierten Typen

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

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

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

Bounds & Wildcards

TL;DR

Typ-Variablen können weiter eingeschränkt werden, in dem man einen verpflichtenden Ober- oder Untertyp angibt mit extends bzw. super. Damit muss der später bei der Instantiierung verwendete Typ-Parameter entweder die Oberklasse selbst sein oder davon ableiten (bei extends) bzw. der Typ-Parameter muss eine Oberklasse der angegebenen Schranke sein (super).

Durch die Einschränkung mit extends können in der Klasse/Methode auf der Typ-Variablen alle Methoden des angegebenen Obertyps verwendet werden.

Ein Wildcard (?) als Typ-Parameter steht für einen beliebigen Typ, wobei die Typ-Variable keinen Namen bekommt und damit innerhalb der Klasse/Methode nicht zugreifbar ist.

Videos (HSBI-Medienportal)
Lernziele
  • (K3) Umgang mit Wildcards und Bounds bei generischen Klassen/Methoden

Bounds: Einschränken der generischen Typen

public class Cps<E extends Number> {
    // Obere Schranke: E muss Number oder Subklasse sein
    // => Zugriff auf Methoden aus Number moeglich
}
Cps<Double> a;
Cps<Number> b;
Cps<String> c;  // Fehler!!!
  • Schlüsselwort extends gilt hier auch für Interfaces

  • Mehrere Interfaces: nach extends Klasse oder Interface, danach mit "&" getrennt die restlichen Interfaces:

    class Cps<E extends KlasseOderInterface & I1 & I2 & I3> {}

Anmerkung: Der Typ-Parameter ist analog auch mit super (nach unten) einschränkbar

Wildcards: Dieser Typ ist mir nicht so wichtig

Wildcard mit "?" => steht für unbestimmten Typ

public class Wuppie {
    public void m1(List<?> a) { ... }
    public void m2(List<? extends Number> b) { ... }
}
  • m1: List beliebig parametrisierbar => In m1 für Objekte in Liste a nur Methoden von Object nutzbar!

  • m2: List muss mit Number oder Subklasse parametrisiert werden. => Dadurch für Objekte in Liste b alle Methoden von Number nutzbar ...

Weitere Eigenschaften:

  • Durch Wildcard kein Zugriff auf den Typ
  • Wildcard kann durch upper bound eingeschränkt werden
  • Geht nicht bei Klassen-/Interface-Definitionen

[Bloch2018]: Nur für Parameter und nicht für Rückgabewerte nutzen!

Hands-On: Ausgabe für generische Listen

Ausgabe für Listen gesucht, die sowohl Elemente der Klasse A als auch Elemente der Klasse B enthalten können

class A { void printInfo() { System.out.println("A"); } }
class B extends A { void printInfo() { System.out.println("B"); } }

public class X {
    public static void main(String[] args) {
        List<A> x = new ArrayList<A>();
        x.add(new A());  x.add(new B());
        printInfo(x);    // Klassenmethode in X, gesucht
        List<B> y = new ArrayList<B>();
        y.add(new B());  y.add(new B());
        printInfo(y);    // Klassenmethode in X, gesucht
    }
}

Hinweis: Dieses Beispiel beinhaltet auch Polymorphie bei/mit generischen Datentypen, bitte vorher auch das Video zum vierten Teil "Generics und Polymorphie" anschauen

Erster Versuch (A und B und main() wie oben)

public class X {
    public static void printInfo(List<A> list) {
        for (A a : list) { a.printInfo(); }
    }
}

=> So gehts nicht! Eine List<B> ist keine List<A> (auch wenn ein B ein A ist, vgl. spätere Sitzung zu Generics und Vererbung ...)!

Zweiter Versuch mit Wildcards (A und B und main() wie oben)

public class X {
    public static void printInfo(List<?> list) {
        for (Object a : list) { a.printInfo(); }
    }
}

=> So gehts auch nicht! Im Prinzip passt das jetzt für List<A> und List<B>. Dummerweise hat man durch das Wildcard keinen Zugriff mehr auf den Typ-Parameter und muss für den Typ der Laufvariablen in der for-Schleife dann Object nehmen. Aber Object kennt unser printInfo nicht ... Außerdem könnte man die Methode X#printInfo dank des Wildcards auch mit allen anderen Typen aufrufen ...

Dritter Versuch (Lösung) mit Wildcards und Bounds (A und B und main() wie oben)

public class X {
    public static void printInfo(List<? extends A> list) {
        for (A a : list) { a.printInfo(); }
    }
}

Das ist die Lösung. Man erlaubt als Argument nur List-Objekte und fordert, dass sie mit A oder einer Unterklasse von A parametrisiert sind. D.h. in der Schleife kann man sich auf den gemeinsamen Obertyp A abstützen und hat dann auch wieder die printInfo-Methode zur Verfügung ...

Wrap-Up

  • Ein Wildcard (?) als Typ-Parameter steht für einen beliebigen Typ

    • Ist in Klasse oder Methode dann aber nicht mehr zugreifbar
  • Mit Bounds kann man Typ-Parameter nach oben oder nach unten einschränken (im Sinne einer Vererbungshierarchie)

    • extends: Der Typ-Parameter muss eine Unterklasse eines bestimmten Typen sein
    • super: Der Typ-Parameter muss eine Oberklasse eines bestimmten Typen sein
Challenges

Spieler, Mannschaften und Ligen Modellieren Sie in Java verschiedene Spielertypen sowie generische Mannschaften und Ligen, die jeweils bestimmte Spieler (-typen) bzw. Mannschaften aufnehmen können.

  1. Implementieren Sie die Klasse Spieler, die das Interface ISpieler erfüllt.

    public interface ISpieler {
        String getName();
    }
  2. Implementieren Sie die beiden Klassen FussballSpieler und BasketballSpieler und sorgen Sie dafür, dass beide Klassen vom Compiler als Spieler betrachtet werden (geeignete Vererbungshierarchie).

  3. Betrachten Sie das nicht-generische Interface IMannschaft. Erstellen Sie daraus ein generisches Interface IMannschaft mit einer Typ-Variablen. Stellen Sie durch geeignete Beschränkung der Typ-Variablen sicher, dass nur Mannschaften mit von ISpieler abgeleiteten Spielern gebildet werden können.

    public interface IMannschaft {
        boolean aufnehmen(ISpieler spieler);
        boolean rauswerfen(ISpieler spieler);
    }
  4. Betrachten Sie das nicht-generische Interface ILiga. Erstellen Sie daraus ein generisches Interface ILiga mit einer Typvariablen. Stellen Sie durch geeignete Beschränkung der Typvariablen sicher, dass nur Ligen mit von IMannschaft abgeleiteten Mannschaften angelegt werden können.

    public interface ILiga {
        boolean aufnehmen(IMannschaft mannschaft);
        boolean rauswerfen(IMannschaft mannschaft);
    }
  5. Leiten Sie von ILiga das generische Interface IBundesLiga ab. Stellen Sie durch geeignete Formulierung der Typvariablen sicher, dass nur Ligen mit Mannschaften angelegt werden können, deren Spieler vom Typ FussballSpieler (oder abgeleitet) sind.

    Realisieren Sie nun noch die Funktionalität von IBundesLiga als nicht-generisches Interface IBundesLiga2.

Quellen

Type Erasure

TL;DR

Generics existieren eigentlich nur auf Quellcode-Ebene. Nach der Typ-Prüfung etc. entfernt der Compiler alle generischen Typ-Parameter und alle <...> (=> "Type-Erasure"), d.h. im Byte-Code stehen nur noch Raw-Typen bzw. die oberen Typ-Schranken der Typ-Parameter, in der Regel Object. Zusätzlich baut der Compiler die nötigen Casts ein. Als Anwender merkt man davon nichts, muss das "Type-Erasure" wegen der Auswirkungen aber auf dem Radar haben!

Videos (YouTube)
Videos (HSBI-Medienportal)
Lernziele
  • (K2) Typ-Löschung und Auswirkungen

Typ-Löschung (Type-Erasure)

Der Compiler ersetzt nach Prüfung der Typen und ihrer Verwendung alle Typ-Parameter durch

  1. deren obere (Typ-)Schranke und
  2. passende explizite Cast-Operationen (im Byte-Code).

Die obere Typ-Schranke ist in der Regel der Typ der ersten Bounds-Klausel oder Object, wenn keine Einschränkungen formuliert sind.

Bei parametrisierten Typen wie List<T> wird der Typ-Parameter entfernt, es entsteht ein sogenannter Raw-Typ (List, quasi implizit mit Object parametrisiert).

=> Ergebnis: Nur eine (untypisierte) Klasse! Zur Laufzeit gibt es keine Generics mehr!

Hinweis: In C++ ist man den anderen möglichen Weg gegangen und erzeugt für jede Instantiierung die passende Klasse. Siehe Modul "Systemprogrammierung" :)

Beispiel: Aus dem folgenden harmlosen Code-Fragment:

class Studi<T> {
    T myst(T m, T n) { return n; }

    public static void main(String[] args) {
        Studi<Integer> a = new Studi<>();
        int i = a.myst(1, 3);
    }
}

wird nach der Typ-Löschung durch Compiler (das steht dann quasi im Byte-Code):

class Studi {
    Object myst(Object m, Object n) { return n; }

    public static void main(String[] args) {
        Studi a = new Studi();
        int i = (Integer) a.myst(1, 3);
    }
}

Die obere Schranke meist Object => new T() verboten/sinnfrei (s.u.)!

Type-Erasure bei Nutzung von Bounds

vor der Typ-Löschung durch Compiler:

class Cps<T extends Number> {
    T myst(T m, T n) {
        return n;
    }

    public static void main(String[] args) {
        Cps<Integer> a = new Cps<>();
        int i = a.myst(1, 3);
    }
}

nach der Typ-Löschung durch Compiler:

class Cps {
    Number myst(Number m, Number n) {
        return n;
    }

    public static void main(String[] args) {
        Cps a = new Cps();
        int i = (Integer) a.myst(1, 3);
    }
}

Raw-Types: Ich mag meine Generics "well done" :-)

Raw-Types: Instanziierung ohne Typ-Parameter => Object

Stack s = new Stack(); // Stack von Object-Objekten
  • Wegen Abwärtskompatibilität zu früheren Java-Versionen noch erlaubt.
  • Nutzung wird nicht empfohlen! (Warum?)

Anmerkung

Raw-Types darf man zwar selbst im Quellcode verwenden (so wie im Beispiel hier), sollte die Verwendung aber vermeiden wegen der Typ-Unsicherheit: Der Compiler sieht im Beispiel nur noch einen Stack für Object, d.h. dort dürfen Objekte aller Typen abgelegt werden - es kann keine Typprüfung durch den Compiler stattfinden. Auf einem Stack<String> kann der Compiler prüfen, ob dort wirklich nur String-Objekte abgelegt werden und ggf. entsprechend Fehler melden.

Etwas anderes ist es, dass der Compiler im Zuge von Type-Erasure selbst Raw-Types in den Byte-Code schreibt. Da hat er vorher bereits die Typsicherheit geprüft und er baut auch die passenden Casts ein.

Das Thema ist eigentlich nur noch aus Kompatibilität zu Java5 oder früher da, weil es dort noch keine Generics gab (wurden erst mit Java6 eingeführt).

Folgen der Typ-Löschung: new

new mit parametrisierten Klassen ist nicht erlaubt!

class Fach<T> {
    public T foo() {
        return new T();  // nicht erlaubt!!!
    }
}

Grund: Zur Laufzeit keine Klasseninformationen über T mehr

Im Code steht return (CAST) new Object();. Das neue Object kann man anlegen, aber ein Cast nach irgendeinem anderen Typ ist sinnfrei: Jede Klasse ist ein Untertyp von Object, aber eben nicht andersherum. Außerdem fehlt dem Objekt vom Typ Object auch sämtliche Information und Verhalten, die der Cast-Typ eigentlich mitbringt ...

Folgen der Typ-Löschung: static

static mit generischen Typen ist nicht erlaubt!

class Fach<T> {
    static T t;                    // nicht erlaubt!!!
    static Fach<T> c;              // nicht erlaubt!!!
    static void foo(T t) { ... };  // nicht erlaubt!!!
}

Fach<String>  a;
Fach<Integer> b;

Grund: Compiler generiert nur eine Klasse! Beide Objekte würden sich die statischen Attribute teilen (Typ zur Laufzeit unklar!).

Hinweis: Generische (statische) Methoden sind erlaubt.

Folgen der Typ-Löschung: instanceof

instanceof mit parametrisierten Klassen ist nicht erlaubt!

class Fach<T> {
    void printType(Fach<?> p) {
        if (p instanceof Fach<Number>)
            ...
        else if (p instanceof Fach<String>)
            ...
    }
}

Grund: Unsinniger Code nach Typ-Löschung:

class Fach {
void printType(Fach p) {
    if (p instanceof Fach)
        ...
    else if (p instanceof Fach)
        ...
    }
}

Folgen der Typ-Löschung: .class

.class mit parametrisierten Klassen ist nicht erlaubt!

boolean x;
List<String>  a = new ArrayList<String>();
List<Integer> b = new ArrayList<Integer>();

x = (List<String>.class == List<Integer>.class);  // Compiler-Fehler
x = (a.getClass() == b.getClass());               // true

Grund: Es gibt nur List.class (und kein List<String>.class bzw. List<Integer>.class)!

Wrap-Up

  • Generics existieren eigentlich nur auf Quellcode-Ebene
  • "Type-Erasure":
    • Compiler entfernt nach Typ-Prüfungen etc. generische Typ-Parameter etc. => im Byte-Code nur noch Raw-Typen bzw. die oberen Typ-Schranken der Typ-Parameter, in der Regel Object
    • Compiler baut passende Casts in Byte-Code ein
    • Transparent für User; Auswirkungen beachten!
Quellen

Generics und Polymorphie

TL;DR

Auch mit generischen Klassen stehen die Mechanismen Vererbung und Überladen zur Verfügung. Dabei muss aber beachtet werden, dass generische Klassen sich "invariant" verhalten: Der Typ selbst folgt der Vererbungsbeziehung, eine Vererbung des Typ-Parameters begründet keine Vererbungsbeziehung! D.h. aus U extends O folgt nicht A<U> extends A<O>.

Bei Arrays ist es genau anders herum: Wenn U extends O dann gilt auch U[] extends O[] ... (Dies nennt man "kovariantes" Verhalten.)

Videos (YouTube)
Videos (HSBI-Medienportal)
Lernziele
  • (K3) Vererbungsbeziehungen mit generischen Klassen
  • (K3) Umgang mit Arrays und generischen Typen

Generische Polymorphie

B<E> extends A<E>

class A<E> { ... }
class B<E> extends A<E> { ... }

A<Double> ad = new B<Double>();
A<String> as = new B<String>();
class Vector<E> { ... }
class Stack<E> extends Vector<E> { ... }

Vector<Double> vd = new Stack<Double>();
Vector<String> vs = new Stack<String>();

=> Polymorphie bei Generics bezieht sich auf Typ (nicht Typ-Parameter)

Invarianz: Generics sind invariant, d.h. ein HashSet<String> ist ein Untertyp von Set<String>. Bei der Vererbung muss der Typ-Parameter identisch sein.

Polymorphie bei Generics bezieht sich nur auf Typ!

"B extends A" bedeutet nicht "C<B> extends C<A>"

Stack<Number> s = new Stack<Integer>(); // DAS GEHT SO NICHT!

// Folgen (wenn obiges gehen wuerde):
s.push(new Integer(3)); // das ginge sowieso ...

// Folgen (wenn obiges gehen wuerde):
// Stack<Number> waere Oberklasse auch von Stack<Double>
s.push(new Double(2.0)); // waere dann auch erlaubt ...

// Das Objekt (Stack<Integer>) kann aber keine Double speichern!
// Zur Laufzeit keine Typ-Informationen mehr!
  • Typ-Löschung => zur Laufzeit keine Typinformationen vorhanden
  • Compiler muss Typen prüfen (können)!

Abgrenzung: Polymorphie bei Arrays

Wenn "B extends A" dann "B[] extends A[]"

Object[] x = new String[] {"Hello", "World", ":-)"};
x[0] = "Hallo";
x[0] = new Double(2.0);  // Laufzeitfehler
String[] y = x;  // String[] ist KEIN Object[]!!!
  • Arrays besitzen Typinformationen über gespeicherte Elemente
  • Prüfung auf Typ-Kompatibilität zur Laufzeit (nicht Kompilierzeit!)

Arrays gab es sehr früh, Generics erst relativ spät (ab Java6) => bei Arrays fand man das Verhalten natürlich und pragmatisch (trotz der Laufzeit-Überprüfung).

Bei der Einführung von Generics musste man Kompatibilität sicherstellen (alter Code soll auch mit neuen Compilern übersetzt werden können - obwohl im alten Code Raw-Types verwendet werden). Außerdem wollte man von Laufzeit-Prüfung hin zu Compiler-Prüfung. Da würde das von Arrays bekannte Verhalten Probleme machen ...

Kovarianz: Arrays sind kovariant, d.h. ein Array vom Typ String[] ist wegen String extends Object ein Untertyp von Object[].

Arrays vs. parametrisierte Klassen

=> Keine Arrays mit parametrisierten Klassen!

Foo<String>[] x = new Foo<String>[2];   // Compilerfehler

Foo<String[]> y = new Foo<String[]>();  // OK :)

Arrays mit parametrisierten Klassen sind nicht erlaubt! Arrays brauchen zur Laufzeit Typinformationen, die aber durch die Typ-Löschung entfernt werden.

Diskussion Vererbung vs. Generics

Vererbung:

  • IS-A-Beziehung
  • Anwendung: Vererbungsbeziehung vorliegend, Eigenschaften verfeinern
  • Beispiel: Ein Student ist eine Person

Generics:

  • Schablone (Template) für viele Datentypen
  • Anwendung: Identischer Code für unterschiedliche Typen
  • Beispiel: Datenstrukturen, Algorithmen generisch realisieren

Wrap-Up

  • Generics: Vererbung und Überladen möglich, aber: Aus "U extends O" folgt nicht "A<U> extends A<O>"

  • Achtung: Bei Arrays gilt aber: Wenn "U extends O" dann gilt auch "U[] extends O[]" ...

Quellen