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. 2. rész

Megjelent a csoportban
Kinek szól ez a cikk?
  • Azoknak szól, akik elolvassák a cikk első részét ;
  • 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.
Ha 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. A lambda-kifejezések magyarázata Java nyelven.  Példákkal és feladatokkal.  2. rész – 1Az 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.

Hozzáférés a külső változókhoz

Ez a kód névtelen osztállyal fordul le?

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

    @Override 
    public void run() { 
        counter++;
    }
};
Nem. A counter változónak final. Vagy ha nem final, akkor legalább nem változtathatja meg az értékét. Ugyanez az elv érvényes a lambda kifejezésekre is. Minden változóhoz hozzáférhetnek, amit a deklarált helyükről "látnak". De a lambda nem változtathatja meg ezeket (új értéket rendelhet hozzájuk). Azonban van mód ennek a korlátozásnak a megkerülésére az anonim osztályokban. Egyszerűen hozzon létre egy referenciaváltozót, és módosítsa az objektum belső állapotát. Ennek során maga a változó nem változik (ugyanarra az objektumra mutat), és biztonságosan megjelölhető ként final.

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

    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Itt countera változónk egy objektumra való hivatkozás AtomicInteger. A incrementAndGet()módszert pedig az objektum állapotának megváltoztatására használják. Maga a változó értéke nem változik a program futása közben. Mindig ugyanarra az objektumra mutat, ami lehetővé teszi, hogy a változót a végső kulcsszóval deklaráljuk. Íme ugyanazok a példák, de lambda kifejezésekkel:

int counter = 0;
Runnable r = () -> counter++;
Ez ugyanazon okból nem fordul le, mint a névtelen osztályú verzió:  counternem változhat a program futása közben. De minden rendben van, ha így csináljuk:

final AtomicInteger counter = new AtomicInteger(0); 
Runnable r = () -> counter.incrementAndGet();
Ez vonatkozik a hívási módszerekre is. A lambda-kifejezéseken belül nem csak az összes "látható" változót érheti el, hanem bármely elérhető metódust is meghívhat.

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!");
    }
}
Bár staticMethod()privát, de a metóduson belül elérhető main(), így a metódusban létrehozott lambda belsejéből is meghívható main.

Mikor kerül végrehajtásra a lambda kifejezés?

Lehet, hogy a következő kérdést túl egyszerűnek találja, de ugyanúgy fel kell tennie: mikor kerül végrehajtásra a lambda kifejezésben lévő kód? Mikor jön létre? Vagy mikor hívják (ami még nem ismert)? Ezt meglehetősen könnyű ellenőrizni.

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(); 
Képernyő kimenet:

Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
Látható, hogy a lambda kifejezés a legvégén, a szál létrehozása után futott le, és csak akkor, amikor a program végrehajtása eléri a run()metódust. Természetesen nem, amikor kijelentik. RunnableA lambda-kifejezés deklarálásával csak egy objektumot hoztunk létre , és leírtuk, hogyan run()viselkedik a metódusa. Maga a módszer sokkal később kerül végrehajtásra.

Módszer hivatkozások?

A módszerre vonatkozó hivatkozások nem kapcsolódnak közvetlenül a lambdákhoz, de úgy gondolom, hogy érdemes néhány szót ejteni róluk ebben a cikkben. Tegyük fel, hogy van egy lambda-kifejezésünk, amely nem csinál semmi különöset, hanem egyszerűen meghív egy metódust.

x -> System.out.println(x)
Fogad néhányat x, és csak hív System.out.println(), átmegy x. Ebben az esetben a kívánt módszerre való hivatkozással helyettesíthetjük. Mint ez:

System.out::println
Így van – nincs zárójel a végén! Íme egy teljesebb példa:

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

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

strings.forEach(x -> System.out.println(x));
Az utolsó sorban a metódust használjuk forEach(), amely egy objektumot vesz fel, amely megvalósítja az Consumerinterfészt. Ez ismét egy funkcionális interfész, csak egy void accept(T t)módszerrel. Ennek megfelelően írunk egy lambda kifejezést, aminek egy paramétere van (mivel magában az interfészben van beírva, nem adjuk meg a paraméter típusát, csak azt jelezzük, hogy hívni fogjuk) x. A lambda kifejezés törzsébe írjuk azt a kódot, amely a accept()metódus meghívásakor lefut. Itt egyszerűen megjelenítjük, hogy mi került a változóba x. Ugyanez forEach()a metódus iterál a gyűjtemény összes elemén, és meghívja a accept()metódust a végrehajtásáraConsumerfelület (a mi lambdánk), átadva a gyűjtemény minden elemét. Mint mondtam, egy ilyen lambda-kifejezést (olyat, amely egyszerűen egy másik metódust osztályoz) helyettesíthetünk a kívánt metódusra való hivatkozással. Ekkor a kódunk így fog kinézni:

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

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

strings.forEach(System.out::println);
println()A lényeg az, hogy a és a metódusok paraméterei accept()egyezzenek. Mivel a println()metódus bármit elfogad (túlterhelt minden primitív típusnál és minden objektumnál), lambda kifejezések helyett egyszerűen átadhatunk egy hivatkozást a metódusra a println()-nak forEach(). Ezután forEach()a gyűjtemény minden elemét átveszi, és közvetlenül a println()metódusnak adja át. Aki először találkozik ezzel, vegye figyelembe, hogy nem hívunk System.out.println()(pontokkal a szavak között és zárójelekkel a végén). Ehelyett utalunk erre a módszerre. Ha ezt írjuk

strings.forEach(System.out.println());
fordítási hibánk lesz. A hívás előtt forEach()a Java látja, hogy a System.out.println()hívás folyamatban van, így megérti, hogy a visszatérési érték az, voidés megpróbálja átadni voida -nak forEach(), amely ehelyett egy Consumerobjektumot vár.

A metódushivatkozások szintaxisa

Nagyon egyszerű:
  1. Hivatkozást adunk át egy statikus módszerre, például: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. Hivatkozást adunk át egy nem statikus metódusra egy meglévő objektum használatával, például: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. Hivatkozást adunk át egy nem statikus metódusra az azt megvalósító osztály használatával: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. Ilyen hivatkozást adunk át egy konstruktorra:ClassName::new

    A metódushivatkozások nagyon kényelmesek, ha már rendelkezik olyan metódussal, amely tökéletesen működne visszahívásként. Ebben az esetben ahelyett, hogy a metódus kódját tartalmazó lambda-kifejezést írnánk, vagy egy lambda-kifejezést írnánk, amely egyszerűen meghívja a metódust, egyszerűen hivatkozást adunk át rá. És ez az.

Érdekes különbségtétel az anonim osztályok és a lambda kifejezések között

Egy névtelen osztályban a thiskulcsszó az anonim osztály egy objektumára mutat. De ha ezt egy lambdán belül használjuk, akkor hozzáférünk a tartalmazó osztály objektumához. Az, ahol valójában a lambda kifejezést írtuk. Ez azért történik, mert a lambda-kifejezések annak az osztálynak a privát metódusába vannak fordítva, amelybe írták őket. Nem javaslom ennek a "szolgáltatásnak" a használatát, mivel mellékhatása van, és ez ellentmond a funkcionális programozás elveinek. Ennek ellenére ez a megközelítés teljes mértékben összhangban van az OOP-val. ;)

Honnan szereztem az információkat, és mit érdemes még olvasni?

És persze rengeteg dolgot találtam a Google-ben :)
Hozzászólások
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION