mer 18 ottobre 2017 - Lo Sviluppatore  anno III

Java 8 : Introduzione alle Lambda expressions

Condividi

Le espressioni lambda sono una nuova e importante funzionalità inclusa in Java SE 8 che sarà ufficialmente fuori a marzo – Se avete programmato in maniera funzionale o se si ha familiarità con l’idea di chiusure (clousures), le espressioni lambda non vi appariranno come niente di nuovo.

In matematica e informatica in generale, un’espressione lambda è una funzione. In Java, un’espressione lambda fornisce un modo per creare una funzione anonima,  introducendo di fatto un nuovo tipo Java: il tipo funzione anonima che può quindi essere passato come argomento o restituito in uscita nei metodi, un pò come si fa con gli oggetti  – In poche parole, si tratta di un metodo senza una dichiarazione, una sorta di scorciatoia che consente di scrivere un metodo nello stesso posto dove ti serve.

Le lambda expressions sono particolarmente utili nei casi in cui serve definire una breve funzione che ha poche linee di codice e che verrà utilizzata una sola volta: In questi casi si risparmia la fatica di scrivere un metodo a parte con modificatore, nome, ecc. In generale le lambda ci permettono di scivere codice più chiaro e meno verboso.

Ma prima di passare al dettaglio sulle lambda expression è necessario ricordare alcuni concetti del linguaggio Java, già presenti nelle versioni precedenti della piattaforma, che ci aiutano a comprendere meglio  le espressioni lambda. Parliamo delle  classi anonime interne (anonymous inner classes) e le interfacce funzionali (functional interfaces).

le classi anonime interne

In Java, le classi anonime interne  forniscono un modo per implementare classi che vengono utilizzate una sola volta in un’applicazione. Un tipico uso dei queste classi lo possiamo vedere in un’applicazione con interfaccia basata su swing o un’applicazione JavaFX in cui è richiesto un numero di gestori di eventi (Event Handler) per gli eventi di tastiera e mouse. Piuttosto che scrivere una classe di gestione degli eventi separata per ogni evento, è possibile scrivere qualcosa del genere:

JButton testButton = new JButton("Test Button");
 testButton.addActionListener(new ActionListener(){
        @Override 
public void actionPerformed(ActionEvent ae){
                System.out.println("Test Button clicked");
          }    
});

Se non si facesse così sarebbe necessario scrivere n classi che implementano ActionListener, una per ogni evento da gestire. In questo modo invece, definendo la classe in loco, dove serve, il codice risulta più facile da leggere anche se perde un pò in eleganza perchè va comunque scritto un pò di codice solo per definire un unico metodo.

Le interfacce funzionali

Le interfacce come ActionListener, la cui definizione è presente sotto, vengono chiamate in Java 8, interfacce funzionali (functional interface) e sono caratterizzate dalla presenza di un solo metodo. L’utilizzo di interfacce funzionali con le classi anonime interne  sono un modello comune in Java. Oltre alle classi EventListener, interfacce come Runnable, Comparator o FileFilter sono da considerarsi in modo simile. Le interfacce funzionali sono sfruttate per l’utilizzo con le espressioni lambda.

package java.awt.event;
import java.util.EventListener;

   public interface ActionListener extends EventListener {
       public void actionPerformed(ActionEvent e);
    }

Le Espressioni Lambda

Un’espressione lambda è come un metodo, fornisce un elenco di parametri formali e un corpo (che può essere un’espressione o un blocco di codice). Le espressioni lambda risolvono il problema della verbosità delle classi interne permettendo una riduzione delle linee di codice da scrivere.
La sintassi generale è la seguente:

(Lista degli argomenti) -> Espressione

oppure 

(Lista degli argomenti)->{ istruzioni; }

Esempi:

// espressione che prende in input due interi e restituisce la somma
(int x, int y) -> x + y 

// espressione che prende in input una stringa e restituisce la sua lunghezza
s -> s.length() 

// espressione senza argomenti che restituisce il valore 50
() -> 50

// espressione che prende in input una stringa e non restituisce nulla
(String s) -> { System.out.println("Benvenuto ");
                System.out.println(s); }

