CodeGym /Java Blog /Random-IT /Una spiegazione delle espressioni lambda in Java. Con ese...
John Squirrels
Livello 41
San Francisco

Una spiegazione delle espressioni lambda in Java. Con esempi e compiti. Parte 2

Pubblicato nel gruppo Random-IT
Per chi è questo articolo?
  • È per le persone che leggono la prima parte di questo articolo;
  • È per le persone che pensano di conoscere già bene Java Core, ma non hanno idea delle espressioni lambda in Java. O forse hanno sentito qualcosa sulle espressioni lambda, ma mancano i dettagli.
  • È per le persone che hanno una certa comprensione delle espressioni lambda, ma ne sono ancora scoraggiate e non sono abituate a usarle.
Se non rientri in una di queste categorie, potresti trovare questo articolo noioso, imperfetto o generalmente non adatto a te. In questo caso, sentiti libero di passare ad altre cose o, se sei esperto in materia, per favore dai suggerimenti nei commenti su come potrei migliorare o integrare l'articolo. Una spiegazione delle espressioni lambda in Java.  Con esempi e compiti.  Parte 2 - 1Il materiale non pretende di avere alcun valore accademico, figuriamoci novità. Al contrario: cercherò di descrivere le cose complesse (per alcune persone) nel modo più semplice possibile. Una richiesta per spiegare l'API Stream mi ha ispirato a scrivere questo. Ci ho pensato e ho deciso che alcuni dei miei esempi di stream sarebbero stati incomprensibili senza una comprensione delle espressioni lambda. Quindi inizieremo con le espressioni lambda.

Accesso a variabili esterne

Questo codice viene compilato con una classe anonima?

int counter = 0;
Runnable r = new Runnable() { 

    @Override 
    public void run() { 
        counter++;
    }
};
No. La counter variabile deve essere final. O se no final, almeno non può cambiare il suo valore. Lo stesso principio si applica alle espressioni lambda. Possono accedere a tutte le variabili che possono "vedere" dal luogo in cui sono dichiarate. Ma un lambda non deve cambiarli (assegnare loro un nuovo valore). Tuttavia, esiste un modo per aggirare questa restrizione nelle classi anonime. Basta creare una variabile di riferimento e modificare lo stato interno dell'oggetto. In tal modo, la variabile stessa non cambia (punta allo stesso oggetto) e può essere tranquillamente contrassegnata come final.

final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() { 

    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Qui la nostra countervariabile è un riferimento a un AtomicIntegeroggetto. E il incrementAndGet()metodo viene utilizzato per modificare lo stato di questo oggetto. Il valore della variabile stessa non cambia mentre il programma è in esecuzione. Punta sempre allo stesso oggetto, che ci permette di dichiarare la variabile con la parola chiave final. Ecco gli stessi esempi, ma con espressioni lambda:

int counter = 0;
Runnable r = () -> counter++;
Questo non verrà compilato per lo stesso motivo della versione con una classe anonima:  counternon deve cambiare mentre il programma è in esecuzione. Ma va tutto bene se lo facciamo in questo modo:

final AtomicInteger counter = new AtomicInteger(0); 
Runnable r = () -> counter.incrementAndGet();
Questo vale anche per i metodi di chiamata. All'interno delle espressioni lambda, non solo puoi accedere a tutte le variabili "visibili", ma anche chiamare qualsiasi metodo accessibile.

public class Main { 

    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    } 

    private static void staticMethod() { 

        System.out.println("I'm staticMethod(), and someone just called me!");
    }
}
Sebbene staticMethod()sia privato, è accessibile all'interno del main()metodo, quindi può anche essere chiamato dall'interno di un lambda creato nel mainmetodo.

Quando viene eseguita un'espressione lambda?

Potresti trovare la seguente domanda troppo semplice, ma dovresti farla lo stesso: quando verrà eseguito il codice all'interno dell'espressione lambda? Quando viene creato? O quando viene chiamato (che non è ancora noto)? Questo è abbastanza facile da controllare.

System.out.println("Program start"); 

// All sorts of code here
// ...

System.out.println("Before lambda declaration");

Runnable runnable = () -> System.out.println("I'm a lambda!");

System.out.println("After lambda declaration"); 

// All sorts of other code here
// ...

System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start(); 
Uscita sullo schermo:

Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
Puoi vedere che l'espressione lambda è stata eseguita alla fine, dopo che il thread è stato creato e solo quando l'esecuzione del programma raggiunge il run()metodo. Certamente non quando viene dichiarato. Dichiarando un'espressione lambda, abbiamo solo creato un Runnableoggetto e descritto come run()si comporta il suo metodo. Il metodo stesso viene eseguito molto più tardi.

Riferimenti di metodo?

I riferimenti ai metodi non sono direttamente correlati ai lambda, ma penso che abbia senso spendere qualche parola su di loro in questo articolo. Supponiamo di avere un'espressione lambda che non fa nulla di speciale, ma chiama semplicemente un metodo.

x -> System.out.println(x)
Riceve alcune xe giuste chiamate System.out.println(), di passaggio x. In questo caso, possiamo sostituirlo con un riferimento al metodo desiderato. Come questo:

System.out::println
Esatto, nessuna parentesi alla fine! Ecco un esempio più completo:

List<String> strings = new LinkedList<>(); 

strings.add("Dota"); 
strings.add("GTA5"); 
strings.add("Halo"); 

strings.forEach(x -> System.out.println(x));
Nell'ultima riga usiamo il forEach()metodo, che accetta un oggetto che implementa l' Consumerinterfaccia. Ancora una volta, questa è un'interfaccia funzionale, con un solo void accept(T t)metodo. Di conseguenza, scriviamo un'espressione lambda che ha un parametro (poiché è digitato nell'interfaccia stessa, non specifichiamo il tipo di parametro; indichiamo solo che lo chiameremo x). Nel corpo dell'espressione lambda, scriviamo il codice che verrà eseguito quando accept()verrà chiamato il metodo. Qui mostriamo semplicemente ciò che è finito nella xvariabile. Questo stesso forEach()metodo scorre tutti gli elementi nella raccolta e chiama il accept()metodo sull'implementazione delConsumerinterface (la nostra lambda), passando in ogni elemento della collezione. Come ho detto, possiamo sostituire una tale espressione lambda (quella che classifica semplicemente un metodo diverso) con un riferimento al metodo desiderato. Quindi il nostro codice sarà simile a questo:

List<String> strings = new LinkedList<>(); 

strings.add("Dota"); 
strings.add("GTA5"); 
strings.add("Halo");

strings.forEach(System.out::println);
L'importante è che i parametri dei metodi println()e accept()corrispondano. Poiché il println()metodo può accettare qualsiasi cosa (è sottoposto a overload per tutti i tipi primitivi e tutti gli oggetti), invece delle espressioni lambda, possiamo semplicemente passare un riferimento al println()metodo a forEach(). Quindi forEach()prenderà ogni elemento nella raccolta e lo passerà direttamente al println()metodo. Per chiunque lo incontri per la prima volta, tieni presente che non stiamo chiamando System.out.println()(con punti tra le parole e con parentesi alla fine). Invece, stiamo passando un riferimento a questo metodo. Se scriviamo questo

strings.forEach(System.out.println());
avremo un errore di compilazione. Prima della chiamata a forEach(), Java vede che System.out.println()viene chiamato, quindi capisce che il valore restituito è voide proverà a passare voida forEach(), che invece si aspetta un Consumeroggetto.

Sintassi per i riferimenti ai metodi

È abbastanza semplice:
  1. Passiamo un riferimento a un metodo statico come questo:ClassName::staticMethodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<String> strings = new LinkedList<>(); 
            strings.add("Dota"); 
            strings.add("GTA5"); 
            strings.add("Halo"); 
    
            strings.forEach(Main::staticMethod); 
        } 
    
        private static void staticMethod(String s) { 
    
            // Do something 
        } 
    }
    
  2. Passiamo un riferimento a un metodo non statico utilizzando un oggetto esistente, come questo:objectName::instanceMethodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<String> strings = new LinkedList<>();
            strings.add("Dota"); 
            strings.add("GTA5"); 
            strings.add("Halo"); 
    
            Main instance = new Main(); 
            strings.forEach(instance::nonStaticMethod); 
        } 
    
        private void nonStaticMethod(String s) { 
    
            // Do something 
        } 
    }
    
  3. Passiamo un riferimento a un metodo non statico utilizzando la classe che lo implementa come segue:ClassName::methodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<User> users = new LinkedList<>(); 
            users.add (new User("John")); 
            users.add(new User("Paul")); 
            users.add(new User("George")); 
    
            users.forEach(User::print); 
        } 
    
        private static class User { 
            private String name; 
    
            private User(String name) { 
                this.name = name; 
            } 
    
            private void print() { 
                System.out.println(name); 
            } 
        } 
    }
    
  4. Passiamo un riferimento a un costruttore come questo:ClassName::new

    I riferimenti ai metodi sono molto convenienti quando hai già un metodo che funzionerebbe perfettamente come callback. In questo caso, invece di scrivere un'espressione lambda contenente il codice del metodo o scrivere un'espressione lambda che chiama semplicemente il metodo, passiamo semplicemente un riferimento ad esso. E questo è tutto.

Un'interessante distinzione tra classi anonime ed espressioni lambda

In una classe anonima, la thisparola chiave punta a un oggetto della classe anonima. Ma se lo usiamo all'interno di un lambda, otteniamo l'accesso all'oggetto della classe che lo contiene. Quello in cui abbiamo effettivamente scritto l'espressione lambda. Ciò accade perché le espressioni lambda sono compilate in un metodo privato della classe in cui sono scritte. Non consiglierei di utilizzare questa "caratteristica", poiché ha un effetto collaterale e contraddice i principi della programmazione funzionale. Detto questo, questo approccio è del tutto coerente con OOP. ;)

Dove ho preso le mie informazioni e cos'altro dovresti leggere?

E, naturalmente, ho trovato un sacco di cose su Google :)
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION