Java 8, lambda e stream

A breve verrà finalmente rilasciato Java 8. Tra le molte novità, i temi caldi rimangono sicuramente project lambda e bulk data operation.

TLDR; lambda expression e stream API avranno un grosso impatto sul futuro del linguaggio, dando un nuovo significato al concetto di funzione e nuovi strumenti per operare sulle Collection

Lambda expression

Una espressione lambda rappresenta un modo compatto per scrivere una funzione.

Ad esempio, usando il JDK8 per definire la logica di un Comparator, si potrà omettere il return statement e delegare al compilatore di inferire il tipo dei parametri:

// sintassi pre-JDK8
Comparator<Float> cmp = new Comparator<Float>() {
    @Override
    public int compare(Float x, Float y) {
        return (x < y) ? -1 : ((x > y) ? 1 : 0);
    }
};

// sintassi JDK8
Comparator<Float> cmp = (x, y) -> (x < y) ? -1 : ((x > y) ? 1 : 0);

Apparentemente le lambda potrebbero sembrare solo zucchero sintattico, utile sì a risparmiare linee di codice ma senza grandi ripercussioni. Tutt’altro!

Introdurre le lambda expression nel linguaggio, significa dare alla funzione un ruolo di primaria importanza, al pari di quello dell’istanza di una classe. Si potrà usare una funzione come parametro di un altro metodo o come tipo di ritorno. Sarà possibile comporre più funzioni ed agire sui dati in maniera più dichiarativa.

Functional Interface

Nel JDK 8 si è deciso di rappresentare le lambda expression attraverso le functional interface, normali interface con un unico metodo astratto: il functional method. La signature di questo metodo definisce parametri e tipo di ritorno dell’espressione.

Già oggi esistono API che soddisfano questi requisiti, come Comparator e Runnable, ma ne sono state introdotte molte altre per permettere di sfruttare l’espressività del nuovo approccio. Ad esempio:

 Function<T,R> // una operazione che trasforma un oggetto T in R,
               // f(T) -> R

 Predicate<T>  // una operazione che riceve un oggetto T e ritorna true o false,
               // f(T) -> Boolean

 Consumer<T>   // una operazione che lavora su un oggetto di tipo T
               // f(T) -> void

 Supplier<T>   // una operazione che ritorna un oggetto T, senza input
               // f() -> T

La annotation @FunctionalInerface indica al compilatore l’intenzione di utilizzare una interface come lambda (cfr. package java.util.function)

@FunctionalInterface
public interface BiFunction<T,U,R>

// una operazione che riceve in input due oggetti di tipo T e U
// e ritorna un oggetto di tipo R
// f(T,U) -> R

Nel functional method è possibile utilizzare variabili catturate dal contesto, ma solo se sono effettivamente final (assegnate solo una volta):

int lesser = -1;  int greater = 1;  int equal = 0;
Comparator<Integer> cmp = (x, y) -> (x < y) ? lesser : ((x > y) ? greater : equal);

Inoltre, se la signature del metodo prevede un solo parametro, è possibile usare una sintassi alternativa in cui il parametro non viene esplicitato:

myMethod((String param) -> System.out.println(param));

myMethod(System.out::println);  // sintassi alternativa

Nella definizione di una espressione lambda, i tipi dei parametri devono essere tutti esplicitati o tutti omessi. Non è possibile specificarli solo in parte:

// Errore di compilazione: java: <identifier> expected
Comparator<Long> cmp = (x, Long y) -> (x < y) ? -1 : ((x > y) ? 1 : 0);

Stream API

L’uso delle espressioni lambda è molto efficace in combinazione con le nuove Stream API: un modo totalmente nuovo di operare sulle Collection.

Uno stream è come un Iterator infinito, lazy, attraversabile un’unica volta

new Random()
    .ints()     // genera uno stream di interi
    .limit(3)   // stampa solo i primi 3
    .forEach(System.out::println);

// Output =>
//   1428485066
//   1185044285
//   1205067754

Finalmente, anche in Java, sarà facile eseguire operazioni sulle Collection.

Filter, Map, Reduce

Generalmente operare su uno stream significa: recuperarlo da una sorgente

List<Person> people = asList(new Person("Aldo", 15), new Person("Lucia", 19));

Stream<Person> everyBody = people.stream();

filtrarlo con un Predicate,

// filter: solo le persone maggiorenni
Stream<Person> peopleOver18 = everyBody.filter(p -> p.getAge() > 18);

operare sui dati in esso contenuti con delle Function

// map: trasforma le istanze di Person in Student
Stream<Student> students = peopleOver18.map(person -> new Student(person));

ed infine consumarlo recuperando i risultati delle precedenti operazioni

// reduce: colleziono gli studenti ottenuti in una List
List<Student> studentsOver18 =  students.collect(Collectors.toList());

Una cosa importate da notare è che le varie operazioni lasciano immutati i dati iniziali.

Concorrenza

Le modalità di attraversamento dello stream possono essere decise in maniera dichiarativa durante il processo, le API si fanno carico di gestire le complicazioni legate alla concorrenza.

List students = persons.stream()
                .parallel()
                .filter(p -> p.getAge() > 18)   // FILTER in modalità concorrente
                .sequential()
                .map(Student::new)      // MAP e REDUCE in modo sequenziale
                .collect(Collectors.toCollection(ArrayList::new));

Conclusioni

Le novità introdotte in Java 8 iniettano nel linguaggio tutta una serie di concetti legati a linguaggi funzionali e cambieranno radicalmente il linguaggio. La speranza è che gli sviluppatori capiscano presto il valore di questi cambiamenti ed comincino ad utilizzare presto le nuove API.

Download

Su Github è disponibile del codice sorgente con cui fare qualche esperimento. Per compilare è necessario avere installato la preview del JDK8.

Riferimenti

derived from