CodeGym /Java blog /Véletlen /A lambda-kifejezések magyarázata Java nyelven. Példákkal ...
John Squirrels
Szint
San Francisco

A lambda-kifejezések magyarázata Java nyelven. Példákkal és feladatokkal. 1. rész

Megjelent a csoportban
Kinek szól ez a cikk?
  • Azoknak szól, akik azt hiszik, hogy már jól ismerik a Java Core-t, de fogalmuk sincs a Java lambda kifejezéseiről. Vagy talán hallottak valamit a lambda kifejezésekről, de a részletek hiányoznak
  • Azoknak az embereknek szól, akik értik a lambda-kifejezéseket, de még mindig elriasztják őket, és nincsenek hozzászokva a használatukhoz.
A lambda-kifejezések magyarázata Java nyelven.  Példákkal és feladatokkal.  1. rész – 1Ha nem felel meg ezeknek a kategóriáknak, akkor ezt a cikket unalmasnak, hibásnak találhatja, vagy általában nem az Ön csésze teája. Ebben az esetben nyugodtan térjen át más dolgokra, vagy ha jól jártas a témában, tegyen javaslatokat a megjegyzésekben, hogyan tudnám javítani vagy kiegészíteni a cikket. Az anyag nem állítja, hogy tudományos értékkel bírna, nemhogy újdonságra. Éppen ellenkezőleg: megpróbálom a lehető legegyszerűbben leírni a bonyolult dolgokat (néhány ember számára). Egy kérés, hogy magyarázzam el a Stream API-t, inspirált ennek megírására. Elgondolkodtam, és úgy döntöttem, hogy néhány adatfolyam-példám értelmezhetetlen lenne a lambda-kifejezések megértése nélkül. Tehát kezdjük a lambda kifejezésekkel. Mit kell tudni a cikk megértéséhez?
  1. Meg kell értenie az objektum-orientált programozást (OOP), nevezetesen:

    • osztályok, objektumok és a köztük lévő különbség;
    • interfészek, hogyan különböznek az osztályoktól, valamint az interfészek és az osztályok közötti kapcsolat;
    • metódusok, azok hívásának módja, absztrakt metódusok (pl. implementáció nélküli metódusok), metódusparaméterek, metódusargumentumok és átadásuk módja;
    • hozzáférés módosítók, statikus metódusok/változók, végső módszerek/változók;
    • osztályok és interfészek öröklése, interfészek többszörös öröklődése.
  2. Java Core ismerete: általános típusok (generics), gyűjtemények (listák), szálak.
Nos, térjünk rá.

Egy kis történelem

A lambda-kifejezések a funkcionális programozásból érkeztek a Java-ba, oda pedig a matematikából. Az Egyesült Államokban a 20. század közepén a Princetoni Egyetemen dolgozott Alonzo Church, aki nagyon szerette a matematikát és mindenféle absztrakciót. Alonzo Church volt az, aki feltalálta a lambda kalkulust, amely kezdetben a programozáshoz teljesen független elvont ötletek halmaza volt. A Princetoni Egyetemen egy időben olyan matematikusok dolgoztak, mint Alan Turing és John von Neumann. Minden összejött: Church kitalálta a lambda kalkulust. Turing kifejlesztette absztrakt számítástechnikai gépét, amelyet ma "Turing-gépnek" neveznek. Neumann pedig egy számítógépes architektúrát javasolt, amely a modern számítógépek alapját képezte (ma "von Neumann architektúrának" nevezik). Abban az időben az Alonzo Church s ötletei nem váltak annyira ismertté, mint kollégái munkái (a tiszta matematika területét leszámítva). Azonban valamivel később John McCarthy (szintén a Princeton Egyetemen végzett, történetünk idején a Massachusetts Institute of Technology munkatársa) érdeklődni kezdett Church ötletei iránt. 1958-ban megalkotta az első funkcionális programozási nyelvet, a LISP-t ezen ötletek alapján. És 58 évvel később a funkcionális programozás ötletei beszivárogtak a Java 8-ba. Még 70 év sem telt el... Őszintén szólva, nem ez a leghosszabb idő ahhoz, hogy egy matematikai ötletet a gyakorlatban alkalmazzanak. a Massachusetts Institute of Technology munkatársa) érdeklődtek Church elképzelései iránt. 1958-ban megalkotta az első funkcionális programozási nyelvet, a LISP-t ezen ötletek alapján. És 58 évvel később a funkcionális programozás ötletei beszivárogtak a Java 8-ba. Még 70 év sem telt el... Őszintén szólva, nem ez a leghosszabb idő ahhoz, hogy egy matematikai ötletet a gyakorlatban alkalmazzanak. a Massachusetts Institute of Technology munkatársa) érdeklődtek Church elképzelései iránt. 1958-ban megalkotta az első funkcionális programozási nyelvet, a LISP-t ezen ötletek alapján. És 58 évvel később a funkcionális programozás ötletei beszivárogtak a Java 8-ba. Még 70 év sem telt el... Őszintén szólva, nem ez a leghosszabb idő ahhoz, hogy egy matematikai ötletet a gyakorlatban alkalmazzanak.

A dolog lényege

A lambda kifejezés egyfajta függvény. Tekinthetjük ezt egy közönséges Java módszernek, de azzal a megkülönböztető képességgel, hogy argumentumként átadható más metódusoknak. Úgy van. Lehetővé vált, hogy ne csak számokat, húrokat, macskákat adjunk át a metódusoknak, hanem más módszereket is! Mikor lehet erre szükségünk? Hasznos lenne például, ha valamilyen visszahívási módszert szeretnénk átadni. Vagyis ha szükségünk van az általunk meghívott metódusra, hogy képesek legyünk meghívni egy másik metódust, amelyet átadunk neki. Más szóval, így lehetőségünk van bizonyos körülmények között egy visszahívást, más esetekben pedig egy másik visszahívást átadni. És úgy, hogy a visszahívásainkat fogadó metódusunk hívja őket. A rendezés egy egyszerű példa. Tegyük fel, hogy írunk valami okos rendezési algoritmust, amely így néz ki:

public void mySuperSort() { 
    // We do something here 
    if(compare(obj1, obj2) > 0) 
    // And then we do something here 
}
Az ifutasításban a metódusnak nevezzük compare(), átadva két összehasonlítandó objektumot, és tudni akarjuk, hogy ezek közül az objektumok közül melyik a "nagyobb". Feltételezzük, hogy a „nagyobb” megelőzi a „kisebbet”. A "nagyobb" szót idézőjelbe teszem, mert egy univerzális módszert írunk, amely nem csak növekvő, hanem csökkenő sorrendben is tudja majd rendezni (ebben az esetben a "nagyobb" objektum valójában a "kisebb" objektum lesz , és fordítva). Ahhoz, hogy beállítsuk a rendezésünk konkrét algoritmusát, szükségünk van valamilyen mechanizmusra, amely átadja a módszerünknek mySuperSort(). Így képesek leszünk "szabályozni" a módszerünket, amikor meghívásra kerül. Természetesen írhatnánk két külön módszert – mySuperSortAscend()ésmySuperSortDescend()— növekvő és csökkenő sorrendbe rendezéshez. Vagy átadhatunk valamilyen argumentumot a metódusnak (például logikai változót; ha igaz, akkor növekvő sorrendbe rendezzük, ha hamis, akkor csökkenő sorrendben). De mi van, ha valami bonyolultat akarunk rendezni, például egy karakterlánc-tömbök listáját? Hogyan fogja a módszerünk mySuperSort()tudni, hogyan kell rendezni ezeket a karakterlánc-tömböket? Méret szerint? Az összes szó összesített hosszával? Talán abc sorrendben a tömb első karakterlánca alapján? És mi van akkor, ha egyes esetekben a tömbök listáját a tömb mérete, más esetekben pedig az egyes tömbök összes szójának összesített hossza alapján kell rendeznünk? Feltételezem, hogy már hallott a komparátorokról, és ebben az esetben egyszerűen átadunk a rendezési módszerünknek egy összehasonlító objektumot, amely leírja a kívánt rendezési algoritmust. Mert a szabványsort()módszert ugyanazon az elven hajtjuk végre mySuperSort(), mint sort()a példáimban.

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);
Eredmény:

  1. Dota GTA5 Halo
  2. if then else
  3. I really love Java
Itt a tömbök az egyes tömbökben lévő szavak száma szerint vannak rendezve. A kevesebb szót tartalmazó tömb "kisebbnek" minősül. Ezért van az első. A több szót tartalmazó tömb „nagyobbnak” minősül, és a végére kerül. Ha egy másik összehasonlítót adunk át a sort()metódusnak, például sortByCumulativeWordLength, akkor más eredményt kapunk:

  1. if then else
  2. Dota GTA5 Halo
  3. I really love Java
Most az are tömbök a tömb szavaiban lévő betűk teljes száma szerint vannak rendezve. Az első tömbben 10 betű, a másodikban 12, a harmadikban 15 betű található. Ha csak egyetlen komparátorunk van, akkor nem kell hozzá külön változót deklarálnunk. Ehelyett egyszerűen létrehozhatunk egy névtelen osztályt közvetlenül a metódus hívásakor sort(). Valami ilyesmi:

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; 
    } 
}); 
Ugyanazt az eredményt kapjuk, mint az első esetben. Feladat 1. Írja át ezt a példát úgy, hogy a tömböket ne az egyes tömbökben lévő szavak számának megfelelően növekvő, hanem csökkenő sorrendbe rendezze. Mindezt már tudjuk. Tudjuk, hogyan adjunk át objektumokat metódusoknak. Attól függően, hogy pillanatnyilag mire van szükségünk, különböző objektumokat adhatunk át egy metódusnak, amely ezután meghívja az általunk megvalósított metódust. Ez felveti a kérdést: mi a fenéért kell itt lambda kifejezés?  Mert a lambda kifejezés olyan objektum, amelynek pontosan egy metódusa van. Mint egy "módszer objektum". Objektumba csomagolt metódus. Csak egy kissé ismeretlen szintaxisa van (de erről később). Nézzük meg még egyszer ezt a kódot:

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
});
Itt fogjuk a tömblistánkat, és meghívjuk a metódusát sort(), aminek egyetlen metódussal adunk át egy komparátor objektumot compare()(nekünk a neve nem számít – elvégre ennek az objektumnak ez az egyetlen metódusa, így nem tévedhetünk). Ennek a módszernek két paramétere van, amelyekkel dolgozni fogunk. Ha az IntelliJ IDEA-ban dolgozik, valószínűleg látta, hogy felajánlja a kód jelentős sűrítését az alábbiak szerint:

arrays.sort((o1, o2) -> o1.length - o2.length);
Ez hat sort egyetlen rövidre redukál. 6 sor egy rövidre van átírva. Valami eltűnt, de garantálom, hogy nem volt semmi fontos. Ez a kód pontosan ugyanúgy fog működni, mint egy névtelen osztály esetében. 2. feladat. Gondolja át, hogyan írja át a megoldást az 1. feladatra lambda-kifejezés használatával (legalábbis kérje meg az IntelliJ IDEA-t, hogy alakítsa át a névtelen osztályát lambda-kifejezéssé).

Beszéljünk az interfészekről

Elvileg egy interfész egyszerűen absztrakt módszerek listája. Amikor létrehozunk egy osztályt, amely valamilyen interfészt valósít meg, akkor osztályunknak meg kell valósítania az interfészben szereplő metódusokat (vagy absztraktra kell tennünk az osztályt). Vannak olyan interfészek, amelyek sok különböző metódussal rendelkeznek (például  List), és vannak olyan felületek, amelyek csak egy metódussal rendelkeznek (például Comparatorvagy Runnable). Vannak olyan interfészek, amelyeknek nincs egyetlen metódusuk (úgynevezett marker interfészek, mint például Serializable). Azokat az interfészeket, amelyek csak egy metódussal rendelkeznek , funkcionális interfészeknek is nevezik . A Java 8-ban még egy speciális megjegyzéssel is meg vannak jelölve:@FunctionalInterface. Ezek az egymódszeres interfészek alkalmasak lambda-kifejezések céltípusaira. Ahogy fentebb mondtam, a lambda kifejezés egy objektumba csomagolt metódus. És amikor elhaladunk egy ilyen objektumon, akkor lényegében ezt az egyetlen módszert adjuk át. Kiderült, hogy nem érdekel minket, hogy hívják a módszert. Nekünk csak a metódus paraméterei számítanak, és természetesen a metódus törzse. Lényegében a lambda kifejezés egy funkcionális interfész megvalósítása. Ahol egyetlen metódussal rendelkező interfészt látunk, egy névtelen osztály átírható lambdává. Ha az interfésznek több vagy kevesebb metódusa van, akkor a lambda-kifejezés nem fog működni, és helyette egy névtelen osztályt, vagy akár egy közönséges osztály példányát fogjuk használni. Itt az ideje, hogy egy kicsit beleássunk a lambdákba. :)

Szintaxis

Az általános szintaxis valahogy így néz ki:

(parameters) -> {method body}
Vagyis a metódus paramétereit körülvevő zárójelek, egy "nyíl" (kötőjellel és nagyobb jellel), majd a metódus törzse kapcsos zárójelben, mint mindig. A paraméterek megfelelnek az interfész metódusban megadottaknak. Ha a változótípusokat egyértelműen meg tudja határozni a fordító (esetünkben tudja, hogy string tömbökkel dolgozunk, mert az objektumunk ] Listhasználatával van beírva String[), akkor nem kell megadni a típusukat.
Ha nem egyértelműek, akkor tüntesse fel a típust. Az IDEA szürkére színezi, ha nincs rá szükség.
Bővebben ebben az Oracle oktatóanyagban és máshol olvashat . Ezt " célgépelésnek " nevezik . A változókat tetszés szerint nevezheti el – nem kell ugyanazokat a neveket használnia, mint a felületen. Ha nincsenek paraméterek, csak üres zárójeleket tüntessen fel. Ha csak egy paraméter van, egyszerűen adja meg a változó nevét zárójelek nélkül. Most, hogy megértettük a paramétereket, ideje megvitatni a lambda kifejezés törzsét. A göndör kapcsos zárójelbe ugyanúgy kódot ír, mint egy közönséges metódus esetében. Ha a kód egyetlen sorból áll, akkor teljesen elhagyhatja a kapcsos zárójeleket (hasonlóan az if-utasításokhoz és a for-ciklusokhoz). Ha az egysoros lambda visszaad valamit, akkor nem kell areturnnyilatkozat. De ha göndör kapcsos zárójelet használ, akkor kifejezetten tartalmaznia kell egy returnutasítást, akárcsak egy közönséges metódusban.

Példák

1. példa

() -> {}
A legegyszerűbb példa. És a legértelmetlenebb :), hiszen nem csinál semmit. 2. példa

() -> ""
Még egy érdekes példa. Nem vesz el semmit, és egy üres karakterláncot ad vissza ( returnkimarad, mert szükségtelen). Itt ugyanaz, csak a következővel return:

() -> { 
    return ""; 
}
3. példa : "Hello, World!" lambda használatával

() -> System.out.println("Hello, World!")
Nem vesz el semmit és nem ad vissza semmit (nem tehetjük returna hívás elé System.out.println(), mert a println()metódus visszatérési típusa void). Egyszerűen megjeleníti az üdvözlést. Ez ideális az interfész megvalósításához Runnable. A következő példa teljesebb:

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

public class Main { 
    public static void main(String[] args) { 
        Thread t = new Thread(() -> System.out.println("Hello, World!")); 
        t.start();
    } 
}
Vagy akár el is menthetjük a lambda kifejezést objektumként Runnable, majd átadhatjuk a Threadkonstruktornak:

public class Main { 
    public static void main(String[] args) { 
        Runnable runnable = () -> System.out.println("Hello, World!"); 
        Thread t = new Thread(runnable); 
        t.start(); 
    } 
}
Nézzük meg közelebbről azt a pillanatot, amikor egy lambda-kifejezést elmentünk egy változóba. Az Runnableinterfész azt mondja, hogy az objektumoknak rendelkezniük kell metódussal public void run(). Az interfész szerint a runmetódus nem vesz fel paramétereket. És nem ad vissza semmit, azaz a visszatérési típusa void. Ennek megfelelően ez a kód egy objektumot hoz létre olyan metódussal, amely nem vesz fel és nem ad vissza semmit. Ez tökéletesen illeszkedik az Runnableinterfész run()módszeréhez. Ezért tudtuk ezt a lambda kifejezést változóba tenni Runnable.  4. példa

