CodeGym /Blog Java /Aleatoriu /O explicație a expresiilor lambda în Java. Cu exemple și ...
John Squirrels
Nivel
San Francisco

O explicație a expresiilor lambda în Java. Cu exemple și sarcini. Partea 1

Publicat în grup
Pentru cine este acest 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ă.
O explicație a expresiilor lambda în Java.  Cu exemple și sarcini.  Partea 1 - 1Dacă 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. Materialul 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. Ce trebuie să știi pentru a înțelege acest articol?
  1. Ar trebui să înțelegeți programarea orientată pe obiecte (OOP), și anume:

    • clase, obiecte și diferența dintre ele;
    • interfețe, modul în care diferă de clase și relația dintre interfețe și clase;
    • metode, cum să le apelăm, metode abstracte (adică metode fără implementare), parametrii metodei, argumentele metodei și cum să le transmită;
    • modificatori de acces, metode/variabile statice, metode/variabile finale;
    • moștenirea claselor și interfețelor, moștenirea multiplă a interfețelor.
  2. Cunoașterea Java Core: tipuri generice (generice), colecții (liste), fire de execuție.
Ei bine, să trecem la asta.

Puțină istorie

Expresiile Lambda au venit în Java din programarea funcțională și acolo din matematică. În Statele Unite, la mijlocul secolului al XX-lea, Alonzo Church, care era foarte pasionat de matematică și de tot felul de abstracții, a lucrat la Universitatea Princeton. Alonzo Church a fost cel care a inventat calculul lambda, care a fost inițial un set de idei abstracte care nu aveau nicio legătură cu programarea. Matematicieni precum Alan Turing și John von Neumann au lucrat la Universitatea Princeton în același timp. Totul a venit împreună: Church a venit cu calculul lambda. Turing și-a dezvoltat mașina de calcul abstractă, cunoscută acum sub numele de „mașina Turing”. Și von Neumann a propus o arhitectură de computer care a stat la baza computerelor moderne (numită acum „arhitectură von Neumann”). La acea vreme, Biserica Alonzo' Ideile lui nu au devenit atât de cunoscute ca lucrările colegilor săi (cu excepția domeniului matematicii pure). Cu toate acestea, puțin mai târziu, John McCarthy (de asemenea, absolvent de Universitatea Princeton și, la momentul poveștii noastre, angajat al Institutului de Tehnologie din Massachusetts) a devenit interesat de ideile Bisericii. În 1958, a creat primul limbaj de programare funcțional, LISP, pe baza acestor idei. Și 58 de ani mai târziu, ideile de programare funcțională s-au scurs în Java 8. Nu au trecut nici măcar 70 de ani... Sincer, nu este cel mai mult timp necesar pentru ca o idee matematică să fie aplicată în practică. un angajat al Institutului de Tehnologie din Massachusetts) a devenit interesat de ideile Bisericii. În 1958, a creat primul limbaj de programare funcțional, LISP, pe baza acestor idei. Și 58 de ani mai târziu, ideile de programare funcțională s-au scurs în Java 8. Nu au trecut nici măcar 70 de ani... Sincer, nu este cel mai mult timp necesar pentru ca o idee matematică să fie aplicată în practică. un angajat al Institutului de Tehnologie din Massachusetts) a devenit interesat de ideile Bisericii. În 1958, a creat primul limbaj de programare funcțional, LISP, pe baza acestor idei. Și 58 de ani mai târziu, ideile de programare funcțională s-au scurs în Java 8. Nu au trecut nici măcar 70 de ani... Sincer, nu este cel mai mult timp necesar pentru ca o idee matematică să fie aplicată în practică.

Miezul problemei

O expresie lambda este un fel de funcție. Puteți considera că este o metodă Java obișnuită, dar cu capacitatea distinctivă de a fi transmisă altor metode ca argument. Asta e corect. A devenit posibil să treceți nu numai numere, șiruri și pisici la metode, ci și alte metode! Când am putea avea nevoie de asta? Ar fi util, de exemplu, dacă dorim să trecem o metodă de apel invers. Adică, dacă avem nevoie ca metoda pe care o apelăm să avem capacitatea de a apela o altă metodă pe care o transmitem. Cu alte cuvinte, avem capacitatea de a transmite un apel invers în anumite circumstanțe și un apel invers diferit în altele. Și astfel încât metoda noastră care primește apelurile noastre le apelează. Sortarea este un exemplu simplu. Să presupunem că scriem un algoritm inteligent de sortare care arată astfel:

public void mySuperSort() { 
    // We do something here 
    if(compare(obj1, obj2) > 0) 
    // And then we do something here 
}
În ifenunț, numim compare()metoda, trecând două obiecte de comparat și vrem să știm care dintre aceste obiecte este „mai mare”. Presupunem că cel „mai mare” vine înaintea celui „mai mic”. Am pus „mai mare” între ghilimele, pentru că scriem o metodă universală care va ști să sorteze nu doar crescător, ci și descrescător (în acest caz, obiectul „mai mare” va fi de fapt obiectul „mai mic” , si invers). Pentru a seta algoritmul specific pentru sortarea noastră, avem nevoie de un mecanism care să-l transmitem mySuperSort()metodei noastre. În acest fel, vom putea să ne „controlăm” metoda când este apelată. Desigur, am putea scrie două metode separate - mySuperSortAscend()șimySuperSortDescend()— pentru sortarea în ordine crescătoare și descrescătoare. Sau am putea trece un argument metodei (de exemplu, o variabilă booleană; dacă adevărat, atunci sortați în ordine crescătoare, iar dacă fals, atunci în ordine descrescătoare). Dar dacă vrem să sortăm ceva complicat, cum ar fi o listă de matrice de șiruri? Cum va ști metoda noastră mySuperSort()cum să sorteze aceste matrice de șiruri? După mărime? După lungimea cumulată a tuturor cuvintelor? Poate alfabetic pe baza primului șir din matrice? Și ce se întâmplă dacă trebuie să sortăm lista de tablouri după dimensiunea matricei în unele cazuri și după lungimea cumulativă a tuturor cuvintelor din fiecare matrice în alte cazuri? Mă aștept că ați auzit deja despre comparatori și că în acest caz am trece pur și simplu metodei noastre de sortare un obiect comparator care descrie algoritmul de sortare dorit. Pentru că standardulsort()metoda este implementată pe același principiu ca și pe care mySuperSort()îl voi folosi sort()în exemplele mele.

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

List<String[]> arrays = new ArrayList<>(); 
arrays.add(array1); 
arrays.add(array2); 
arrays.add(array3); 

Comparator<;String[]> sortByLength = new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
}; 

Comparator<String[]> sortByCumulativeWordLength = new Comparator<String[]>() { 

    @Override 
    public int compare(String[] o1, String[] o2) { 
        int length1 = 0; 
        int length2 = 0; 
        for (String s : o1) { 
            length1 += s.length(); 
        } 

        for (String s : o2) { 
            length2 += s.length(); 
        } 

        return length1 - length2; 
    } 
};

arrays.sort(sortByLength);
Rezultat:

  1. Dota GTA5 Halo
  2. if then else
  3. I really love Java
Aici matricele sunt sortate după numărul de cuvinte din fiecare matrice. O matrice cu mai puține cuvinte este considerată „mai mică”. De aceea este primul. O matrice cu mai multe cuvinte este considerată „mai mare” și este plasată la sfârșit. Dacă trecem metodei un comparator diferit sort(), cum ar fi sortByCumulativeWordLength, atunci vom obține un rezultat diferit:

  1. if then else
  2. Dota GTA5 Halo
  3. I really love Java
Acum matricele sunt sortate după numărul total de litere din cuvintele matricei. În prima matrice, există 10 litere, în a doua — 12, iar în a treia — 15. Dacă avem doar un singur comparator, atunci nu trebuie să declarăm o variabilă separată pentru acesta. În schimb, putem crea pur și simplu o clasă anonimă chiar în momentul apelului la sort()metodă. Ceva de genul:

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

List<String[]> arrays = new ArrayList<>(); 