Le istruzioni di break e continue non si possono usare all’interno del blocco anche se sono permessi all’interno di cicli. Se il corpo produce un risultato, ogni possibile ramo del flusso del codice deve restituire qualcosa o lanciare un’eccezione.

Nota: in un’espressione lambda è possibile omettere il tipo dei parametri. Inoltre, è possibile omettere le parentesi se c’è solo un parametro.

Ora che abbiamo definito la sintassi e visto qualche semplice esempio vediamo di utilizzare le lambda nel caso delle interfacce funzionali viste sopra:

Tutto più semplice con le lambda

public class RunnableTest {
public static void main(String[] args) {

     System.out.println("=== RunnableTest ===");

    // Anonymous Runnable
    Runnable r1 = new Runnable(){

      @Override
      public void run(){
        System.out.println("Hello world old style!");
      }
    };

   // Lambda Runnable
   Runnable r2 = () -> System.out.println("Hello world with Lambda!");

   r1.run();
   r2.run();   
 }
}

E’ abbastanza evidente come con l’suo delle lambda abbiamo ridotto un pezzo di codice da 5 linee a 1 soltanto. Lo stesso vale per il caso degli ActiontListener visto prima :

public class ListenerTest {

   public static void main(String[] args) {

   // Anonymous ActionListener
   JButton testButton = new JButton("Test Button");
    testButton.addActionListener(new ActionListener(){
    @Override public void actionPerformed(ActionEvent ae){
        System.out.println("Click Detected by Anon Class");
      }
    });

    // Lambda ActionListener
    testButton.addActionListener(e -> System.out.println("Click Detected by Lambda Listner"));

    // Swing stuff
    JFrame frame = new JFrame("Listener Test");
    . . . 
  }
 }

Da notare in questo ultimo caso come la lambda expression sia passata come argomento di un metodo.
Proseguiamo con gli esempi per mostrare in che modo le lambda riescono a semplificare il codice che scriviamo rendendolo più leggibile.

Un tipico caso d’uso

Un tipo caso d’uso è quello di filtrare gli elementi di una Collection in base a dei criteri.
Consideriamo il seguente scenario:

Supponiamo di avere un social network italiano dove vengono inviati agli iscritti dei messaggi pubblicitari mirati in base a determinate caratteristiche dei soggetti e supponiamo di essere interessati ad inviare tre messaggi differenti: uno indirizzato alle giovani donne (persone di sesso femminile di età compresa tra i 18 e i 29 anni), uno indirizzato a tutti gli iscritti di sesso maschile e uno indirizzato ai soli utenti di nazionalità estera. Vediamo come potremmo procedere per implementare questo tipo di scenario.

Modelliamo le nostre entity, gli iscritti, con la seguente classe:

public class Persona {

    private String nome;
    private String cognome;
    private String sesso="";
    private int eta;
    private String nazionalita="";

    public Persona(String nome, String cognome, String sesso, int eta, String nazionalita) {
        this.nome = nome;
        this.cognome = cognome;
        this.sesso = sesso;
        this.eta = eta;
        this.nazionalita = nazionalita;
    }

    // getter e setter
        ...........
}

Primo approccio: creare tre metodi specifici di ricerca