() -> 42
Megint nem vesz el semmit, de a 42-es számot adja vissza. Egy ilyen lambda kifejezést változóba lehet tenni Callable, mert ezen a felületen csak egy metódus van, ami valahogy így néz ki:

V call(),
hol  V van a visszatérési típus (esetünkben  int). Ennek megfelelően a lambda kifejezést a következőképpen menthetjük el:

Callable<Integer> c = () -> 42;
5. példa: Több sorból álló lambda kifejezés

() -> { 
    String[] helloWorld = {"Hello", "World!"}; 
    System.out.println(helloWorld[0]); 
    System.out.println(helloWorld[1]); 
}
Ez ismét egy lambda kifejezés, paraméterek nélkül és voidvisszatérési típussal (mivel nincs returnutasítás).  6. példa

x -> x
Itt veszünk egy xváltozót és visszaadjuk. Kérjük, vegye figyelembe, hogy ha csak egy paraméter van, akkor elhagyhatja a körülötte lévő zárójeleket. Itt ugyanaz, csak zárójelekkel:

(x) -> x
És itt van egy példa explicit return utasítással:

x -> { 
    return x;
}
Vagy így zárójelekkel és visszatérési utasítással:

(x) -> { 
    return x;
}
Vagy a típus kifejezett megjelölésével (és így zárójelekkel):

(int x) -> x
7. példa

x -> ++x
Elvesszük xés visszaküldjük, de csak 1 hozzáadása után. Ezt a lambdát átírhatod így:

x -> x + 1
Mindkét esetben elhagyjuk a paraméter és a metódus törzse körüli zárójeleket az utasítással együtt return, mivel ezek nem kötelezőek. A zárójeles és return utasítással ellátott változatokat a 6. példa 8. példa tartalmazza

(x, y) -> x % y
Kivesszük xés visszaadjuk a -val yvaló osztás maradékát . A paraméterek körüli zárójelek itt kötelezőek. Csak akkor kötelezőek, ha csak egy paraméter van. Itt van a típusok pontos megjelölésével: xy

(double x, int y) -> x % y
9. példa

(Cat cat, String name, int age) -> {
    cat.setName(name); 
    cat.setAge(age); 
}
Vegyünk egy Cattárgyat, egy Stringnevet és egy kort. Magában a metódusban az átadott nevet és életkort használjuk a macska változóinak beállítására. Mivel az catobjektumunk referencia típusú, a lambda kifejezésen kívül módosul (megkapja az átadott nevet és életkort). Íme egy kicsit bonyolultabb változat, amely hasonló lambdát használ:

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 + 
                '}';
    }
}
Eredmény:

Cat{name='null', age=0}
Cat{name='Smokey', age=3}
Amint láthatja, az Catobjektumnak egy állapota volt, majd az állapot megváltozott, miután a lambda kifejezést használtuk. A lambda kifejezések tökéletesen kombinálódnak az általános kifejezésekkel. És ha létre kell hoznunk egy Dogosztályt, amely szintén implementálja a -t HasNameAndAge, akkor a lambda kifejezés megváltoztatása nélkül elvégezhetjük ugyanazokat a műveleteket Doga metódusban main() . 3. feladat Írjon egy funkcionális interfészt olyan metódussal, amely számot vesz fel és logikai értéket ad vissza. Írjon egy ilyen interfész implementációját lambda kifejezésként, amely igazat ad vissza, ha az átadott szám osztható 13-mal. 4. feladat!Írjon egy funkcionális interfészt olyan metódussal, amely két karakterláncot vesz fel, és egy karakterláncot is ad vissza. Írjon egy ilyen interfész megvalósítását lambda kifejezésként, amely a hosszabb karakterláncot adja vissza. 5. feladat Írjon egy funkcionális interfészt olyan metódussal, amely három lebegőpontos számot vesz fel: a, b és c, és egy lebegőpontos számot ad vissza. Írjon egy ilyen interfész megvalósítását lambda-kifejezésként, amely a diszkriminánst adja vissza. Ha elfelejtette, akkor az D = b^2 — 4ac. 6. feladat. Az 5. feladat funkcionális interfészével írjon egy lambda kifejezést, amely az eredményt adja vissza a * b^c. A lambda-kifejezések magyarázata Java nyelven. Példákkal és feladatokkal. 2. rész
Hozzászólások
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION