Antwort auf: Polymorphie mit Gerenics

EIDI-Crashkurs 2020 Polymorphie mit Gerenics Antwort auf: Polymorphie mit Gerenics

#3525

Beispiel (kompiliert nicht):

class A<T> {
    T t;
    void f(T x) { x.m(); }
}
class X { void m() {} }
class Y extends X {}

Beispielobjekte:

X g = new X(); Y h = new Y();

 

Was gibt es zu beachten?

  1. Eine generische Klasse gibt es nicht ohne eingesetzten Typ. Es gibt z. B. Objekte der Klassen A<X> und A<Y> und A<String>, es gibt aber keine Objekte der Klasse A, weil es die “allgemeine” Klasse A nicht gibt. Auch A<T> gibt es nicht, weil T keine Klasse ist. Der generische Typ T könnte zusätzlich eingeschränkt sein, z. B. class A<T extends X>, dann wäre A<String> nicht mehr möglich, weil für T dann mindestens X (oder spezieller, also Y geht auch) eingesetzt werdem muss.
     
  2. Der generische Typ gibt nicht die Vererbungshierarchie vor. Während die Zuweisung von einem Y an eine Variable vom Typ X möglich wäre (d. h. X var = new Y(); geht), da Y spezieller ist als X, gilt das nicht für die Klasse A<...>. Weder die Zuweisung von A<Y> an A<X> noch umgekehrt wäre möglich (d. h. A<X> var = new A<Y>(); ist ein Compiler-Fehler), weil die Klassen A<Y> und A<X> überhaupt nichts miteinander zu tun haben.
     
  3. Generische Typen sind nur Platzhalter. Angenommen es existiert eine Variable A<Y> m, dann gibt es dafür die Methode void f(Y x) (siehe Beispiel oben). Der Aufruf m.f(h) wäre möglich, m.f(g) wäre nicht möglich. Für eine Variable A<X> n wären beide Aufrufe möglich. Für eine Variable A<String> o wäre keiner der beide Aufrufe möglich.
     
  4. Type Erasure: Generische Typen werden nicht als Platzhalter übersetzt, sondern wird beim Kompilieren der allgemeinste Typ verwendet. Vielleicht fragst du dich, warum das Beispiel eigentlich gar nicht kompiliert. Der Grund dafür ist Type Erasure. Der Compiler übersetzt “A” nicht in allen möglichen Formen (A<X>, A<Y>, A<Integer>, …), weil es dafür ja unendlich viele Möglichkeiten gäbe. Obwohl es die Klasse in unendlich vielen verschiedenen Ausführungen gibt, wird sie nur einmal übersetzt – und zwar als A<Object>, d. h. T wird durch Object ersetzt. Angenommen wir hätte ein A<X> a = new A<X>(); und rufen nun die Methode f mit unserem X-Objekt von oben auf, also a.f(g), dann wird in der f-Methode ja x.m() aufgerufen (wobei x = g gilt). Die Methode m() gibt es aber aus statischer Sicht nicht… Warum? Der statische Typ von x ist innerhalb von der “allgemeinen” Klasse A leider Object (eben wegen Type Erasure). Wegen Zeile 2 kompiliert das Programm also nicht. Der sog. raw type ist deshalbObject, weil T nicht eingeschränkt ist. Wäre die Definition stattdessen A<T extends X>, so würde T durch X statt Objekt erased werden und alles würde wie erwartet laufen.
     
    Auch bei Attributtypen werden entsprechend ersetzt. Betrachten wir wieder A<X> a = new A<X>();, dann ist der statische Typ von a.t hier X und der Aufruf a.t.m() wäre möglich. Benutzen wir das Attribut aber innerhalb von A, dann ist der Typ von t wieder Object… Während a.t.m() also funktioniert, wäre der Aufruf t.m() innerhalb von A nicht möglich (aus dem gleichen Grund weshalb auch x.m() nicht möglich ist).
     
    Type Erasure kann noch komplizierter werden, wenn es bspw. um Überschreibung geht (siehe Altklausur WS 18/19 Wdh.). Nähere Erklärungen dazu mache ich hier nicht, weil das zu kompliziert zu schreiben ist. Man lernt es außerdem besser, indem selbst Beispiele in der IDE macht. Type Erasure haben wir bspw. bei Aufruf 8 in meiner GenericPoly Aufgabe – schau dir das mal an + Erklärung dazu.