package losviluppatore.lambda;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MessageSender1 {

    List<Persona> iscritti = Arrays.asList(
       new Persona("Mario", "Rossi", "M", 35, "italiana"),
       new Persona("Lucy", "Parker", "F", 22, "inglese"),
       new Persona("Gianni", "Bianchi", "M", 20, "italiana"),
       new Persona("Fabio", "Marchi", "M", 40, "italiana"),
       new Persona("John", "Simpson", "M", 18, "USA"),
       new Persona("Adele", "Fabi", "F", 20, "italiana")
    );

     public List<Persona> getGiovaniDonne(){
      List<Persona> persone = new ArrayList<Persona>();
      for (Persona p:iscritti)
          if (p.getSesso.equals("F") && p.getEta() > 17 && p.getEta() < 30)
              persone.add(p);

      return persone;
    }

    public List<Persona> getMaschi(){
      List<Persona> persone = new ArrayList<Persona>();
      for (Persona p:iscritti)
          if (p.getSesso().equals("M"))
              persone.add(p);

      return persone;
    }

     public List<Persona> getStranieri(){
      List<Persona> persone = new ArrayList<Persona>();
      for (Persona p:iscritti)
          if (!p.getNazionalita().equals("italiana"))
              persone.add(p);

      return persone;
    }

     public void sendMessage(String msg, List<Persona> persone){
       // Logica di Invio messaggio
       // .........

       System.out.println("Inviato messaggio a "+persone.size()+" iscritti");
     }

    public static void main(String[] args) {
      MessageSender1 ms = new MessageSender1(); 
      ms.sendMessage("messaggioX", ms.getGiovaniDonne());
      ms.sendMessage("messaggioY", ms.getMaschi());
      ms.sendMessage("messaggioZ", ms.getStranieri());
    } 

}

Questo approccio è abbastanza chiaro visto che i nomi dei metodi descrivono esattamente la loro funzione e il criterio di ricerca è espresso chiaramente all’interno. ma presenta degli aspetti negativi:

  • Il principio DRY (Don’t Repeat Yourself) non è rispettato :
    • Ogni metodo ripete un meccanismo di loop.
    • I criteri di ricerca devono essere riscritti per ciascun metodo
  • Va scritto un metodo per ogni criterio di ricerca che si vuole implementare
  • Il codice non è molto flessibile. Se i criteri di ricerca cambiano, sarebbe necessaria una serie di modifiche un pò in tutto il codice. Così, il codice non è molto manutenibile.

Secondo approccio : miglioriamo facendo un refactoring dei metodi

package losviluppatore.lambda;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MessageSender2 {

    List<Persona> iscritti = Arrays.asList(
       new Persona("Mario", "Rossi", "M", 35, "italiana"),
       new Persona("Lucy", "Parker", "F", 22, "inglese"),
       new Persona("Gianni", "Bianchi", "M", 20, "italiana"),
       new Persona("Fabio", "Marchi", "M", 40, "italiana"),
       new Persona("John", "Simpson", "M", 18, "USA"),
       new Persona("Adele", "Fabi", "F", 20, "italiana")
    );

     public List<Persona> getGiovaniDonne(){
      List<Persona> persone = new ArrayList<Persona>();
      for (Persona p:iscritti)
          if (isGiovaneDonna(p))
              persone.add(p);

      return persone;
    }

    public List<Persona> getMaschi(){
      List<Persona> persone = new ArrayList<Persona>();
      for (Persona p:iscritti)
          if (isMaschio(p))
              persone.add(p);

      return persone;
    }

     public List<Persona> getStranieri(){
      List<Persona> persone = new ArrayList<Persona>();
      for (Persona p:iscritti)
          if (isStraniero(p))
              persone.add(p);

      return persone;
    }

    public boolean isGiovaneDonna(Persona p){
      return p.getSesso.equals("F") && p.getEta() > 17 && p.getEta() < 30;  
    } 

    public boolean isMaschio(Persona p){
      return p.getSesso().equals("M");  
    } 

    public boolean isStraniero(Persona p){
      return !p.getNazionalita().equals("italiana");  
    } 

    public void sendMessage(String msg, List<Persona> persone){
       // Logica di Invio messaggio
       // .........

       System.out.println("Inviato messaggio a "+persone.size()+" iscritti");
     }

    public static void main(String[] args) {
      MessageSender2 ms = new MessageSender2(); 
      ms.sendMessage("messaggioX", ms.getGiovaniDonne());
      ms.sendMessage("messaggioY", ms.getMaschi());
      ms.sendMessage("messaggioZ", ms.getStranieri());
    } 

}

