Java: Über Type-Erasure und Type-Variance
Ich habe diese Woche einen interessanten Artikel gelesen, in dem es um die Frage ging, wie man eine Referenz vom Typ Iterable<Derived>
einer Variablen vom Typ Iterable<Base>
zuweist – sozusagen angewendete Kovarianz. Der Artikel beschreibt eine Lösung, die auf ein Proxy-Objekt setzt, geht jedoch leider nicht auf weitere Alternativen ein.
Die Ausgangssituation ist also eine Variable vom Typ Iterable<String>
, die einer Variablen vom Typ Iterable<Object>
zugewiesen werden soll. Ohne Weiteres funktioniert diese Zuweisung nicht, und der Compiler von Java 7 bemängelt im folgenden Code zurecht die zweite Zeile:
Iterable<String> strings = ...;
Iterable<Object> objects = strings; // ERROR: incompatible types
Type-Erasure
Die erste Lösung ist zwar nicht elegant, dafür aber unglaublich einfach. Man muss sich lediglich klar machen, was Type Erasure
in Java bedeutet: Parametrisierte Typen unterscheiden sich nur während der statischen Code-Analyse – spielen zur Laufzeit also keine Rolle. Konkreter: Zur Laufzeit gibt es keinen Unterschied zwischen Iterable<String>
und Iterable<Object>
.
Das einzige Problem bei der Zuweisung ist also die statische Code-Analyse. Doch dieses Problem lässt sich mit einem Cast
und einer geeignet parametrisierten Hilfsfunktion einfach und sicher umgehen:
@SuppressWarnings("unchecked")
private static <T, U extends T> Iterable<T> cast(Iterable<U> iterable) {
return (Iterable<T>) iterable;
}
Die Situation ist nun ein wenig kurios: Im Quelltext wird die Referenz eines Typs in die Referenz eines anderen Typs umgewandelt, obwohl beide Typen zur Laufzeit identisch sind. Aber was passiert denn nun tatsächlich? Die Antwort offenbart ein Blick in den generierten Byte-Code: Es passiert nichts! Denn der Cast existiert wenig überraschend im generierten Byte-Code nicht mehr.
Type-Variance
Die zweite Lösung ist eleganter und gleichzeitig weniger praktikabel. Denn sie setzt voraus, dass Kovarianz und Wildcards in der Breite verstanden sind, was – wie man bereits an der Standardbibliothek erkennt – leider nicht der Fall ist.
Eigentlich ist die Situation ganz einfach: Iterable<T>
ist ein kovarianter Typ und Java 7 unterstützt Kovarianz nur in Form von Wildcards bei Referenzen. Ersetzt man die Referenzen der Form Iterable<T>
durch Iterable<? extends T>
, so funktionieren auch die kovarianten Zuweisungen, ohne dass es zu signifikanten Einschränkungen kommt. Im folgenden Code-Fragment wird das verdeutlicht:
Iterable<? extends String> strings = ...;
Iterable<? extends Object> objects = strings; // works
Die Probleme daran: Die Angaben sind redundant, ziehen sich durch den gesamten Code und werden von zu vielen Entwickler nur unzureichend verstanden. Das muss man akzeptieren und berücksichtigen.
Also: Besser Type-Erasure statt Type-Variance. Beide Vorgehen erfordern zwar ein gewisses Grundverständnis über generische Typen, doch zumindest beschränken sich die Auswirkungen des ersten Vorgehens auf einzelne Code-Stellen, so dass die darunter liegenden Konzepte nicht von jedem Entwickler verstanden werden müssen.