Il Java che verrà: una breve introduzione alle espressioni lambda

Il Progetto Lambda ha lo scopo di portare in Java le espressioni lambda ed è il “piatto forte” delle novità che si vedranno in Java 8. La nuova release del linguaggio è programmata per settembre 2013 ma già entro Gennaio le funzionalità saranno congelate in vista della Developer Preview. Diamo quindi un assaggio anticipato di quello che ci aspetta anche perché, con questa modifica, il linguaggio cambierà come non faceva dai tempi dei Generics (introdotti in J2SE 5.0, nel lontano 2004!)

Cosa sono le lambda expression di Java

In prima approssimazione, possiamo dire che le espressioni lambda sono un tipo di metodo anonimo con una sintassi compatta che permette di omettere i modificatori, il tipo di ritorno e in alcuni casi anche i tipi dei parametri di ingresso. Per far prima e farsi comprendere da chi usa altri linguaggi, qualcuno potrebbe dire più semplicemente che sono delegati (.NET), puntatori a funzione (C++) o closures.

Iniziamo quindi a scrivere un po’ di lambda expression:

(File f) -> f.isDirectory();
event -> {ui.showSomething();}
(String s) -> {System.out.println(s);}
(x, y) -> x - y; 

e vediamo l’esempio della prima riga in azione. Vogliamo produrre la lista di sottodirectory di una data directory. Prima delle lambda expression un modo1 per produrre il risultato voluto poteva essere il seguente:

File dir = new File("c:\\");

FileFilter directoryFilter = new FileFilter() {
  public boolean accept(File file) {
    return file.isDirectory();
  }
};

File[] directories = dir.listFiles(directoryFilter);

Con le lambda expression invece si può scrivere direttamente:

File dir = new File("c:\\");
File[] directories = dir.listFiles(f -> f.isDirectory());

Cosa succede dietro le quinte? Il compilatore sa che il metodo listFiles ha come unico parametro un FileFilter. Questa classe ha il solo metodo accept, di conseguenza la lambda expression deve esserne la sua implementazione. Il metodo accept ha come parametro di ingresso la classe File, quindi f deve essere di tipo File. Dato che isDirectory ritorna un boolean così come il metodo accept, si può dire che tutto il giro torna e che la lambda expression è esattamente l’implementazione del metodo accept di un FileFilter.

Proprio perché il compilatore è in grado di inferire i tipi, l’espressione si può ulteriormente semplificare passando da (File f) -> f.isDirectory() dell’inizio a f -> f.isDirectory(). Il nuovo compilatore è in grado di dedurre le informazioni mancanti nella lambda expression esaminando il contesto. Questa caratteristica non è però una novità di Java 8, già con il diamond operator Java 7 ha infatti iniziato a “riempire” le parti “omesse” dal programmatore.

Il compilatore è quindi responsabile di riconoscere il tipo di ogni lambda expression e lo fa analizzando il contesto in cui l’espressione appare; questo tipo è chiamato target type. Nell’esempio di prima, i target type sono stati FileFilter e File. Chiaramente non tutte le lambda expression vanno bene per tutti i target type; chiamando T un potenziale target type e L la lambda expression devono valere le seguenti condizioni:

  • T è una interfaccia (o una classe astratta) che ha solamente un metodo (T viene detto anche functional interface type);
  • L ha esattamente lo stesso numero di parametri del metodo di T e i tipi coincidono;
  • ogni espressione ritornata nel corpo di L è compatibile con il tipo ritornato del metodo di T;
  • ogni eccezione lanciata da L è tra quelle che il metodo può lanciare.

La lambda expression sono un altro passo verso la riduzione della tanto contestata verbosità di Java: permettono di evitare tutte le righe di codice boilerplate dei functional interface type e condensano elegantemente il sorgente alla sua essenza. Sono diversi i punti del JDK dove è possibile trarre vantaggio da questa nuova caratteristica del linguaggio, tra cui:

  • java.lang.Runnable;
  • java.util.concurrent.Callable;
  • java.security.PrivilegedAction;
  • java.util.Comparator;
  • java.io.FileFilter;
  • java.nio.file.PathMatcher;
  • java.lang.reflect.InvocationHandler;
  • java.beans.PropertyChangeListener;
  • java.awt.event.ActionListener;
  • javax.swing.event.ChangeListener.

Iterazione: da esterna a interna

Anche se la cosa che salta di più all’occhio è la riduzione del numero di righe da scrivere, le lambda expression non servono solo a questo. In Java 8 cambierà il modo con cui si lavora con le collezioni permettendo di passare dall’iterazione esterna all’iterazione interna proprio grazie alle lambda. Cosa vuol dire esattamente questo passaggio? Al posto di scrivere esplicitamente dei cicli for per compiere delle operazioni su ogni elemento di una collezione, scriveremo una lambda expression che, in accoppiata con nuove classi del JDK, faranno il lavoro richiesto sull’intera collezione dandoci direttamente il risultato finale.

Prima di rendere più chiaro quando detto, elenchiamo le principali operazioni che generalmente si svolgono su di una collezione:
  • filtrare, cioè produrre una nuova collezione decidendo per ogni elemento se ne farà parte o meno;
  • applicare una trasformazione ad ogni elemento della collezione, ad esempio moltiplicare una lista di numeri per un fattore;
  • ridurre la collezione ad un singolo valore come una somma o una media.

  • Facciamo, quindi, un esempio di codice che filtra un array di interi e li stampa in console secondo quanto è possibile fare oggi:

    List<Integer> myList = Arrays.asList(3,5,4,23,5,12,14,1,45);
    for(Integer i : myList){
        if (i < 10)
            System.out.println(i);
    }
    

    In Java 8, invece, usando le nuove interfacce Predicate, Block e Stream si può scrivere il seguente codice:

    Predicate<Integer> p = i -> i < 10;
    Block<Integer> b = i -> {System.out.println(i);};
    myList.stream().filter(p).forEach(b);
    

    Uno Stream rappresenta una sequenza potenzialmente infinita di elementi che vengono consumati in sequenza o da un’operazione. Il suo metodo filter restituisce un nuovo Stream filtrato in base al risultato dell’implementazione di un Predicate. Questa interfaccia ha il semplice compito di esporre un metodo test che restituisce true o false a seconda che una verifica sia passata o meno. Ad ogni elemento dello stream risultante, viene infine applicata l’operazione definita nel metodo apply dell’interfaccia Block.

    Presa la giusta confidenza con le lambda expression e con le nuove interfacce in java.utils.functions e java.utils.streams, il codice può essere scritto in maniera ancora più naturale e compatta:

    myList.stream().filter(i -> i < 10)
                   .forEach(i -> {System.out.println(i);});
    

    Gran parte dei metodi di Stream restituiscono a loro volta uno Stream e quindi è possibile combinarli in pipeline per ottenere velocemente dei risultati altrimenti più complicati da raggiungere. Il codice sotto ad esempio restituisce la lista di partenza senza elementi ripetuti, ordinandola2 in ordine decrescente, scartando gli elementi dispari e limitandola ai primi due elementi.

    myList.stream().uniqueElements()
                   .filter(i -> i % 2 == 0)
    			   .sorted((i,j) -> {if (i == j) return 0; 
    			                     if (i > j)  return -1; 
    			                     return 1;})
    			   .limit(2)
                   .forEach(i -> {System.out.println(i);});
    

    Come detto inizialmente, non è solo una questione di compattezza del codice; lasciando allo Stream il compito di eseguire l’operazione sugli elementi, è possibile che l’operazione venga parallelizzata sugli elementi portando ad una maggiore efficienza in esecuzione. Nel codice precedente, ad esempio, l’operazione di filtro non richiede esplicitamente un’esecuzione sequenziale (cosa che invece è obbligatoria se siamo noi a scrivere un ciclo for) e quindi sfruttando il multithreading del processore può essere svolta in parallelo su più thread con ovvi benefici sul tempo di risposta.

    Esempi come quelli proposti non sono nuovi sulle pagine di CoseNonJaviste. Nel post introduttivo di Manuele su Scala abbiamo visto che questo linguaggio permette la programmazione funzionale, anzi che le lambda expression ne sono una parte fondamentale. Anche se un po’ in ritardo, con la versione 8, Java entra nell’ambito dei linguaggi funzionali e sarà inevitabilmente confrontato con Scala (e non solo) nei mesi a venire.

    Java 8? Non finisce qui

    Ci sono diverse altre cose da dire sulle lambda expression e tuttavia queste non sono l’unica novità che ci aspetta in Java 8. La nuova versione del linguaggio introdurrà altre novità sintattiche come i riferimenti ai metodi (espressioni tipo String::length) e i metodi di default per le interfacce. Affrontare anche questi argomenti adesso sarebbe mettere troppa carne al fuoco per un solo post e, in fin dei conti, c’è tutto il tempo per familiarizzare con questi concetti prima che diventino d’uso quotidiano. Scopo di questo articolo è stato, invece, quello di introdurre l’argomento senza pretesa di affrontarlo in tutta la sua complessità e di suscitare la curiosità sulle novità a venire. Tuttavia, per coloro che vogliono approfondire già da subito senza aspettare i prossimi post di CoseNonJaviste, consiglio i link sottostanti:

Edit

Successivamente a questo post, abbiamo parlato di Java 8 in:

1In questo esempio si è voluto utilizzare il package java.io al posto del più recente java.nio.

2L’ordinamento inverso può essere scritto più facilmente moltiplicando per -1 il risultato del metodo compare. E’ voluto l’esempio con più righe per mostrare che la lambda expression può comprendere più righe purché racchiuse in parentesi graffe. Le parentesi servono anche quando il tipo restituito dall’espressione è void come nel caso della stampa su console.

Giampaolo Trapasso

Sono laureato in Informatica e attualmente lavoro come Software Engineer in Databiz Srl. Mi diverto a programmare usando Java e Scala, Akka, RxJava e Cassandra. Qui mio modesto contributo su StackOverflow e il mio account su GitHub