In quest’ultima soluzione troviamo i criteri di ricerca  incapsulati in metodi, e abbiamo già un primo vantaggio che sta nel fatto di poter riutilizzare tali condizioni altrove, inoltre eventuali modifiche di una condizione (ad esempio modificare il limite superiore di quelle persone da considerarsi come “giovani donne” a 25 anni anzichè 29) permettono di fare la modifica in un solo metodo e l’effetto si propaga a tutte le classi che lo utilizzano. Tuttavia, il codice è aumentato, c’è ancora un sacco di codice ripetuto ed è sempre necessario un metodo per ogni caso d’uso. C’è invece un modo migliore per passare i criteri di ricerca ai metodi

Terzo approccio: le classi anonime

Vediamo adesso la soluzione mediante l’uso delle “vecchie” classi anonime, utilizzandole in maniera congiunta ad un interfaccia funzionale (ITest.java)  con un unico metodo test che restituisce un boolean. Qui i criteri di ricerca possono essere passati quando il metodo viene chiamato. L’interfaccia ITest può essere una cosa simile:

public interface ITest<T> {
    public boolean test(T t);   
}

E la nuova implementazione è la seguente:

package losviluppatore.lambda;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MessageSender3 {

    List<Persona> iscritti = Arrays.asList(
       new Persona("Mario", "Rossi", "M", 35, "italiana"),
       new Persona("Lucy", "Parker", "F", 22, "inglese"),
       new Persona("Gianni", "Bianchi", "M", 20, "italiana"),
       new Persona("Fabio", "Marchi", "M", 40, "italiana"),
       new Persona("John", "Simpson", "M", 18, "USA"),
       new Persona("Adele", "Fabi", "F", 20, "italiana")
    );

     public List<Persona> getIscrittiFiltratiPer(ITest<Persona> aTest){
      List<Persona> persone = new ArrayList<Persona>();
      for (Persona p:iscritti)
          if (aTest.test(p))
              persone.add(p);

      return persone;
    }

    public void sendMessage(String msg, List<Persona> persone){
       // Logica di Invio messaggio
       // .........

       System.out.println("Inviato messaggio a "+persone.size()+" iscritti");
     }

    public static void main(String[] args) {
      MessageSender3 ms = new MessageSender3(); 

      // ------------ invio messaggio per giovani donne ---------------
      ms.sendMessage("messaggioX", ms.getIscrittiFiltratiPer(
       new ITest<Persona>(){
          @Override
          public boolean test(Persona p){
            return p.getSesso().equals("F") && p.getEta() > 17 && p.getEta() < 30;
          }
        }
      ));

      // ------------ invio messaggio per iscritti maschi ---------------
      ms.sendMessage("messaggioY", ms.getIscrittiFiltratiPer(
       new ITest<Persona>(){
          @Override
          public boolean test(Persona p){
            return p.getSesso().equals("M");
          }
        }
      ));

      // ------------ invio messaggio per iscritti stranieri ---------------
      ms.sendMessage("messaggioZ", ms.getIscrittiFiltratiPer(
       new ITest<Persona>(){
          @Override
          public boolean test(Persona p){
             return !p.getNazionalita().equals("italiana");  
          }
        }
      ));
    } 
}

Questo è sicuramente un altro miglioramento, Passando il criterio di ricerca come argomento è possibile creare un solo metodo di ricerca più generale. Tuttavia, c’è un piccolo problema di leggibilità del codice quando questo metodo deve essere chiamato, cosa abbastanza evidente nel metodo main della nostra classe che è diventato molto più verboso.

Quarto approccio: usiamo le lambada expressions

Con l’uso delle lambda risolviamo tutti i problemi visti finora. Ma prima di passare all’esempio è necessario fare una precisazione che ci permette anche di introdurre una nuova feature presente in Java 8. Nell’esempio precedente, la scrittura dell’interfaccia funzionale ITest passata al metodo delle classi anonime non era necessario in quanto Java SE 8 fornisce il package java.util.function che contiene un certo numero di interfacce funzionali standard. Rimandando ad altra occasione l’approfondimento di queste interfacce funzionali, nel nostro caso possiamo sicuramente dire che l’interfaccia Predicate soddisfa le nostre esigenze.

public interface Predicate<T> {
   public boolean test(T t);
}

Il metodo test prende in input un tipo generico e restituisce un risultato booleano che è proprio quello che fa al caso nostro e che ci permette di  effettuare le selezioni.

