JavaRush /Java Blog /Random-IT /Popolare sulle espressioni lambda in Java. Con esempi e c...
Стас Пасинков
Livello 26
Киев

Popolare sulle espressioni lambda in Java. Con esempi e compiti. Parte 2

Pubblicato nel gruppo Random-IT
A chi è rivolto questo articolo?
  • Per chi ha letto la prima parte di questo articolo;

  • Per coloro che pensano di conoscere già bene Java Core, ma non hanno idea delle espressioni lambda in Java. O forse hai già sentito parlare di lambda, ma senza dettagli.

  • per coloro che hanno una certa comprensione delle espressioni lambda, ma hanno ancora paura ed è insolito usarle.

Accesso alle variabili esterne

Questo codice verrà compilato con una classe anonima?
int counter = 0;
Runnable r = new Runnable() {
    @Override
    public void run() {
        counter++;
    }
};
NO. La variabile counterdeve essere final. O non necessariamente final, ma in ogni caso non può modificarne il valore. Lo stesso principio viene utilizzato nelle espressioni lambda. Hanno accesso a tutte le variabili che sono "visibili" dal luogo in cui sono dichiarate. Ma la lambda non dovrebbe cambiarli (assegnare un nuovo valore). È vero, esiste un'opzione per aggirare questa limitazione nelle classi anonime. È sufficiente creare una variabile di tipo riferimento e modificare lo stato interno dell'oggetto. In questo caso la variabile stessa punterà allo stesso oggetto, e in questo caso potrete tranquillamente indicarla come final.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Qui la nostra variabile counterè un riferimento a un oggetto di tipo AtomicInteger. E per modificare lo stato di questo oggetto, viene utilizzato il metodo incrementAndGet(). Il valore della variabile stessa non cambia durante l'esecuzione del programma e punta sempre allo stesso oggetto, il che ci permette di dichiarare immediatamente una variabile con la parola chiave final. Gli stessi esempi, ma con espressioni lambda:
int counter = 0;
Runnable r = () -> counter++;
Non si compila per lo stesso motivo dell'opzione con classe anonima: counternon dovrebbe cambiare mentre il programma è in esecuzione. Ma così, va tutto bene:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
Questo vale anche per i metodi di chiamata. Dall'interno di un'espressione lambda, non solo puoi accedere a tutte le variabili "visibili", ma anche chiamare i metodi a cui hai accesso.
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    }

    private static void staticMethod() {
        System.out.println("Я - метод staticMethod(), и меня только-что кто-то вызвал!");
    }
}
Sebbene il metodo staticMethod()sia privato, è “accessibile” per essere chiamato all'interno del metodo main(), quindi è anche accessibile per essere chiamato dall'interno del lambda creato nel metodo main.

Il momento dell'esecuzione del codice dell'espressione lambda

Questa domanda potrebbe sembrarti troppo semplice, ma vale la pena chiedersi: quando verrà eseguito il codice all'interno dell'espressione lambda? Al momento della creazione? Oppure nel momento in cui (ancora non si sa dove) verrà chiamato? È abbastanza facile da controllare.
System.out.println("Запуск программы");

// много всякого разного codeа
// ...

System.out.println("Перед объявлением лямбды");

Runnable runnable = () -> System.out.println("Я - лямбда!");

System.out.println("После объявления лямбды");

// много всякого другого codeа
// ...

System.out.println("Перед передачей лямбды в тред");
new Thread(runnable).start();
Uscita sul display:
Запуск программы
Перед объявлением лямбды
После объявления лямбды
Перед передачей лямбды в тред
Я - лямбда!
Si può vedere che il codice dell'espressione lambda è stato eseguito proprio alla fine, dopo la creazione del thread e solo quando il processo di esecuzione del programma ha raggiunto l'effettiva esecuzione del metodo run(). E per niente al momento del suo annuncio. Dichiarando un'espressione lambda, abbiamo solo creato un oggetto del tipo Runnablee descritto il comportamento del suo metodo run(). Il metodo stesso è stato lanciato molto più tardi.

Riferimenti al metodo?

Non direttamente correlato alle lambda stesse, ma penso che sarebbe logico spendere qualche parola al riguardo in questo articolo. Diciamo di avere un'espressione lambda che non fa nulla di speciale, ma chiama semplicemente qualche metodo.
x -> System.out.println(x)
Gli hanno consegnato qualcosa хe lui semplicemente lo ha chiamato System.out.println()e lo ha passato lì х. In questo caso, possiamo sostituirlo con un collegamento al metodo di cui abbiamo bisogno. Come questo:
System.out::println
Sì, senza parentesi alla fine! Esempio più completo:
List<String> strings = new LinkedList<>();
strings.add("Mother");
strings.add("soap");
strings.add("frame");

