Pentru cine este acest articol?
  • Este pentru cei care citesc prima parte a acestui articol;
  • Este pentru oamenii care cred că cunosc deja Java Core bine, dar nu au nicio idee despre expresiile lambda în Java. Sau poate au auzit ceva despre expresiile lambda, dar detaliile lipsesc.
  • Este pentru oamenii care au o anumită înțelegere a expresiilor lambda, dar sunt încă descurajați de ele și neobișnuiți să le folosească.
Dacă nu te încadrezi în una dintre aceste categorii, s-ar putea să găsești acest articol plictisitor, cu defecte sau, în general, să nu-ți fie ceașca de ceai. În acest caz, nu ezitați să treceți la alte lucruri sau, dacă sunteți bine versat în subiect, vă rugăm să faceți sugestii în comentarii despre cum aș putea îmbunătăți sau completa articolul. O explicație a expresiilor lambda în Java.  Cu exemple și sarcini.  Partea 2 - 1Materialul nu pretinde a avea vreo valoare academică, darămite noutate. Dimpotrivă: voi încerca să descriu cât mai simplu lucruri complexe (pentru unii oameni). O solicitare de a explica API-ul Stream m-a inspirat să scriu asta. M-am gândit la asta și am decis că unele dintre exemplele mele de flux ar fi de neînțeles fără a înțelege expresiile lambda. Deci vom începe cu expresii lambda.

Acces la variabile externe

Acest cod este compilat cu o clasă anonimă?

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

    @Override 
    public void run() { 
        counter++;
    }
};
Nu. counter Variabila trebuie să fie final. Sau dacă nu final, atunci cel puțin nu își poate schimba valoarea. Același principiu se aplică și în expresiile lambda. Ei pot accesa toate variabilele pe care le pot „vedea” din locul în care sunt declarate. Dar un lambda nu trebuie să le modifice (atribuiți-le o nouă valoare). Cu toate acestea, există o modalitate de a ocoli această restricție în clasele anonime. Pur și simplu creați o variabilă de referință și modificați starea internă a obiectului. Procedând astfel, variabila în sine nu se modifică (indică același obiect) și poate fi marcată în siguranță ca final.

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

    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Aici variabila noastră countereste o referință la un AtomicIntegerobiect. Și incrementAndGet()metoda este folosită pentru a schimba starea acestui obiect. Valoarea variabilei în sine nu se modifică în timp ce programul rulează. Indică întotdeauna același obiect, ceea ce ne permite să declarăm variabila cu cuvântul cheie final. Iată aceleași exemple, dar cu expresii lambda:

int counter = 0;
Runnable r = () -> counter++;
Aceasta nu se va compila din același motiv ca și versiunea cu o clasă anonimă:  counternu trebuie să se schimbe în timp ce programul rulează. Dar totul este bine dacă procedăm astfel:

final AtomicInteger counter = new AtomicInteger(0); 
Runnable r = () -> counter.incrementAndGet();
Acest lucru se aplică și metodelor de apelare. În cadrul expresiilor lambda, puteți nu numai să accesați toate variabilele „vizibile”, ci și să apelați orice metode accesibile.

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!");
    }
}
Deși staticMethod()este privat, este accesibil în interiorul main()metodei, deci poate fi apelat și din interiorul unui lambda creat în mainmetodă.

Când se execută o expresie lambda?

S-ar putea să vi se pară că următoarea întrebare este prea simplă, dar ar trebui să o întrebați la fel: când va fi executat codul din interiorul expresiei lambda? Când este creat? Sau când se numește (ceea ce încă nu se știe)? Acest lucru este destul de ușor de verificat.

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(); 
Ieșire ecran:

Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
Puteți vedea că expresia lambda a fost executată chiar la sfârșit, după crearea firului de execuție și numai când execuția programului ajunge la metodă run(). Cu siguranță nu atunci când este declarată. Prin declararea unei expresii lambda, am creat doar un Runnableobiect și am descris cum run()se comportă metoda acestuia. Metoda în sine este executată mult mai târziu.

Referințe de metodă?

Referințele la metode nu sunt direct legate de lambda, dar cred că are sens să spunem câteva cuvinte despre ele în acest articol. Să presupunem că avem o expresie lambda care nu face nimic special, ci pur și simplu apelează o metodă.

x -> System.out.println(x)
Primește câteva xși doar apeluri System.out.println(), trecând x. În acest caz, îl putem înlocui cu o referire la metoda dorită. Ca aceasta:

System.out::println
Așa este, fără paranteze la sfârșit! Iată un exemplu mai complet:

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

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

strings.forEach(x -> System.out.println(x));
În ultima linie, folosim forEach()metoda, care preia un obiect care implementează Consumerinterfața. Din nou, aceasta este o interfață funcțională, având o singură void accept(T t)metodă. În consecință, scriem o expresie lambda care are un parametru (pentru că este introdusă în interfața însăși, nu specificăm tipul de parametru; indicăm doar că îl vom numi x). În corpul expresiei lambda, scriem codul care va fi executat atunci când metoda accept()este apelată. Aici afișăm pur și simplu ceea ce a ajuns în xvariabilă. Aceeași forEach()metodă iterează prin toate elementele din colecție și apelează accept()metoda de implementare aConsumerinterfața (lambda noastră), trecând fiecare articol din colecție. După cum am spus, putem înlocui o astfel de expresie lambda (una care pur și simplu clasifică o metodă diferită) cu o referință la metoda dorită. Atunci codul nostru va arăta astfel:

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

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

strings.forEach(System.out::println);
Principalul lucru este că parametrii metodelor println()și accept()se potrivesc. Deoarece println()metoda poate accepta orice (este supraîncărcată pentru toate tipurile de primitive și toate obiectele), în loc de expresii lambda, putem pur și simplu să transmitem o referință la println()metodă către forEach(). Apoi forEach()va lua fiecare element din colecție și îl va transmite direct metodei println(). Pentru oricine întâlnește acest lucru pentru prima dată, vă rugăm să rețineți că nu sunăm System.out.println()(cu puncte între cuvinte și cu paranteze la sfârșit). În schimb, trecem o referință la această metodă. Daca scriem asta

strings.forEach(System.out.println());
vom avea o eroare de compilare. Înainte de apelul la forEach(), Java vede că System.out.println()este apelat, așa că înțelege că valoarea returnată este voidși va încerca să fie transmisă voidcătre forEach(), care așteaptă în schimb un Consumerobiect.

Sintaxă pentru referințele de metodă

Este destul de simplu:
  1. Transmitem o referință la o metodă statică ca aceasta: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. Transmitem o referință la o metodă non-statică folosind un obiect existent, astfel: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. Transmitem o referință la o metodă non-statică folosind clasa care o implementează după cum urmează: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. Transmitem o referință la un constructor ca acesta:ClassName::new

    Referințele de metodă sunt foarte convenabile atunci când aveți deja o metodă care ar funcționa perfect ca un apel invers. În acest caz, în loc să scriem o expresie lambda care conține codul metodei sau să scriem o expresie lambda care apelează pur și simplu metoda, pur și simplu trecem o referință la aceasta. Si asta e.

O distincție interesantă între clasele anonime și expresiile lambda

Într-o clasă anonimă, thiscuvântul cheie indică un obiect din clasa anonimă. Dar dacă folosim acest lucru în interiorul unei lambda, obținem acces la obiectul clasei care le conține. Cel în care am scris de fapt expresia lambda. Acest lucru se întâmplă deoarece expresiile lambda sunt compilate într-o metodă privată a clasei în care sunt scrise. Nu aș recomanda utilizarea acestei „funcții”, deoarece are un efect secundar și care contrazice principiile programării funcționale. Acestea fiind spuse, această abordare este în întregime în concordanță cu OOP. ;)

De unde mi-am luat informațiile și ce ar trebui să mai citiți?

Și, desigur, am găsit o mulțime de lucruri pe Google :)