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ă.
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ă counter
este o referință la un AtomicInteger
obiect. Ș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ă: counter
nu 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 main
metodă.
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 Runnable
obiect ș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ă Consumer
interfaț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 x
variabilă. Aceeași forEach()
metodă iterează prin toate elementele din colecție și apelează accept()
metoda de implementare aConsumer
interfaț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ă void
către forEach()
, care așteaptă în schimb un Consumer
obiect.
Sintaxă pentru referințele de metodă
Este destul de simplu:-
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 } }
-
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 } }
-
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); } } }
-
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ă,this
cuvâ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?
- Tutorial pe site-ul oficial Oracle. O mulțime de informații detaliate, inclusiv exemple.
- Capitolul despre referințele de metodă din același tutorial Oracle.
- Fii absorbit de Wikipedia dacă ești cu adevărat curios.
GO TO FULL VERSION