Casts mit zusätzlichen Bounds in Java 8

Immer wieder beschert mir Java wundervolle WTF-Momente, so auch heute. Eine Syntaxerweiterung in Java 8, die komplett an mir vorbeigegangen ist, sind Casts mit zusätzlichen Bounds:

LinkedList<String> list = new LinkedList<>();
List<String> list1 = (List & Queue) list; //OK
List<String> list2 = (List & RandomAccess) list; //ClassCastException

Die JLS schreibt dazu recht lakonisch :

If the cast operator contains a list of types – that is, a ReferenceType followed by one or more AdditionalBound terms – then all of the following must be true, or a compile-time error occurs.

Stellt sich die Frage ist, wozu das Ganze gut sein soll. Der einzige sinnvolle Anwendungsfall, den ich gefunden habe, ist die Spezifizierung zusätzlicher Interfaces bei Lambdas:

Runnable r = (Runnable & Serializable) () -> System.out.println("Serializable!");

Lambdas haben ja eigentlich keinen Typ, sie sind ein wenig wie Schrödingers Katze: Erst wenn man etwas mit ihnen anstellt – etwa die Zuweisung zu einer Variablen – entscheidet sich, was ihr Typ ist. Durch den vorgelagerten Cast wird diese Typbestimmung vorgezogen, so dass das Objekt r nicht nur Runnable, sondern auch Serializable ist, und später auch problemlos serialisiert werden kann.

Wie findet ihr dieses etwas obskure Feature? Seht ihr noch andere sinnvolle Anwendungsmöglichkeiten?

Werbeanzeigen

zipWith in Java 8

Heute einmal ein ziemlich einfaches Beispiel, wie Lambdas das Leben in Java 8 leichter machen. In Haskell und Scala gibt es die Funktione zipWith, mit der zwei Datenstrukturen wie Listen durch elementweise Verknüfung zu einer neuen „zusammengeklebt“ werden. Dabei muss man aufpassen: Ist eine der Ausgangsstrukturen länger als die andere, werden die „überflüssigen“ Elemente einfach ignoriert. In Java bietet sich so eine Funktion an mindestens zwei Stellen an: Bei Iterables und bei den neuen Streams. Da ich mich mit letzteren (noch) nicht so gut auskenne, will ich heute den einfacheren ersten Fall behandeln.

Ein besonders nützlicher Anwendungsfall für zipWith ist, wenn man mit der erweiterten for-Schleife zwei Collections gleichzeitig durchgehen will – vorher musste man meist auf andere Sprachmittel (z.B. Indexe oder Iteratoren) ausweichen. Wie könnte nun so eine Schleife aussehen?

List<String> strings = Arrays.asList("a","b","c");
List<Integer> ints = Arrays.asList(6,9,14,32);
for(String result : zipWith(strings, ints, (s,i) -> s + i)) {
   System.out.println(result);
}

Das erwartete Ergebnis wären hier die Zeilen „a6“, „b9“ und „c14“. Natürlich wäre auch eine anonyme Klasse an Stelle des Lambda-Ausdrucks möglich gewesen, aber erst durch diesen wird das ganze Konstrukt lesbar. Die Umsetzung ist trivial:

import java.util.function.BiFunction;
...
public static <A,B,C> Iterable<C> zipWith(Iterable<A> iterableA, Iterable<B> iterableB, BiFunction<A,B,C> fn) {
    return () -> new Iterator<C>() {
        private Iterator<A> itA = iterableA.iterator();
        private Iterator<B> itB = iterableB.iterator();

        public boolean hasNext() {
            return itA.hasNext() && itB.hasNext();
        }

        public C next() {
            return fn.apply(itA.next(), itB.next());
        }
    };
}

Wer sich wundert, wo das „new Iterable“ geblieben ist: Da das Interface nur eine Methode (nämlich iterator()) besitzt, können wir es durch einen Lambda-Ausdruck ersetzen. Wir brauchen auch die remove-Methode von Iterator nicht zu implementieren, es gibt in Java 8 eine Default-Methode dafür (die eine UnsupportedOperationException wirft). Und als letztes fällt auf, dass wir aus dem Lambda-Ausdruck heraus auf iterableA und iterableB zugreifen konnten, ohne dass wir diese final machen mussten. Da beide Argumente nicht (weder in der Methode noch im Lambda-Ausdruck) verändert werden, sind sie „effektiv final“ und benötigen keinen entsprechenden Modifikator.

Das war jetzt etwas leichtere Kost, aber ich hoffe trotzdem ein wenig nützlich.

Wer weiß, wie man das Gleiche mit Streams anstellt, darf seine Lösung hier gerne vorstellen, ich bin gespannt darauf…

[Update]

Für Streams habe ich auf Stackoverflow diesen Schnipsel gefunden:

public static<A, B, C> Stream<C> zip(Stream<? extends A> a,
                                     Stream<? extends B> b,
                                     BiFunction<? super A, ? super B, ? extends C> zipper) {
    Objects.requireNonNull(zipper);
    @SuppressWarnings("unchecked")
    Spliterator<A> aSpliterator = (Spliterator<A>) Objects.requireNonNull(a).spliterator();
    @SuppressWarnings("unchecked")
    Spliterator<B> bSpliterator = (Spliterator<B>) Objects.requireNonNull(b).spliterator();

    // Zipping looses DISTINCT and SORTED characteristics
    int both = aSpliterator.characteristics() & bSpliterator.characteristics() &
            ~(Spliterator.DISTINCT | Spliterator.SORTED);
    int characteristics = both;

    long zipSize = ((characteristics & Spliterator.SIZED) != 0)
            ? Math.min(aSpliterator.getExactSizeIfKnown(), bSpliterator.getExactSizeIfKnown())
            : -1;

    Iterator<A> aIterator = Spliterators.iterator(aSpliterator);
    Iterator<B> bIterator = Spliterators.iterator(bSpliterator);
    Iterator<C> cIterator = new Iterator<C>() {
        @Override
        public boolean hasNext() {
            return aIterator.hasNext() && bIterator.hasNext();
        }

        @Override
        public C next() {
            return zipper.apply(aIterator.next(), bIterator.next());
        }
    };

    Spliterator<C> split = Spliterators.spliterator(cIterator, zipSize, characteristics);
    return (a.isParallel() || b.isParallel())
           ? StreamSupport.stream(split, true)
           : StreamSupport.stream(split, false);
}

Der Original-Code war wohl in der Stream-Implementierung im Lambda-Projekt dabei und ist später unverständlicherweise rausgeflogen.

Java 8 und der Y-Kombinator

So schön Lambda-Ausdrücke auch sind, sie haben wenigstens eine Achillesferse: Rekursion. Betrachten wir folgendes Beispiel, das schon seit Java 5 funktionieren würde (ein entsprechendes Function-Interface einmal vorausgesetzt):

...
Function<Integer, Long> fac = new Function<Integer, Long>() {
    @Override
    public Long apply(Integer n) {
        return n == 0 ? 1L : n * apply(n-1);
    }
};

System.out.println(fac.apply(10)); //--> 3628800
...

So weit, so gut und auch so langweilig: Die gute alte Fakultät. Aber wie lässt sich die Definition der anonymen Klasse durch einen Lambda-Ausdruck ersetzen? Hier zwei Beispiele dafür, wie es schon einmal nicht geht:

//Compiler-Fehler: "Variable fac might not have been initialized"
Function<Integer, Long> fac = n -> n == 0 ? 1L : n * fac.apply(n-1);  

//this verweist hier auf die umgebende Klasse, nicht auf den Lambda-Ausdruck selbst
Function<Integer, Long> fac = n -> n == 0 ? 1L : n * this.apply(n-1);  

Wir müssen also tiefer in die Trickkiste greifen. Man bräuchte einen Weg, um die Rekursion sozusagen „explizit“ zu machen. Erfreulicherweise ist dieses Problem schon lange gelöst und gehört zur Folklore der funktionalen Sprachen: Man verwendet einen Fixpunkt-Kombinator wie den Y-Kombinator. Ich will an dieser Stelle nicht genauer auf die Funktionsweise eingehen, hauptsächlich um zu verhindern, dass mein Gehirn schmilzt, aber auch, weil man unter Magiern keine Tricks verrät. Deshalb hier eine sehr direkte Umsetzung dieses Konzepts (z.B. ohne weitere Vorkehrungen, um einen Stacküberlauf zu verhindern):

...
public static <X,Y> Function<X,Y> yCombinator(Function<Function<X,Y>,Function<X,Y>> f) {
   return x -> f.apply(yCombinator(f)).apply(x);
}
...

Function<Integer,Long> fac = yCombinator(f -> n -> n == 0 ? 1L : n * f.apply(n-1));

System.out.println(fac.apply(10)); //--> 3628800

Wer mehr dazu erfahren will, sollte sich den Wikibook-Eintrag zu Haskell’s fix-Funktion anschauen. Aber selbst wenn man die „Magie“ nicht im Detail nachvollziehen kann oder will, ist es nützlich zu wissen, dass es diesen Trick gibt. Es ist auch ein schönes Beispiel dafür, dass es sich lohnen kann, einmal über den objektorientierten Tellerrand zu schauen, um die tollen neuen Features von Java 8 auch wirklich ausnutzen zu können.

Update

Vielleicht ist folgende Schreibweise mit BiFunction lesbarer:


public static <X,Y> Function<X,Y> yCombinator(BiFunction<X,Function<X,Y>,Y> f) {
    return x -> f.apply(x, yCombinator(f));
}
...
//Anwendung
Function<Integer,Long> fac = yCombinator((n,f) -> n == 0 ? 1L : n * f.apply(n-1));
System.out.println(fac.apply(10));