Typinferenz fĂĽr lokale Variablen in Java 10, Teil 2

Seite 3: Mixins

Inhaltsverzeichnis

Zu guter Letzt kommen wir zu einem weiteren Feature einiger Programmiersprachen, das sich in Java nun dank var (umständlich) ausdrücken lässt: Mixins. Dabei geht es darum, ad hoc eine Variable zu erstellen, die die Funktionen verschiedener Typen vereint. Konzeptionell könnte das so aussehen:

type Megacorp {
String name();
BigDecimal earnings();
}

type IsSuccessful {
boolean isSuccessful() {
return earnings().compareTo(SUCCESS_BOUNDARY) > 0;
}
}

type IsEvil {
boolean isEvil() { return true; }
}

Megacorp & IsSuccessful & IsEvil corp =
new (Megacorp & IsSuccessful & IsEvil)(/*...*/);
System.out.printf(
"Corporation %s is %s and %s.\n",
// relying on 'corp' as 'Megacorp'
corp.name(),
// relying on 'corp' as 'IsSuccessful'
corp.isSuccessful() ? "successful" : "a failure",
// relying on 'corp' as 'IsEvil'
corp.isEvil() ? "evil" : "a failure"
);

Das ist zunächst recht weit von Java weg, aber mit ein paar Generics-basierten Tricks kann man dahin kommen. Das Grundprinzip ist, einen Lambda-Ausdruck zu verwenden, der auf den gewünschten Intersection Type gecastet und einer var-Variable zugewiesen wird:

// compiler infers desired intersection type for 'corp'
var corp = (MegacorpDelegate & IsSuccessful & IsEvil) () -> megacorp;

Was genau geht da vor? Zunächst einmal muss der zentrale Typ, in diesem Fall Megacorp, ein Interface sein. Um eine Instanz davon mit einem Lambda-Ausdruck erstellen zu können, braucht man außerdem ein delegierendes Interface, das alle Aufrufe an eine gegebene Instanz weiterleitet:

interface Megacorp {
String name();
BigDecimal earnings();
}

@FunctionalInterface
interface MegacorpDelegate extends Megacorp {
Megacorp delegate();
default String name() { return delegate().name(); }
default BigDecimal earnings() { return delegate().earnings(); }
}

Dabei ist wichtig, dass delegate() die einzige abstrakte Methode ist, sodass man fĂĽr eine gegebene Megacorp-Instanz megacorp mit () -> megacorp eine Instanz von MegacorpDelegate erstellen kann. Damit funktioniert schon mal der erste Teil der obigen Zuweisung:

Megacorp megacorp = // ...
// compiler infers 'MegacorpDelegate' for 'corp'
var corp = (MegacorpDelegate) () -> megacorp;

Jetzt kann man auf Megacorp aufbauend beliebige Interfaces erstellen, die allerlei erdenkliche Zusatzfunktionalität haben – solange sie sich ausschließlich mit Default-Methoden, das heißt auf Basis der Megacorp-Funktion, implementieren lassen.

interface IsSuccessful extends Megacorp {
final BigDecimal SUCCESS_BOUNDARY = new BigDecimal("500000000");

default boolean isSuccessful() {
return earnings().compareTo(SUCCESS_BOUNDARY) > 0;
}

}

interface IsEvil extends Megacorp {

default boolean isEvil() {
return true;
}

}

IsSuccessful und IsEvil kann man nicht mit einem Lambda-Ausdruck erstellen, da sie keine abstrakten Methoden haben. Auf der anderen Seite muss man deswegen aber auch keine Methode implementieren, um ihren Vertrag zu erfĂĽllen, und deswegen kann man sie mit einem durch einen Lambda-Ausdruck erstellten Typ schneiden:

// compiles and runs
Megacorp megacorp = // ...
var corp = (MegacorpDelegate & IsSuccessful & IsEvil) () -> megacorp;
System.out.printf(
"Corporation %s is %s and %s.\n",
// relying on 'Megacorp'
corp.name(),
// relying on 'IsSuccessful'
corp.isSuccessful() ? "successful" : "a failure",
// relying on 'IsEvil'
corp.isEvil() ? "evil" : "a failure"
);

Ist eine Megacorp-Instanz gegeben, kann man an Ort und Stelle entscheiden, welche anderen Features man zu einer neuen Instanz zusammenmischen möchte. Und zwar ohne, dass der Ersteller von Megacorp oder MegacorpDelegate davon wissen müsste. Das erlaubt es, unabhängig von existierenden Typen Zusatzfunktionen für sie zu entwickeln, die man im passenden Kontext einbringen und anschließend auf natürliche Art und Weise mit Methodenaufrufen auf der Instanz ansteuern kann.

Nachdem das "Wie" geklärt ist, stellt sich wieder die Frage nach dem "Ob". Lohnt sich das wirklich? Zunächst ist der Aufwand nicht unerheblich, denn passend zum existierenden Interface, hier Megacorp, ist ein ...Delegate-Interface zu erstellen, das alle Methoden weiterleitet.

Der Todesstoß für diesen Ansatz ist aber, dass Default-Methoden keine Methoden aus Object implementieren können. Dadurch kann zum Beispiel ein Aufruf von equals auf einer delegierenden Instanz nicht zum darunterliegenden Objekt weitergeleitet werden. Gemischte Instanzen sind immer auf equals, hashCode, toString etc. von Object festgenagelt, ohne dass das "von außen" ersichtlich ist.

Dass dann auch noch die Mischung nichttrivialer Sprachfeatures und die Einschränkung, dass man die Mixins nur mit den beschränkten Möglichkeiten von Interfaces erstellen kann, dagegen sprechen, fällt da schon fast nicht mehr ins Gewicht. Deswegen ein Ratschlag: So schön Mixins in anderen Sprachen auch sein mögen und so viel Spaß Experimente machen, in Code, der in Produktion läuft, hat dieser Trick nichts zu suchen.

Alternativ kann man auch hier, wie bei den Ad-hoc-Methoden, die gewĂĽnschte Funktionen in Helferklassen oder einer designierten Unterklasse sammeln.