Út a lambdákhoz¶
Ez a leírás egy általános bevezetőt ad a Java 8-ban megjelent lambda kifejezésekhez. A jegyzet bizonyos pontjain kitérünk a JavaFX kapcsolódási pontokra is, de a megállapítások általánosak érvényűek.
Anonymous osztályok¶
Az egyik fő kiváltó oka, hogy léteznek lambda kifejezések, az anonim osztályok létezése. Gyorsan ismételjük át ennek alapjait!
Alapvetően az anonim osztályok azt a célt szolgálják, hogy tömörebben írhassuk a programjainkat. Segítségükkel egy időben tudunk deklarálni egy osztályt és ebből példányosítani is egy objektumot. Tekinthetőek lokális osztálynak (mint egy lokális változó, csak ez osztály), aminek nincs neve.
Tipp
Akkor használjunk anonim inner class-okat, ha csak ott helyben, egyszer lesz rá szükségünk, máskülönben csináljunk egy külön osztályt!
Anonymous osztályok deklarációja¶
Fontos, hogy az anonim osztályok kifejezések lesznek amikor létrehozzuk őket és nem pedig osztály deklarációk (nem kell példányosítani, mivel az is megtörténik).
Példa: Legyen egy interface-ünk, melyet Hello
-nak hívunk és legyen egy sayHi
metódusa!
1 2 3 |
|
Ezt az interface-t valósítsa meg egy standart osztálydeklaráció!
1 2 3 4 5 6 7 |
|
Ezután ebből az osztályból tudunk példányosítani egy objektumot!
1 2 |
|
Nézzük, hogy ananymous módon, hogyan tehetném meg ezt:
1 2 3 4 5 |
|
Ebben az esetben olyan mintha egy konstruktort hívnék meg, de mivel interface-t nem lehet példányosítani (a Hello-t) ez így nem elegendő, kell egy blokkot is nyitnom, ahol az interface által előírt metódusokat kell kifejtenem (jelen esetben ez csak a sayHi
).
Még egyszer hangsúlyozom, hogy itt kvázi egy osztálydefiníciót adtam meg, de ehhez csak egy kifejezést használtam (ez egy utasítás része: látszik is, hogy egy pontosvessző van a kapcsos után), magát egy anonim inner class-t.
Nézzünk egy életszerűbb példát! Új szálak létrehozása egy remek példa:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
A példában létrehozunk egy új futtatható szálat, melynek implementálnia kell a Runnable
interface-t és itt alkalmazzuk az anonim inner class-t.
Ezután elindítjuk a szálat.
Másik tipikus használat a GUI alkalmazásokban.
Például az alábbi JavaFX kódrészlet a btn
gombhoz rendel egy eseménykezelőt, mely azt eredményezi, hogy a gomb megnyomásakor kiírjuk a Hello World
üzenetet a konzolra.
A gomb eseménykezelőknek az EventHandler<ActionEvent>
interface-t kell megvalósítani.
1 2 3 4 5 6 7 8 9 10 |
|
Osztálybővítés¶
Az interface-ek megvalósítása mellett az anonim osztályokat használhatjuk úgy, mintha osztályokból származnánk.
A módja ugyanaz, mint amikor interfészt implementálunk, de a new XXX(){...}
kifejezésben az XXX
nem interfész, hanem egy osztály!
Az egyszerű sayHi
példa ilyen módon így nézne ki.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Ebben az esetben egy meglévő osztályt terjesztünk ki (konkrétan extends
), és itt felülírjuk a sayHi()
metódusát.
Valós helyzetben ezt használhatjuk a TableView
-nál, amikor egy oszlop celláit egyedi módon szeretnénk megjeleníteni.
Az alábbi kódban az oszlop összes cellájában a FooBar
szöveg fog megjelenni.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Különbségek a hagyományos és az anonim osztályok között¶
- Egy normál osztály akármennyi interface-t implementálhat, de egy anonim inner class csak egyet (ez a megadás módjából is jön)
- Az anonim inner class vagy egy interfészt implementálhat vagy egy osztályt terjeszthet ki, de egyszerre mindkettőt nem.
- A reguláris osztályoknak akármennyi konstruktora lehet, viszont az anonim osztályokhoz nem tudunk írni konstruktort, mivel annak neve meg kéne egyezzen az osztály nevével, de ebben az esetben nincs neve. Helyette lehet használni például a példány inicializáló blokkot, mely minden példány létrehozásakor lefut.
Változók elérése anonymous inner classból¶
- Az anonymous inner class hozzáfér a befoglaló osztály adattagjaihoz
- A befoglaló blokkon belül csak a final lokális változókhoz fér hozzá
- A anonim osztályon belüli változódeklarációk elfedik az ugyanolyan nevű befoglaló blokkban található változókat
Megszorítások¶
- Nem használhatunk statikus inicializáló blokkot
- Statikus adattagokból csak konstansokat használhatunk
További források:
Lambda kifejezések¶
Most, hogy már rendberaktuk az anonim inner classokat, ráfordulhatunk magukra a lambda kifejezésekre.
A lambda kifejezéseket akkor tudjuk használni, amikor egy anonymous inner class-t használunk interfészen, és ott csak egy metódust kell kifejtenünk, azaz az interface csak egy metódust tartalmaz.
Az ilyen interface-eket hívják functional interface-nek.
Például ilyen a java.lang.Runnable
.
1 2 3 4 |
|
Az 1.8-as JDK-ban ezeket az interface-eket el is látják a FunctionalInterface
annotációval, bár ez inkább csak jelzés értékű. Ha az interface ténylegesen csak egy absztrakt metódussal rendelkezik, akkor FunctionalInterface
lesz akkor is, ha ez nincs annotálva.
A lambda expression-ök ilyen interface-ek implementálásakor jöhetnek jól, mivel automatikusan implementálják a functional interface abstract metódusát. Nézzünk is egy példát:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
A fenti példában amikor deklaráljuk az fobj
-t, akkor annak egy lambda expression-t adunk értékül (itt nagy szerepet kap az úgynevezett Type Inferencer, azaz a típuskikövetkeztető motor).
Mivel tudjuk, hogy a bal oldalon egy FuncInterface-t kell kapnunk ezért az értékadás jobb oldalán is ilyen típusú elemnek kell lennie.
Mivel ott egy lambda kifejezést találunk, ezért azt megpróbálja a rendszer ráhúzni erre az interface-re, azaz a lambda kifejezésnek illeszkednie kell az abstractFun()
metódus fejlécéhez (void visszatérés, egy int paraméter).
Elemezzük a kifejezést!
A lambda kifejezések általános alakja: (params)->{függvénytörzs}
.
A lambda kifejezés egy az egyben felfogható úgy, mint egy függvény, aminek a paraméterei a kerek zároljelek között vannak, illetve a törzse a kapcsos zárójelek között van.
A fenti példa leírható tömörebben is, mivel egy paraméter esetében elhagyhatom a kerek zárójeleket, de 0 paraméter esetében az üres zárójelpárost ki kell írnom, illetve egy utasítás esetén elhagyhatom a törzset körülölelő kapcsos zárójelpárost
Továbbá, mivel azt is ismerem, hogy az abstractFun
-nak milyen típusú paraméteri vannak, így a paraméter típusokat sem kell kiírnom.
Tehát a fenti lambda kifejezés tömörebben:
1 |
|
A könnyebb megértés végett a fenti példa Anonymous Inner Class megvalósítással:
1 2 3 4 5 6 7 8 |
|
Látható, hogy a lambda kifejezések sokkal tömörebb írásmódot tesznek lehetővé, továbbá a lambdák előnyei:
- viselkedést (függvényt) adhatunk át paraméterként, a kódot adatként tudjuk kezelni
- az így létrehozott függvényeknek nem kell egyik osztályhoz sem tartozniuk
- objektumként passzolhatjuk a függvényeket a rendszerben és szükség esetén meg lehet őket hívni
Lambda expression-ök használatát a JDK széleskörűen támogatja.
A következő példában hozzunk létre egy ArrayList
-et, melyet feltöltünk elemekkel, majd listázzuk ki az elemeket(System.out.println
-el).
1 2 3 4 5 6 7 8 9 10 11 |
|
A legtöbb kollekcióban az 1.8-as JDK óta szerepel a forEach
metódus, mely végigmegy a kollekció elemein és meghívja minden elemre a paraméterül kapott függvényt.
A forEach a következőt várja: Consumer<T> action
.
Ne ijedjünk meg tőle, ő is csak egy simpla functional interface, melynek egy abstract metódusa, az accept
egy T
típusú paramétert vár és nincs visszatérési értéke.
Azért kell generikusnak lenni, mert maga a listánk is generikus, tehát egy Integer-eket tartalmazó lista elemeire csak olyan függvényt hívhatunk meg, ami egy Integer-t kap paraméterül.
A fenti példa alapján írjunk olyan forEach
-et a tömbhöz, mely csak a páros számokat írja ki a konzolra!
1 2 3 4 5 |
|
Felfedezések
Nézzünk körül, hogy a listákon milyen lambda expression-t paraméterül váró metódusok jelentek még meg az 1.8-as JDK-ban.
Mielőtt a következő feladatot megoldjuk még egy fontos dolgot kell észrevennünk és elraktároznunk elménkben. Amikor az interface-nek van visszatérése, de a lambda expression-ben csak egy utasításunk van, akkor annak az egy utasításnak (expression-nek) az értékét (típusát) adjuk vissza eredményül. Amennyiben az interface void-ot ad vissza, akkor viszont nem fog a lambda expression törzse sem visszadni semmit. Amennyiben a lambda törzsében több utasítás található akkor mindig ki kell írnunk a return kulcsszót is, kivéve ha void visszatérést ír elő a functional interface!
Írjunk egy functional interface-t, amelynek egy double apply(double a, double b)
abstract metódusa van! Az interface neve legyen BasicOperator
! Ezt az inteface-t rakjuk egy osztályon belülre, mely osztálynak legyen egy double doCalc(double a, double b, BasicOperator op)
metódusa, amely belül meghívja a paraméterül kapott függvényt a két megkapott paraméterre. A main-en belül hívjuk meg a doCalc
-ot a 4 alapműveletre (2-t helyben adjunk meg paraméterként, kettőre pedig csináljunk referenciát és csak a refernciát adjuk a doCalc
-nak)!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Most, hogy megcsináltuk a saját functional interface-ünket, gondolkozzunk el azon, hogy vajon beépített lehetőségünk van-e ugyanerre? Nézzük meg, hogy a java.util.function
package-ben milyen megoldások vannak erre! A megoldás csak az összeadásra mutat példát, mivel a többire is hasonlóan alkalmazható.
Az egyik opció a DoubleBinaryOperator
functional interface használata, ami két double paramétert vár és egy double-t ad vissza:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
A következő megoldásunk lehet az általánosabb BinaryOperator<T>
használata, mely két T
típusú paramétert vár és T
típussal tér vissza:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Mivel a BinaryOperator<T>
interface a BiFunction<T,U,R>
interface egy specializált változata, ezért ez utóbbival is megcsinálhatjuk ugyanezt, bár ez nem a legszebb (viszont általánosabb helyzetekben jól jöhet):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
java.util.function csomag legfontosabb elemei¶
Ebben a fejezetben bemutatjuk a legfontosabb beépített functional interface-eket, melyekkel gyakran találkozhatunk.
Consumer¶
A Consumer
az egyik leggyakrabban használt elem.
Például az összes Iterable
kollekcióban megtalálható forEach
metódus egy ilyen Consumer
funkcionális interfész megvalósítást vár.
Paraméterek: Egy darab T generikus típusú paraméter
Visszatérés: Nincs
Példa:
1 2 3 |
|
UnaryOperator¶
Egy paraméteren elvégzett műveletet reprezenzál.
Paraméterek: Egy darab T generikus típusú paraméter
Visszatérés: T típusú eredmény
Példa: A listákon hívhatunk egy replaceAll
metódust, mely a lista minden elemére meghívja a megadott függvényt. Az alábbi példában minden számnak az abszolút értékét számítja ki.
1 2 3 |
|
Predicate¶
Valamilyen feltétel eldöntésére használható.
Paraméterek: Egy darab T generikus típusú paraméter
Visszatérés: Nincs
Példa: A kollekciókban megtalálható egy removeIf
metódus, mely a kollekcióból kiveszi az összes olyan elemet, melyre a predikátumot alkalmazva, az igazat adja vissza. A lenti példa a eltávolítja a páros számokat a listából.
1 2 3 |
|
Comparator¶
A Comparator
egy jó példa arra, hogy a régebbi funkcionális interfészekkel is gond nélkül használhatjuk a lambda kifejezéseket.
A Comparator
interface Java 1.2 óta jelen van és az általános rendező algoritmusok pl.: (Collections.sort
) masszívan támaszkodnak rá.
Paraméterek: Két darab T generikus típusú paraméter
Visszatérés: int, ahol a visszatérés azt jelzi, hogy a két paraméter az összehasonlítás szempontjából milyen viszonyban állnak. - < 0: az első paraméter a rendezésben előrébb kerül - = 0: a két paraméter ugyanolyan - > 0: a második paraméter kerül előrébb a rendezésben
Példa: Szeretnénk egy Double lista elemeit a szinuszaik alapján sorba rendezni:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Function¶
Hasonló a UnaryOperator
-hoz, azonban ez utóbbi ugyanolyan típusú értéket ad vissza, mint amilyen típusú a paraméter.
A Function
ezzel szemben lazább feltételrendszert fogalmaz meg, mivel nem kell, hogy ugyanolyan típussal térjünk vissza.
Paraméterek: Egy darab T generikus típusú paraméter
Visszatérés: Egy darab R generikus típusú paraméter
Példa: A Map osztály biztosítja a computeIfAbsent
metódust, mely visszaadja a Map-ből a kulcshoz tartozó értéket, ugyanakkor ha a kulcs még nem létezik a Map-ben, akkor létrehozza azt a megadott Function
segítségével. A példában a map String-ekhez tárolja el azok hosszát.
1 2 3 |
|
Scope-ok¶
A lambda expression-ökre hasonló elemek vonatkoznak, mint az anonim inner classokra, azonban van néhány fontos különbség. Amikor anonymous inner class-t használunk, akkor az egy új scope-ot jelent. Ebbe a scope-ba rakhatunk a megvalósításhoz tartozó field-eket, használhatjuk a this kulcsszót is, amely ilyenkor az inner class példányára vonatkozik. A lambda kifejezések scope-ja viszont a befoglaló scope-ra korlátozódik. Nem tudunk field-eket létrehozni a lambda kifejezésben, csak lokális változókat, illetve a this a befoglaló példányra vonatkozik (például, ha a Test osztály foo metódusában használok egy lambdát akkor a this a Test osztály objektumára vonatkozik).
Best practices¶
- Ahogy már korábbi feladatban is láttuk a
java.util.function
package-ben található functional interface-ekkel kiválthattuk saját interface írását. Ennek megfelelően használjunk standard functional interface-eket! - Saját functional interface-ek írásakor, használjuk a
@FucntionalInterface
annotációt. Ez biztosíthatja, hogy később nem bővíti senki az interface-t úgy, hogy az elveszíti functional interface jellegét. - Inner classok helyett használjunk lambda expression-t ott ahol lehet.
- Próbáljunk elkerülni az olyan metódusok túlterhelését (overload), amik functional interface-t várnak paraméterül:
Első ránézésre ez jó ötlet lehet, de amint használni akarjuk a
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
public interface Processor { String process(Callable<String> c) throws Exception; String process(Supplier<String> s); } public class ProcessorImpl implements Processor { @Override public String process(Callable<String> c) throws Exception { // implementation details } @Override public String process(Supplier<String> s) { // implementation details } }
process
-t akkor nem tudja eldönteni a rendszer, hogy melyik functional interface-re akarjuk hívni, így ezt csak "csúnyán tudjuk megtenni":1 2 3 4
String result = processor.process(() -> "abc"); // helytelen String result = processor.process((Supplier<String>) () -> "abc"); // helyes, de nem szép
- A lambdáink legyenek minél rövidebbek és érthetőek (ha kell akkor szervezzük ki a bennük található kódot metódusokba)
- Kerüljük a lambda kifejezésekben a paraméterek típusának kiírását
- Egy paraméter esetében hagyjuk el a befoglaló kerek zárójeleket
- Egy utasítás esetében ne írjunk kapcsos zárójeleket, illetve
return
utasítást
További haladó best practice-eket találhatunk itt.
Method refernces¶
A method reference témakör szorosan kapcsolódik a lambdákhoz, így érdemes lehet ezeket is megismernünk.
Áttekintés¶
A method reference egy speciális lambda kifejezésként értelmezhető. Arra használjuk, hogy egyszerű lambda kifejezések helyett létező metódusokra hivatkozzunk és ezeket használjuk fel. Például az alábbi lambda kifejezés
1 |
|
megadható a következő metódus refernciával is:
1 |
|
Vegyük észre a ::
operátort!
Használatakor a ::
elé megy az objektum vagy osztály neve, utána pedig magának a metódusnak a neve paraméterek nélkül (magukat a kerek zárójeleket sem kell kirakni hiszen azt híváskor használjuk).
A metódus referenciát elnézve az alábbi kérdések merülhetnek fel bennünk:
- Ez most miért lenne szebb mintha lambdát használunk?
- Mi történik a metódus argumentumaival?
- Miért lehet ez egy valid kifejezés?
- Hogyan konstruálhatok valid metódus referenciát?
A metódus referenciákat csak ott alkalmazhatjuk, ahol a lambda kifejezések egy darab függvényhívást tartalmaznak. Például a fenti lambda, csak egy System.out.println
hívást tartalmazott.
Ezek alapján a következő megállapítást tehetjük:
- Anonim osztályok helyett használhatunk lambda expression-t, ha a megvalósítandó interface-nek egy abstract metódusa van
- Lambda kifejezés helyett használhatunk method reference-t, ha a lambda kifejezés csak egy metódushívást tartalmaz
4 féle method reference létezik, aszerint, hogy mire fogjuk a referenciát. Mivel elég nehéz magyarra fordítani ezeket, így angol megnevezésüket is használjuk:
- static method reference (statikus metódus referencia)
- constructor method reference (konstruktor referencia)
- instance method of an object of a particular type (egy adott típusú objektum egy metódusára)
- instance method of a particular object (egy objektum metódusára referencia)
Static method reference (Statikus metódus referencia)¶
A lambda kifejezés, melyet jelen esetben metódus referenciára cserélhetünk az alábbi formát követi:
1 |
|
Ez metódus referenciával a következőképpen írható:
1 |
|
A metódus referenciáknál a ::
operátort használjuk az osztály neve és az osztálymetódus között nem pedig a '.' operátort.
Ezen felül az argumentumokat elhagyjuk (magukat a kerek zárójeleket is).
Nézzünk egy faladatot a könnyebb megértés végett!
A feladat, hogy egy listában lévő számoknak vegyük az abszolútértékét!
A listán való végigiterálásra már láttuk a forEach
-et, viszont ez nem módosítja a lista elemeit, mely a Consumer<T>
funkcionális interface megvalósításából adódik, hiszen az void-ot ad vissza.
A forEach
helyett ilyen esetekben használhatjuk a replaceAll
metódust, mely egy UnaryOperator<T>
funkcionális interface megvalósítást vár.
A UnaryOperator
egy paramétert vár és ugyanolyan típusú a visszatérése, mint a paramétere.
Erre a sémára illeszkedik a Math.abs
függvény mindegyik megvalósítása (külön van minden primitív típusra, ami számot reprezentál).
Nézzük a megoldást!
1 2 3 4 |
|
A megoldásban a második sorban van a lényeg.
A statikus metódus referenciáknál a ::
operátort kell minden esetben használnunk, melynek jelentése a fenti példában: a Math
osztályon belüli abs
metódus referenciáját szeretném.
A fenti lényegi sor lambda megfelelője a következő volna: numbers.replaceAll( n -> Math.abs(n))
.
A paramétereket nem kell átadnunk, azok a színpad mögött automatikusan átadódnak, amikor ténylegesen meghívódik a metódus.
A fenti példában a Math.abs
-ról tudjuk, hogy egy darab paramétert vár és ugyanolyan típusú a visszatérése mint a paraméter.
Ez pedig pontosan illeszkedik a UnaryOperator
által előrtakra.
Segíthet a megértésben, ha megnézzük, hogy a replaceAll metódus belül, hogyan használja fel a paraméterben kapott UnaryOperator
megvalósítást.
Készítsünk egy saját statikus függvényt, mely double-t vár és double-t ad vissza és a következőt számolja ki: a számot megszorozza 3-mal, majd hozzáad 42-t és gyököt von ezen a részeredményen. Ezt afüggvényt hívjuk meg egy lista összes elemére!
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Konstruktor referenciák¶
Ebben az esetben a lambda kifejezés, melyet le szeretnénk cserélni a következő formájú:
1 |
|
ebből metódus referencia a következő formában néz ki:
1 |
|
Példa:
1 2 3 |
|
Instance method of an object of a particular type¶
Ebben az esetben a kiváltandó lambda kifejezésünk a következő formájú:
1 |
|
ahol paraméterül egy objektumot adunk át, melynek valamelyik metódusát hívjuk meg a megadott paraméterrel (paraméterekkel). Ez a következőképpen néz ki metódus referencia használatával:
1 |
|
Ez a transzformáció már nem annyira triviális, mivel nem az objektumot, hanem annak típusát adjuk át. Továbbá a második paraméter a színfalak mögött kerül átadásra.
Vegyük a következő példát:
1 2 3 |
|
compareToIgnoreCase
egy példánymetódus, melynek fejléce a következő formájú: int compareToIgnoreCase(String str)
.
A fenti példában a sort egy Comparator<String>
-t vár, ami viszont a következő formájú int compare(String o1, String o2);
.
A metódus referenciánk eredményeképpen az első paraméter lesz maga az objektum, a második a paraméterül kapott String.
Instance method of a particular object (egy adott típusú objektum egy metódusára)¶
Ebben az esetben a kiváltandó lambda kifejezésünk a következő formájú:
1 |
|
Ennek a metódus reference formája a következő lesz:
1 |
|
1 2 3 4 5 6 7 8 9 10 11 |
|
Best practice - metódus referenciák¶
- Ahol lehet a lambdák helyett használjunk metódus referenciákat