CRTP in Java

von Hubert Schmid vom 2013-07-28

Die Abkürzung CRTP steht für Curiously Recurring Template Pattern. Dabei handelt es sich um ein Idiom, das vor allem in C++ weit verbreitet ist, und bei dem Vererbung und Templates auf eine ganz bestimmte Weise miteinander kombiniert werden: Eine Klasse leitet von einer generischen Klasse oder einem generischen Interface ab, wobei als Typparameter die ableitende Klasse verwendet wird. In Java wird beispielsweise das Interface Comparable auf diese Weise eingesetzt:

class Date implements Comparable<Date> { // ... public int compare(Date other) { /* ... */ } }

Was ist daran besonders? Zunächst einmal fällt auf, dass die compare-Methode spezifisch für die ableitende Klasse ausgeprägt ist. Entsprechend sieht das für andere Implementierungen aus, wie im folgenden Listing skizziert. Das liskovsche Substitutionsprinzip gilt in solchen Situationen also nicht. Nicht einmal die Signaturen der sich entsprechenden Methoden sind zueinander kompatibel.

class Time implements Comparable<Time> { // ... public int compare(Time other) { /* ... */ } }

Genau genommen stimmt das natürlich nicht: Java realisiert generische Typen mittels Type Erasure. Der dadurch entstehende Bruch der Typisierung sorgt dafür, dass Date und Time zur Laufzeit doch das gemeinsame Interface Comparable implementieren – auch wenn es im Quellcode nicht danach aussieht.

Zurück zum CRTP: Die Konsequenz ist, dass generische Interfaces, die für CRTP konzipiert sind, nur auf zwei Arten verwendet werden:

  1. Als Super-Interface in einer implements-Klausel.
  2. Als obere Schranke für Typ-Parameter.

In anderen Worten: Man arbeitet nie direkt mit dem statischen Typ Comparable<T>, sondern nur mit statischen Typen T, die das Interface Comparable<T> implementieren. Am Beispiel der Methode isSorted ist das gut zu sehen. Die Methode im folgenden Listing lässt sich ohne Umgehung der statischen Typisierung nicht direkt auf Comparable implementieren.

public static <T extends Comparable<T>> boolean isSorted(Iterable<T> iterable) { Iterator<T> iterator = iterable.iterator(); if (iterator.hasNext()) { T previous = iterator.next(); while (iterator.hasNext()) { T current = iterator.next(); if (previous.compareTo(current) > 0) { return false; } previous = current; } } return true; }

Ich mache noch ein zweites Beispiel, um die Eigenschaften des CRTP genauer aufzuzeigen. Dazu verwende ich dieses Mal eine abstrakte Klasse, um ein Mixin zu realisieren. In Java ist dieses Vorgehen ungewöhnlich, da Mixins nur in Zusammenhang mit Mehrfachvererbung wirklich sinnvoll sind. Die gute Nachricht allerdings ist: Java 8 führt Mehrfachvererbung unter dem Stichwort Default Methods ein, so dass sich solche Mixins auch in Java sinnvoll möglich sind.

Die Klasse ComparableMixin führt die Implementierungen der Methoden isEqualTo und compareTo auf die abstrakte Methode isLessThan zurück. Konkrete Klassen müssen also lediglich Letztere implementieren, wohingegen die anderen beiden Methoden durch das Mixin realisiert werden. Weiter unten zeige ich, wie diese Verwendung aussieht.

abstract class ComparableMixin<T> { protected abstract boolean isLessThan(T other); public final boolean isEqualTo(T other) { return !isLessThan(other) && !other.isLessThan((T) this); } public final int compareTo(T other) { if (isLessThan(other)) { return -1; } else if (other.isLessThan((T) this)) { return 1; } else { return 0; } } }

Allerdings funktioniert das so in Java nicht, da über den Typ T nichts bekannt ist. Die Lösung ist denkbar einfach. Ich versuche lediglich einen gewissen Spannungsbogen zu erzeugen, da der folgende Punkt entscheidend ist: Generische Klassen und Interfaces, die für CRTP konzipiert sind, sollten stets den Typ-Parameter wie im folgenden Listing beschränken:

abstract class ComparableMixin<T extends ComparableMixin<T>> { // ... }

Einerseits erreicht man dadurch eine stärkere Typisierung des Typ-Parameters T. Und andererseits wird die Verwendung auf sinnvolle Ableitungen eingeschränkt.

Verwendet wird das Mixin als Basisklasse. Das folgende Listing zeigt dazu die Klasse Duration, die lediglich die abstrakte Methode isLessThan implementiert und die restliche Funktionalität von ComparableMixin erbt. Wichtig ist nochmals: Das Mixin sollte nur als Basisklasse oder für Typschranken verwendet werden. Dagegen ist jede andere Verwendung eine starke Indikation für schlechten Code.

class Duration extends ComparableMixin<Duration> { private long millis; protected boolean isLessThan(Duration other) { return millis < other.millis; } }

Das CRTP ist also auch für Java relevant. Hier ist es häufig zusammen mit Interfaces zu sehen, die Eigenschaften von Klassen beschreiben. Java 8 macht den Einsatz darüber hinaus auch für Mixins interessant, um Typ-spezifische Ausprägungen von Methoden zu erben. Doch dabei sollte man stets die folgende Regel beachten: Die Basisklasse des CRTP sollte nie direkt als statischer Typ von Objekten verwendet werden. Hält man sich an diese Regel, kann fast nichts mehr schief gehen.