Probleme mit Java-Generics

Eine recht interessante Kurz-Debatte zur Anwendung von Java-Generics findet sich im Issue-Tracker des JSR-310-Projekts. Folgende generische Methodendefinition liefert leider keine Compile-Time-Type-Safety, wie Stephen Colebourne richtig festgestellt hat:

<R extends Temporal> long between(R dateTime1, R dateTime2);

Zitat von S. Colebourne:

I’ve examined the generics on the temporal classes twice now. It isn’t possible to enhance their generics without making a terrible mess. Because Java doesn’t have the Self-type generic, or the notional of parameter self-type generic where the class is final, and because LocalDate extends ChronoLocalDate, it is more or less impossible to go further than we have.

Und das Fazit des JSR-Teams inklusive von R. Riggs von Oracle lautet dann, die Generics sang- und klanglos zu entfernen. Mehr oder weniger unmöglich. Wirklich?

Nehmen wir noch einmal die Klasse Temporal unter die Lupe. Wir finden da nicht-generische Methoden wie:

long periodUntil(Temporal endTemporal, TemporalUnit unit)

Calculates the period between this temporal and another temporal in terms of the specified unit.
Temporal plus(long amountToAdd, TemporalUnit unit)

Returns an object of the same type as this object with the specified period added.

Sehr unbefriedigend daran ist natürlich, daß null Typsicherheit gegeben ist. Zu einem Temporal-Date könnte eine Implementierung in der plus()-Methode theoretisch eine Sekunde addieren und einen Zeitstempel als Kombination von Datum und Zeit zurückgeben. Es ist so wirklich beliebig. Der Compiler ist mit allem zufrieden. Nur in der Spezifikation der plus()-Methode steht dann, daß Implementierungen den gleichen Typ zurückgeben müssen. Sicherlich hat dieser offensichtliche Design-Mangel Stephen Colebourne dazu bewegt, noch mal einen Blick daraufzuwerfen – leider ohne Ergebnis. Und wenn doch, würde es auch nichts mehr nützen, weil zu einer konsequenten aber sehr zeit- und arbeitsintensiven Umstellung des JSR-Projekts auf Generics schon längst keine Zeit mehr vorhanden ist. Die Debatte, die Colebourne losgetreten hat, ist wenige Tage vor dem Java-8-Meilenstein M6!!!, also recht sinnfrei.

Ich habe mich im Gegensatz zum JSR-Team von Anfang an mit dem Problem der Generics in einer Datums- und Zeitbibliothek beschäftigt, nämlich in Vorstudien ab Oktober 2011. Und das Ergebnis meiner Überlegungen ist schon im Frühstadium in die Entwicklung von Time4J ab Herbst 2012 eingeflossen. So sieht das Ergebnis aus:

Ich habe eine abstrakte Klasse mit dem Namen TimePoint. Diese Klasse referenziert sich selbst über Generics. Konkrete Subklassen müssen final sein und die Generics auflösen, nämlich in einem ersten vereinfachten Schritt so:

abstract class TimePoint<T extends TimePoint<T>> {…}

final class IsoTime extends TimePoint<IsoTime> {}

final class IsoDate extends TimePoint<IsoDate> {}

Und dann betrachten wir erneut die oben erwähnte between()-Methode, was bei der Eingabe verschiedener Argumenttypen für T passiert (z.B. IsoDate und IsoTime):

<T extends TimePoint> long between(T start, T end); // Compiler sagt: OK

<T extends TimePoint<T>> long between(T start, T end); // Compiler verweigert

Entscheidend für die Compile-Time-Type-Safety ist also das “self-referencing-generics”-Feature. Freilich ist dieses Feature nicht neu. Es wird schon seit Java 5 in der bekannten Klasse java.lang.Enum angewandt. Und auch für Stephen Colebourne ist es nicht unbekannt, wurde er mehrfach in seinem eigenen Blog darauf hingewiesen, zuletzt 2012. Aber offenbar WILL er das Feature nicht einbauen und erklärt es für “terrible mess”, zu deutsch etwa: “furchtbar umständlich”.

Ich bin hingegen der Meinung, daß Generics hier das Problem der Typsicherheit zur Kompilierzeit entscheidend lösen können, während gleichzeitig konkrete Anwendungen, die die finalen Subklassen IsoDate und IsoTime nutzen, davon quasi nichts mitbekommen, weil die selbstreferenzierenden Typparameter auf dieser Ebene gar nicht mehr vorkommen. Es sind somit auch keine störenden Typparameter oder gar Wildcards im Anwendungscode notwendig. Also wirklich nur Vorteile auf der ganzen Linie.

Verschweigen will ich allerdings nicht, daß zwar Anwendungen ungemein von Java-Generics in dieser Form profitieren, aber der Framework-Bau auf dieser Basis hoch anspruchsvoll ist. So ist meine tatsächliche Lösung in Time4J noch etwas komplizierter. Zum Beispiel ist die obige Signatur der Klasse TimePoint wie folgt erweitert worden:

abstract class TimePoint<U, T extends TimePoint<U, T>> {…}

Hier steht der Typparameter U für das zugehörige System von Zeiteinheiten. Es hat sich nämlich gezeigt, daß es in einer gegebenen Chronologie ausgedrückt durch eine TimePoint-Klasse nicht sinnvoll ist, beliebige Zeiteinheiten zuzulassen. Ein Zeitpunkt ist inhärent über seine Koordinaten (als Zustandsattribute modelliert) auch mit einer Zeitachse verbunden, auf der der Zeitpunkt liegt. Die zulässigen Zeiteinheiten, die letztlich Längenabstände auf der Zeitachse messen, müssen zu den Koordinaten passen. Z.B. ist eine Zeitarithmetik basierend auf Sekunden in einer reinen Datumsklasse unsinnig. Also auch hier über den zweiten Typparameter U eine wichtige Maßnahme für mehr Typsicherheit. Die finalen Subklassen IsoDate und IsoTime lösen auch diesen U-Typparameter konkret auf.

Der Framework-Bau mit dann zwei Typparametern U und T ist natürlich nicht einfach und erfordert großen Aufwand. Tatsächlich ein Aufwand im Bereich des low-level-API, mit dem ich schon weit mehr als 1 Jahr beschäftigt (und jetzt praktisch fertig) bin. Wahrscheinlich hat das JSR-310-Team diesen Aufwand schon 2007 nicht leisten können und wollen. Jetzt ist es erst recht viel zu spät für den JSR, diesen Weg im Design einzuschlagen. Ich fürchte auch, sie haben noch nicht einmal wirklich den Weg gesehen. Schade!

Die Kommentarfunktion ist geschlossen.