strings.forEach(x -> System.out.println(x));
Nell'ultima riga utilizziamo un metodo forEach()che accetta un oggetto interfaccia Consumer. Anche questa è un'interfaccia funzionale con un solo metodo void accept(T t). Di conseguenza, scriviamo un'espressione lambda che accetta un parametro (poiché è digitato nell'interfaccia stessa, non indichiamo il tipo del parametro, ma indichiamo che verrà chiamato х). Nel corpo dell'espressione lambda scriviamo il codice che verrà eseguito quando viene chiamato il metodo accept(). Qui mostriamo semplicemente sullo schermo cosa c'è nella variabile х. Il metodo stesso forEach()attraversa tutti gli elementi della raccolta, chiama Consumeril metodo dell'oggetto interfaccia che gli è stato passato (la nostra lambda) accept(), dove passa ogni elemento della collezione. Come ho già detto, questa è un'espressione lambda (semplicemente chiamando un altro metodo) che possiamo sostituire con un riferimento al metodo di cui abbiamo bisogno. Quindi il nostro codice sarà simile a questo:
List<String> strings = new LinkedList<>();
strings.add("Mother");
strings.add("soap");
strings.add("frame");

strings.forEach(System.out::println);
La cosa principale è che i parametri accettati dei metodi (println()e accept()). Poiché il metodo println()può accettare qualsiasi cosa (è sovraccaricato per tutte le primitive e per qualsiasi oggetto), invece di un'espressione lambda, possiamo passare forEach()solo un riferimento al metodo println(), quindi forEach()prenderà ogni elemento della raccolta e lo passerà direttamente a il metodo println()Per coloro che incontrano questo problema per la prima volta, notare che non chiamiamo il metodo System.out.println()(con punti tra le parole e con parentesi alla fine), ma piuttosto passiamo il riferimento a questo metodo stesso.
strings.forEach(System.out.println());
avremo un errore di compilazione. Perché prima della chiamata forEach(), Java vedrà che viene chiamato System.out.println(), capirà che viene restituito voide proverà voida passarlo all'oggetto forEach()di tipo che sta aspettando lì Consumer.

Sintassi per l'utilizzo dei riferimenti ai metodi

È piuttosto semplice:
  1. Passaggio di un riferimento a un metodo staticoNameКласса:: NameСтатическогоМетода?

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("Mother");
            strings.add("soap");
            strings.add("frame");
    
            strings.forEach(Main::staticMethod);
        }
    
        private static void staticMethod(String s) {
            // do something
        }
    }
  2. Passaggio di un riferimento a un metodo non statico utilizzando un oggetto esistenteNameПеременнойСОбъектом:: method name

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("Mother");
            strings.add("soap");
            strings.add("frame");
    
            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 in cui è implementato tale metodoNameКласса:: method name

    public class Main {
        public static void main(String[] args) {
            List<User> users = new LinkedList<>();
            users.add(new User("Vasya"));
            users.add(new User("Коля"));
            users.add(new User("Петя"));
    
            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. Passare un collegamento al costruttore NameКласса::new
    L'uso dei collegamenti ai metodi è molto comodo quando esiste un metodo già pronto di cui sei completamente soddisfatto e desideri utilizzarlo come callback. In questo caso, invece di scrivere un'espressione lambda con il codice di quel metodo, o un'espressione lambda in cui chiamiamo semplicemente questo metodo, gli passiamo semplicemente un riferimento. È tutto.

Differenza interessante tra classe anonima ed espressione lambda

In una classe anonima, la parola chiave thispunta a un oggetto di quella classe anonima. E se lo usiamo thisall'interno di una lambda, avremo accesso all'oggetto della classe di framing. Dove abbiamo effettivamente scritto questa espressione. Ciò accade perché le espressioni lambda, una volta compilate, diventano un metodo privato della classe in cui sono scritte. Non consiglierei di utilizzare questa “funzionalità”, poiché ha un effetto collaterale che contraddice i principi della programmazione funzionale. Ma questo approccio è abbastanza coerente con l’OOP. ;)

Da dove ho preso le informazioni o cos'altro leggere

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION