Default-Methoden in Java 8 und Architektur

Während man genügend Erklärungen zu Default-Methoden in Java 8 im Netz findet, scheinen Erklärungen, was man damit in Bezug auf die Software-Architektur besser machen kann, ziemlich rar zu sein. Ich habe dazu eigentlich nur das gefunden:

Template-Method-Pattern: http://java.dzone.com/articles/template-method-pattern-using

Eigentlich ist nicht besonders verwunderlich, dass für viele das nachträgliche Hinzufügen von Methoden zu Interfaces (etwa foreach() zu Iterator in Java 8) als Hauptanwendungsgebiet angesehen wird. Schließlich **hießen **Default-Methoden einmal so, nämlich Erweiterungsmethoden (Extension Methods).

Wer Scalas Traits (als eine Form der Mixin-Vererbung) kennt, weiß auch, dass man damit noch mehr anstellen kann, nur ist Scala eine andere Sprache, und nicht alles dort lässt sich eins zu eins nach Java übersetzen.

Deshalb will ich einmal an dieser Stelle einmal über die sinnvolle Anwendungen von Default-Methoden brainstormen. Spontan kommen mir neben den bereits genannten noch vier Anwendungsfälle in den Sinn:

  1. Vermeidung abstrakter Adapter: Oft hat man in einer Bibliothek (und auch in den Java-APIs) ein Interface Foo, und darauf aufbauend eine abstrakte Klasse zur bequemen Implementierung (oft FooAdapter, AbstractFoo oder DefaultFoo genannt). Typische Beispiele sind java.awt.event.MouseAdapter oder java.util.AbstractList. Aber diesen Job kann in Zukunft das Interface selbst übernehmen, wenn eine zustandslose Implementierung möglich ist.

  2. Stateless Diamond: Das bekannte Diamant-Problem tritt bei Mixin-Vererbung nicht auf. Manchmal benötigt man diese z.B. bei mathematischen Strukturen (etwa bei Gruppe und Magma, siehe http://en.wikipedia.org/wiki/Magma_(algebra) )

  3. Verallgemeinerndes Sub-Interface: Angenommen wir hätten ein Interface Equals<T> mit einer “symmetrischen” Vergleichsmethode boolean eq(T t1, T t2). Dann wäre es sinnvoll, Comparator<T> als dessen Sub-Interface anzusehen. Wenn man aber sowieso die übliche int compare(T t1, T t2)-Methode implementieren muss - die in einem gewissen Sinne equals “verallgemeinert”, kann man mit diesem Wissen auch davon eine Standard-equals-Implementierung ableiten, nämlich return compare(t1, t2) == 0;.

  4. Decorator-Pattern: Hat man z.B. in einem GUI-Framework eine Hierarchie ausgehend von einer abstrakten Component-Klasse, die etwa eine drawBorder() und eine drawBackground()-Methode besitzt, könnte man Interfaces schreiben, die dafür jeweils Default-Methoden bereitstellen, und dann “on the fly” Komponenten zusammensetzen: class MyBorderLabel extends Label implements LineBorder, TransparentBackground. Ich weiß nicht so richtig, ob diese Technik wirklich praktisch ist, insbesondere weil die entsprechenden Methoden in der normalen Vererbungshierarchie nicht implementiert sein dürfen. Ebenfalls hinderlich ist, dass man (im Gegensatz zu Scala) zusätzliche Interfaces nicht einfach bei der Instantiierung “mitgeben kann”, also etwas wie Label myLabel = new Label("foo") implements LineBorder, TransparentBackground; nicht erlaubt ist.

Fällt euch noch was ein?

Mir fällt da noch die Vermeidung von Fassaden oder Factories ein. Ich hab total oft Interfaces und package-private Default-Implementierungen (nicht abstrakt). Instanzen dieser Default-Implementierungen gebe ich dann mit einer Fassaden- oder Factoryklasse nach draußen. Das könnte ich mir dann schenken. Würde die Anzahl der nötigen Klassen auf die Hälfte minus mindestens 1 reduzieren.

Und bei den Methoden der Fassade-/Factory Klassen schwanke ich immer zwischen “static, weil’s schneller geht” und “auch Fassade/Factory als Interface/Implementierung, weil’s sauberer ist”. Also, könnte man auch Fassaden/Factories sauberer implementieren.

Einziger Knackpunkt, den ich sehe ist, dass bei reinen Funktions-Interfaces (sowas wie Comparator bspw.) eigentlich ein Singleton (ich weiß, böse, aber hier sinnvoll, denke ich) angebracht ist, um unnötige Objekterzeugung zu vermeiden. Das wäre bei Interfaces mit Default-Methoden nicht durchsetzbar, oder doch?

Eigentlich ist nicht besonders verwunderlich, dass für viele das nachträgliche Hinzufügen von Methoden zu Interfaces (etwa foreach() zu Iterator in Java 8) als Hauptanwendungsgebiet angesehen wird

Ich sehe nicht das sich das irgendwie geändert hätte. Schaut man durch die Sourcen des JDK bestätigt sich das auch, das default-Methoden hauptsächlich für die API-Designer relevant sind um Schnittstellen endlich auf Source-Ebene erweitern zu können. Vorher war es nur „binär kompatibel“, d.h. die alten Klassen liefen alle solange man sie nicht neu kompilierte.

Aber zurück zum Thema.

Das Template Method Pattern unter dem Link ist schon eine recht ungewöhnliche Variante. Bevor ich auf den Link geklickt habe hätte ich etwas folgender Form erwartet.

public interface A{
    void method1(int);
    int method2()
    default void defaultMethod(int arg){
        method1(method2(arg));
    }
}

Nach ein wenig durchcruisen des JDK, sind für mich folgende Punkte bzgl. default-Methoden abgefallen. Zuerst die Implementierung von optionalen-Methoden (entspricht Punkt 1). Dann die Mehrfachvererbung von Verhalten. Es ist nun endlich möglich sinnvoll Interfaces in kleinere voneinander unabhängige zu unterteilen und dementsprechend „gemischt“ zu implementieren. Dann in Kombination mit „Lambdas“[1] können vernünftige „Pipelines“ gebastelt werden. Z.B. die and/or-defaultMethoden oder die „thenComparing“-Methode die z.B. das Dekoratorpattern bei Comparatoren überflüssig macht (Sortieren nach mehreren Kriterien). Vll. kann man auch solche Dispatch-Krücken wie das Visitor-Pattern vereinfachen. Mehr fällt mir spontan jetzt nicht ein.

[1] Eigentlich möchte ich das Wort gar nicht in den Mund nehmen wenn es um die Functional Interfaces in Java geht.

Schöne Vorschläge!

@nillehammer : Ist zwar kein “echtes” Singleton, hilft aber auch schon, unnötige Instanzen zu vermeiden (dummes Beispiel, aber egal):

public interface ReverseComparator extends Comparator<String> {
    
    ReverseComparator INSTANCE = new ReverseComparator() {}; 
    
    @Override
    default int compare(String o1, String o2) {
        return new StringBuilder(o1).reverse().toString().compareTo(
                new StringBuilder(o2).reverse().toString());
    }
}

@ThreadPool :
Mir ist klar, dass das “Retrofitting” ein Hauptgrund für die Einführung war, aber allein das wird meiner Meinung nach dem Potential von Default-Methoden nicht gerecht. Ich denke, dass Default-Methoden zu Unrecht im Schatten der Lambdas stehen, und in Zukunft das Design (insbesondere von Bibliotheken) stark beeinflussen werden.

Beim Visitor-Pattern denke ich, dass da eher Lambdas helfen könnten. Müsste man sich mal genauer anschauen.

Zuerst ich verstehe was ihr mit den Singleton-Ideen ausdrücken wollt und wohin der Weg geht. Aber, auch wenn du erwähntest dass das Beispiel nicht so toll sei, finde ich dann sollte man ein anderes wählen. Es gibt, wie ich finde, zwei Probleme mit deinem Beispiel. Das Erste ist, würde man das ernsthaft so noch in Java8 schreiben? Ich denke nicht, vll. kommt mehr das Folgende heraus.

public class StringFunctions {
    private static Comparator<String> reverseComp = (String a, String b) ->{
        return new StringBuilder(a)
                    .reverse()
                    .toString()
                    .compareTo(new StringBuilder(b)
                                    .reverse()
                                    .toString());
    };

    public static Comparator<String> reverseComp(){
        return reverseComp;
    }
}

Das zweite Problem, ist der Gedanke das man irgendwas „sparen“ müsste. Den ReverseComparator würde man im Code irgendwie als Lambda-Ausdruck verwerten. Dann wird aber kein Objekt erzeugt wie z.B. bei einer anonymen Klasse sondern vielmehr mit dem invokeDynamic auf Byte-Code Ebene herumgespielt. Das wiederrum macht das Cachen des Comparators obsolet. Für mehr zu invokeDynamic siehe Service Reliability Management | Harness. Ich bin mir recht sicher das Scala da vll. auch nachzieht um noch performanter zu werden.

Vollste Zustimmung! Die Lamdas könnte man - etwas polemisch abwertend - als „syntaktischen Zucker“ bezeichnen, weil sie nicht wirklich eine neue Funktionalität bieten: Was mit Lambdas geht, ginge auch mit einem Wust aus SingleAbstractMethod-Types und einem einem Haufen anyonymen inneren Klassen - nur nicht so schön, natürlich.

Mit den default-methods kommt aber eine wichtige Funktion in Java, die ich zumindest schon oft aufs schmerzlichste vermisst habe. Zum ersten Mal wird „Interface Evolution“ mit Java möglich. Vorher war die Definition eines Interfaces (zumindest für mich) eine extreme Herausforderung - aus mindestens zwei (zusammenhängenden) Gründen: Der erste ist: Es ist FÜR IMMER. Man wird nie wieder etwas daran ändern können, wenn es erstmal „released“ ist. Der zweite ist die Frage „Minimal oder bequem?“. Ein Interface sollte meiner Ansicht nach „sauber“ sein. Rein theoretisch-algebraisch würde das eigentlich bedeuten: Es sollte den minimalen Satz von Methoden enthalten, die dieses „Ding“, was man damit beschreiben will, unbedingt haben muss. Konsequent angewendet bedeutet das aber, dass es extrem unbequem zu verwenden sein kann.

Als suggestives Beispiel: Angenommen, man wollte ein interface für einen 2D-Punkt definieren:

interface Point2D {
    float getX();
    float getY();
    void setX(float x);
    void setY(float y);
}

Das wars. Punkt-fertig-aus. Mehr braucht man nicht. Aber das ist natürlich ziemlich sinnlos. Man hätte natürlich auch gerne Methoden wie add(Point2D other), distance(Point2D other) und 100 andere. Der „übliche Weg“ war da bisher, dass man diese Methoden mit leicht flauem Gefühl im Magen ins interface aufgenommen hat, und eben einen „AbstractPoint2D“ definiert hat, bei dem all diese Methoden vorimplementiert waren, und wo nur die oben schon skizzierten Methoden wirklich abstrakt waren.

In Zukunft können diese Methoden bedenkenlos mit ins interface aufgenommen werden. Man kann das auch, ganz grob, recht einfach klassifizieren: Ins interface können jetzt alle Methoden, die NICHT auf irgendwelchen Fields arbeiten müssen (die bei einer abstrakten Klasse dann „protected“ gewesen wären). Aber da abstrakte Klassen mit protected Fields IMHO ohnehin immer etwas kritisch waren, bedeutet das für mich: Durch default-Methoden verlieren abstrakte Klassen praktisch jegliche Existenzberechtigung. (Es könnte Ausnahmen geben, aber wohl sehr selten).

Soweit ich das bisher gesehen habe, ist der verlinkte Artikel nur eine Ausprägung dieses Gedankens (aber ich muss wohl nochmal schauen, ob ich da nicht irgendeinen Knackpunkt übersehen habe…). Die wichtigsten Anwendungsfälle stehen damit schon im Eröffnungspost, und es dauert vielleicht eine Weile, bis man ein „Gefühl dafür“ hat, wo man das noch trickreich und nützlich einsetzen kann, und sich bestimmte neue Patterns/Paradigmen/Best Practices etablieren. Auf jeden Fall ist es ein wichtiger neuer Freiheitsgrad beim Interfacedesign.