Infine ecco la versione finale del nostro esempio:

 

package losviluppatore.lambda;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MessageSender4 {

    List<Persona> iscritti = Arrays.asList(
       new Persona("Mario", "Rossi", "M", 35, "italiana"),
       new Persona("Lucy", "Parker", "F", 22, "inglese"),
       new Persona("Gianni", "Bianchi", "M", 20, "italiana"),
       new Persona("Fabio", "Marchi", "M", 40, "italiana"),
       new Persona("John", "Simpson", "M", 18, "USA"),
       new Persona("Adele", "Fabi", "F", 20, "italiana")
    );

     public List<Persona> getIscrittiFiltratiPer(Predicate<Persona> pred){
      List<Persona> persone = new ArrayList<Persona>();
      for (Persona p:iscritti)
          if (pred.test(p))
              persone.add(p);

      return persone;
    }

    public void sendMessage(String msg, List<Persona> persone){
       // Logica di Invio messaggio
       // .........

       System.out.println("Inviato messaggio a "+persone.size()+" iscritti");
     }

    public static void main(String[] args) {
      MessageSender4 ms = new MessageSender4();

      // Predicates
      Predicate<Persona> allGiovaniDonne = p -> p.getSesso().equals("F") && p.getEta() > 17 && p.getEta() < 30;
      Predicate<Persona> allMaschi = p -> p.getSesso().equals("M");
      Predicate<Persona> allStranieri = p -> !p.getNazionalita().equals("italiana");

      // ------------ invio messaggio per giovani donne ---------------
      ms.sendMessage("messaggioX", ms.getIscrittiFiltratiPer(allGiovaniDonne));

      // ------------ invio messaggio per iscritti maschi ---------------
      ms.sendMessage("messaggioY", ms.getIscrittiFiltratiPer(allMaschi));

      // ------------ invio messaggio per iscritti stranieri ---------------
      ms.sendMessage("messaggioZ", ms.getIscrittiFiltratiPer(allStranieri));
    } 

}

Si noti che viene creato un Predicate per ciascun gruppo di iscitti:  allGiovaniDonne, allMaschi e allStranieri, inoltre è semplice poter creare nuovi predicati da passare al metodo di ricerca. Qui per questioni di chiarezza si è preferito usare i Predicate ma un ulteriore semplificazione sarebbe stata possibile passando le definizioni dei predicati, cioè le lambda expressions, direttamente al metodo di ricerca. Giusto un caso per esempio:

// ------------ invio messaggio per iscritti stranieri ---------------
ms.sendMessage("messaggioZ", ms.getIscrittiFiltratiPer(p -> !p.getNazionalita().equals("italiana")));

In questo approccio con l’uso delle lambda expression il codice risulta compatto, facile da leggere, e non è ripetitivo.

Conclusioni

Ci sarebbe sicuramente altro da dire sulle lambda expression, come ad esempio i nuovi modi di iterazione e filtraggio delle classi Collection, ma per adesso direi ci fermiamo qui 😉 L’obbiettivo di questo post era solo quello di introdurre questa nuova feature di Java SE 8 e fare vedere come l’uso delle lambda ci permette di scrivere codice più elegante e meno verboso. Spero di esser riuscito nell’intento.  Alla prossima!

Riferimenti e Risorse

Lambda Expressions

Java SE 8: Lambda Quick Start

Java 8 in Action

JDK 8 Project

 

5 thoughts on “Java 8 : Introduzione alle Lambda expressions

  1. Veramente non capisco perché sono espressioni “potenti”.
    Fanno solo in modo più conciso ciò che si poteva già fare prima, o non ho capito bene io?

    1. Ciao

      beh, la potenzialità di scrivere codice più conciso è già un vantaggio, ma in vantaggi sono anche altri,
      per dirne una, basti pensare alle operazioni di ricerca/filtraggio sulle Collection che con le lambda diventano molto più semplici,
      consentendo al programmatore di concentrasi sul business principale e non sull’implementazione dei suddetti algoritmi.

      G.

Lascia un commento

Top