arrays.add(array1); 
arrays.add(array2); 
arrays.add(array3); 

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
}); 
Vom obține același rezultat ca în primul caz. Sarcina 1. Rescrieți acest exemplu astfel încât să sorteze tablourile nu în ordinea crescătoare a numărului de cuvinte din fiecare matrice, ci în ordine descrescătoare. Știm deja toate acestea. Știm să transmitem obiecte metodelor. În funcție de ceea ce avem nevoie în acest moment, putem trece diferite obiecte unei metode, care va invoca apoi metoda pe care am implementat-o. Acest lucru ridică întrebarea: de ce avem nevoie de o expresie lambda aici?  Pentru că o expresie lambda este un obiect care are exact o metodă. Ca un „obiect de metodă”. O metodă ambalată într-un obiect. Are doar o sintaxă puțin necunoscută (dar mai multe despre asta mai târziu). Să aruncăm o altă privire la acest cod:

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
});
Aici luăm lista noastră de tablouri și numim sort()metoda acesteia, căreia îi trecem un obiect comparator cu o singură compare()metodă (numele lui nu contează pentru noi - la urma urmei, este singura metodă a acestui obiect, așa că nu putem greși). Această metodă are doi parametri cu care vom lucra. Dacă lucrați în IntelliJ IDEA, probabil ați văzut-o oferind condensarea semnificativă a codului, după cum urmează:

arrays.sort((o1, o2) -> o1.length - o2.length);
Acest lucru reduce șase linii la una singură scurtă. 6 rânduri sunt rescrise ca unul scurt. Ceva a dispărut, dar vă garantez că nu a fost nimic important. Acest cod va funcționa exact în același mod ca și cu o clasă anonimă. Sarcina 2. Gândiți-vă la rescrierea soluției pentru Sarcina 1 folosind o expresie lambda (cel puțin, cereți IntelliJ IDEA să vă convertească clasa anonimă într-o expresie lambda).

Să vorbim despre interfețe

În principiu, o interfață este pur și simplu o listă de metode abstracte. Când creăm o clasă care implementează o interfață, clasa noastră trebuie să implementeze metodele incluse în interfață (sau trebuie să facem clasa abstractă). Există interfețe cu o mulțime de metode diferite (de exemplu,  List), și există interfețe cu o singură metodă (de exemplu, Comparatorsau Runnable). Există interfețe care nu au o singură metodă (așa-numitele interfețe de marcare, cum ar fi Serializable). Interfețele care au o singură metodă se mai numesc și interfețe funcționale . În Java 8, acestea sunt chiar marcate cu o adnotare specială:@FunctionalInterface. Aceste interfețe cu o singură metodă sunt potrivite ca tipuri țintă pentru expresiile lambda. După cum am spus mai sus, o expresie lambda este o metodă înfășurată într-un obiect. Și când trecem cu un astfel de obiect, trecem în esență prin această metodă unică. Se pare că nu ne interesează cum se numește metoda. Singurele lucruri care contează pentru noi sunt parametrii metodei și, desigur, corpul metodei. În esență, o expresie lambda este implementarea unei interfețe funcționale. Oriunde vedem o interfață cu o singură metodă, o clasă anonimă poate fi rescrisă ca lambda. Dacă interfața are mai multe sau mai puține metode, atunci o expresie lambda nu va funcționa și vom folosi în schimb o clasă anonimă sau chiar o instanță a unei clase obișnuite. Acum este timpul să sapă puțin în lambda. :)

Sintaxă

Sintaxa generală este cam așa:

(parameters) -> {method body}
Adică, parantezele care înconjoară parametrii metodei, o „săgeată” (formată dintr-o cratimă și semnul mai mare decât), și apoi corpul metodei între acolade, ca întotdeauna. Parametrii corespund celor specificați în metoda interfeței. Dacă tipurile de variabile pot fi determinate fără ambiguitate de către compilator (în cazul nostru, acesta știe că lucrăm cu matrice de șiruri, deoarece Listobiectul nostru este tastat folosind String[]), atunci nu trebuie să le indicați tipurile.
Dacă sunt ambigue, atunci indicați tipul. IDEA îl va colora în gri dacă nu este necesar.
Puteți citi mai multe în acest tutorial Oracle și în alte părți. Acest lucru se numește „ titare țintă ”. Puteți numi variabilele cum doriți - nu trebuie să utilizați aceleași nume specificate în interfață. Dacă nu există parametri, indicați doar parantezele goale. Dacă există un singur parametru, pur și simplu indicați numele variabilei fără paranteze. Acum că înțelegem parametrii, este timpul să discutăm despre corpul expresiei lambda. În interiorul acoladelor, scrieți cod exact așa cum ați face pentru o metodă obișnuită. Dacă codul dvs. constă dintr-o singură linie, atunci puteți omite în întregime acoladele (similar cu instrucțiunile if și buclele for). Dacă lambda pe o singură linie returnează ceva, nu trebuie să includeți areturnafirmație. Dar dacă folosiți acolade, atunci trebuie să includeți în mod explicit o returndeclarație, așa cum ați face într-o metodă obișnuită.

Exemple

Exemplul 1.

() -> {}
Cel mai simplu exemplu. Și cel mai inutil :), din moment ce nu face nimic. Exemplul 2.

() -> ""
Un alt exemplu interesant. Nu ia nimic și returnează un șir gol ( returneste omis, deoarece este inutil). Iată același lucru, dar cu return:

() -> { 
    return ""; 
}
Exemplul 3. „Bună, lume!” folosind lambda

() -> System.out.println("Hello, World!")
Nu ia nimic și nu returnează nimic (nu putem pune returnînainte apelul la System.out.println(), deoarece println()tipul de returnare al metodei este void). Afișează pur și simplu salutul. Acest lucru este ideal pentru o implementare a Runnableinterfeței. Următorul exemplu este mai complet:

public class Main { 
    public static void main(String[] args) { 
        new Thread(() -> System.out.println("Hello, World!")).start(); 
    } 
}
Sau cam asa:

public class Main { 
    public static void main(String[] args) { 
        Thread t = new Thread(() -> System.out.println("Hello, World!")); 
        t.start();
    } 
}
Sau putem salva chiar expresia lambda ca Runnableobiect și apoi o transmitem constructorului Thread:

public class Main { 
    public static void main(String[] args) { 
        Runnable runnable = () -> System.out.println("Hello, World!"); 
        Thread t = new Thread(runnable); 
        t.start(); 
    } 
}
Să aruncăm o privire mai atentă la momentul în care o expresie lambda este salvată într-o variabilă. Interfața Runnablene spune că obiectele sale trebuie să aibă o public void run()metodă. Conform interfeței, runmetoda nu ia parametri. Și nu returnează nimic, adică tipul său de returnare este void. În consecință, acest cod va crea un obiect cu o metodă care nu preia și nu returnează nimic. Acest lucru se potrivește perfect cu metoda Runnableinterfeței run(). De aceea am putut pune această expresie lambda într-o Runnablevariabilă.  Exemplul 4.

() -> 42
Din nou, nu ia nimic, dar returnează numărul 42. O astfel de expresie lambda poate fi pusă într-o Callablevariabilă, deoarece această interfață are o singură metodă care arată cam așa:

V call(),
unde  V este tipul de returnare (în cazul nostru,  int). În consecință, putem salva o expresie lambda după cum urmează:

Callable<Integer> c = () -> 42;
Exemplul 5. O expresie lambda care implică mai multe linii

() -> { 
    String[] helloWorld = {"Hello", "World!"}; 
    System.out.println(helloWorld[0]); 
    System.out.println(helloWorld[1]); 
}
Din nou, aceasta este o expresie lambda fără parametri și un voidtip de returnare (pentru că nu există nicio returninstrucțiune).  Exemplul 6

x -> x
Aici luăm o xvariabilă și o returnăm. Vă rugăm să rețineți că, dacă există un singur parametru, atunci puteți omite parantezele din jurul acestuia. Iată același lucru, dar cu paranteze:

(x) -> x
Și iată un exemplu cu o declarație de returnare explicită:

x -> { 
    return x;
}
Sau așa cu paranteze și o instrucțiune return:

(x) -> { 
    return x;
}
Sau cu o indicație explicită a tipului (și astfel cu paranteze):

(int x) -> x
Exemplul 7

x -> ++x
Îl luăm xși îl returnăm, dar numai după ce adăugăm 1. Puteți rescrie acea lambda astfel:

x -> x + 1
În ambele cazuri, omitem parantezele din jurul corpului parametrului și metodei, împreună cu instrucțiunea return, deoarece acestea sunt opționale. Versiunile cu paranteze și o instrucțiune return sunt date în Exemplul 6. Exemplul 8

(x, y) -> x % y
Luăm xși yși returnăm restul împărțirii xprin y. Aici sunt necesare parantezele din jurul parametrilor. Sunt opționale doar atunci când există un singur parametru. Iată-l cu o indicație explicită a tipurilor:

(double x, int y) -> x % y
Exemplul 9

(Cat cat, String name, int age) -> {
    cat.setName(name); 
    cat.setAge(age); 
}
Luăm un Catobiect, un Stringnume și o vârstă int. În metoda în sine, folosim numele transmis și vârsta pentru a seta variabile pe pisică. Deoarece obiectul nostru cateste un tip de referință, acesta va fi modificat în afara expresiei lambda (va obține numele și vârsta transmise). Iată o versiune puțin mai complicată care folosește o lambda similară:

public class Main { 

    public static void main(String[] args) { 
        // Create a cat and display it to confirm that it is "empty" 
        Cat myCat = new Cat(); 
        System.out.println(myCat);
 
        // Create a lambda 
        Settable<Cat> s = (obj, name, age) -> { 
            obj.setName(name); 
            obj.setAge(age); 

        }; 

        // Call a method to which we pass the cat and lambda 
        changeEntity(myCat, s); 

        // Display the cat on the screen and see that its state has changed (it has a name and age) 
        System.out.println(myCat); 

    } 

    private static <T extends HasNameAndAge>  void changeEntity(T entity, Settable<T> s) { 
        s.set(entity, "Smokey", 3); 
    }
}

interface HasNameAndAge { 
    void setName(String name); 
    void setAge(int age); 
}

interface Settable<C extends HasNameAndAge> { 
    void set(C entity, String name, int age); 
}

class Cat implements HasNameAndAge { 
    private String name; 
    private int age; 

    @Override 
    public void setName(String name) { 
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age; 
    } 

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' + 
                ", age=" + age + 
                '}';
    }
}
Rezultat:

Cat{name='null', age=0}
Cat{name='Smokey', age=3}
După cum puteți vedea, Catobiectul avea o singură stare, iar apoi starea s-a schimbat după ce am folosit expresia lambda. Expresiile Lambda se combină perfect cu genericele. Și dacă trebuie să creăm o Dogclasă care implementează și HasNameAndAge, atunci putem efectua aceleași operații în Dogmetodă main() fără a modifica expresia lambda. Sarcina 3. Scrieți o interfață funcțională cu o metodă care ia un număr și returnează o valoare booleană. Scrieți o implementare a unei astfel de interfețe ca expresie lambda care returnează adevărat dacă numărul transmis este divizibil cu 13. Sarcina 4.Scrieți o interfață funcțională cu o metodă care ia două șiruri și returnează, de asemenea, un șir. Scrieți o implementare a unei astfel de interfețe ca o expresie lambda care returnează șirul mai lung. Sarcina 5. Scrieți o interfață funcțională cu o metodă care ia trei numere în virgulă mobilă: a, b și c și returnează, de asemenea, un număr în virgulă mobilă. Scrieți o implementare a unei astfel de interfețe ca o expresie lambda care returnează discriminantul. În caz că ai uitat, asta e D = b^2 — 4ac. Sarcina 6. Folosind interfața funcțională din Sarcina 5, scrieți o expresie lambda care returnează rezultatul a * b^c. O explicație a expresiilor lambda în Java. Cu exemple și sarcini. Partea 2
Comentarii
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION