Java fejtörők – haladó

Java fejtörők

Java fejtörőkJava fejtörők – csapdák, buktató, és szélsőséges esetek. Ez egy könyv címe, amelynek szerzői J. Bloch és N. Gafter. Magyar nyelven a Kiskapu Kft. jelentette meg. A 2010-es magyar kiadás a 2005-ös angol nyelvű kiadás fordítása. A könyv weboldaláról (http://www.javapuzzlers.com), letölthető a 95 fejtörőhöz tartozó mintapéldák gyűjteménye, és elérhető a 270 oldalból minta fejezetként 28 oldalnyi tartalom 9 fejtörővel és azok részletes magyarázataival.

A két részre bontott blog bejegyzés a könyv anyagából válogatva készült el. Az első rész a bevezetés. Ez a második rész, haladó szintű példákkal. Néhány példát továbbfejlesztettem.

7. fejtörő: Mit ír ki program a konzolra?

Kivételkezelés nélkül arra számítanánk, hogy a Hello World! nem jelenik meg a konzolon, mert a wordHard() metódus feltétel nélkül rekurzív módon folyamatosan újrahívja önmagát és az emiatt keletkező StackOverflowError hibával elszáll a program. A kivételkezelés természetesen módosítja a program működését.

Ha azt feltételezzük, hogy minden Throwable utódosztályból futás közben létrehozott objektum kivételkezeléssel elkapható, akkor végtelen ciklusnak tűnik a vezérlés, hiszen a try blokkban hibát okozó metódushívásra a finally blokkban újra ugyanannak a metódusnak a meghívásával reagálunk, amelyik korábban a hibát kiváltotta. Ez a gondolatmenet tévút. A kulcsszó a rekurzív vezérlést megvalósító verem adatszerkezet mérete. Részletes indoklás a blog bejegyzés végén található.

8. fejtörő: Mit ír ki program a konzolra?

Természetesen NullPointerException-re gyanakszunk, pedig a program hibátlanul működik. A kulcsszó a statikus metódusok minősítése, vagyis annak jelölése, hogy melyik osztálytól vagy objektumtól kérjük annak végrehajtását. Részletes indoklás a blog bejegyzés végén található.

9. fejtörő: Mit ír ki a program a konzolra?

Arra számítunk, hogy a kiírás 1999-12, de ehelyett 2000 1-et látunk a konzolon. Tudjuk, hogy a Date osztály jó része már deprecated és ezen próbáltak javítani a Calendar osztállyal. Bár ne tették volna. A kulcsszó az ős dátumkezelést megvalósító API rejtelmeiben van. Részletes indoklás a blog bejegyzés végén található.

10. fejtörő: Mit ír ki a program a konzolra?

Nem tűnik egyértelműen eldönthetőnek a helyzet, ezért szintaktikai hibára gyanakodhatunk. Azonban a forráskód helyes, a program futás közben sem dob kivételt/hibát és a konzolon megjelenik a White. Szokatlan, hogy nagybetűvel konstansokat szokás jelölni, pedig nincs erre utaló final a forráskódban. A kulcsszó a sorrendiségen van, ha ugyanabban a hatókörben/blokkban van azonos nevű változó és típus/osztály. Részletes indoklás a blog bejegyzés végén található.

11. fejtörő: Mit ír ki a program a konzolra?

A program helyes és egyértelműnek tűnik. A konzolra az s1 szövegtömb elemei kerülnek ki véletlenszerűen összekeverve. Finomítsunk a kérdésen. Vajon minden lehetséges permutáció azonos eséllyel fordul elő? Ha ez a kérdés egyáltalán felmerül, akkor a válasz nyilván nem. A kulcsszó most egy kis matematika. Részletes indoklás a blog bejegyzés végén található.

12. fejtörő: Mit ír ki a program a konzolra?

Ez a forráskód nem úgy működik, ahogyan a könyv írja. Meglepő módon nem a [3, 1, 4, 1, 5, 9]-et adja, hanem az [1, 1, 3, 4, 5, 9]-et. Némi indoklás a blog bejegyzés végén található.

Részletes indoklások

  • 7. fejtörő: ha a try blokkban folyamatosan meghívja saját magát a workHard() metódus, akkor előbb-utóbb betelik a verem. Ekkor a finally blokkra kerül a vezérlés, ahonnan újra hívja saját magát a workHard() metódus. Persze követni kell, hogy a rekurzió végrehajtása során a lefelé haladó vagy a felszálló ágon vagyunk és nem mindegy, hogy melyik szinten. A háttérben egy teljes bináris fa bontakozik ki, amelynek mélysége azonos a verem méretével, mélységi korlátjával. Ezt a teljes bináris fát járja be a program, azaz mélységi fabejárás. Egy n mélységű teljes bináris fa elemeinek száma 2n-1. A verem mérete a virtuális gép beállításaitól függ, több ezer mélységű is lehet. Végtelen ciklusról tehát nincs szó. Ugye milyen izgalmas? További részletek a könyv 100-102. oldalán találhatók.
  • 8. fejtörő: a végrehajtás kiértékeli a statikus greet() metódus hívásának minősítő kifejezését, de figyelmen kívül hagyja a kapott értéket. A metódust végrehajthatnánk Null.greet()-ként vagy közvetlenül (minősítés nélkül) meghívva is. További részletek a könyv 122-124. oldalán találhatók.
  • 9. fejtörő: a Date osztály a hónapokat nulla bázissal kezeli, ezért csak 0-11-ig “van értelme”. Számíthatnánk a tömb vagy szöveg túlindexelésénél tapasztaltakhoz hasonlóan kivételre, de nem ez történik. A 12. hónap a következő év első/nulladik hónapját jelöli. Ezért látjuk a konzolon a 2000-et, amit egy kötőjel követ. A Date.getDay() deprecated metódus pedig a dátumobjektumban tárolt nap adott héten (nem hónapban!) elfoglalt helyét adja meg, ami nullával, azaz vasárnappal indul. Tehát a konzolon megjelenő 1 nem a 2000. januárt jelenti, hanem azt, hogy a 2000. január 31. hétfőre esik. Aki ezek után meri használni a régi dátumkezelő API-t, magára vessen. További részletek a könyv 141-143. oldalán találhatók.
  • 10. fejtörő: ha ugyanabban a hatókörben/blokkban van azonos nevű változó és típus/osztály, akkor a változó neve az elsődleges. Ha betartjuk a névadási konvenciókat ( ClassName, objectName, CONSTANT_NAME), akkor nem adódhatnak ilyen gondok. Még egy csavar van: ha az előző elnevezési módosításokat megtesszük, akkor a program a Black-et írja ki a konzolra. További részletek a könyv 161-163. oldalán találhatók.
  • 11. fejtörő: konkrét esetből általánosítunk. 4 elemre a ciklus 4-szer hajtódik végre és minden lépésben kiválaszt egyet a 0 és az 3 indexű elemek közül, ami 44=256-féle lehetséges eredményt ad. Ha az r objektum jól működik, akkor az egyes futások esélye/valószínűsége megegyezik. 4 elemű tömb elemeinek 4!=24 (faktoriális) féle permutációja (lehetséges sorrendje) van. Mivel a 256 nem osztható 24-gyel, így biztos, hogy a shuffle() metódus bizonyos permutációkat gyakrabban állít elő, mint másokat. Általánosan: nn nem osztható n!-sal, ha n>2 egész szám. Vajon mi történik, ha egy 52 lapos pakli kártyát keverünk össze? Vajon milyen érdekességet vet ez fel? Minden poént nem lövünk le itt a blogban. További részletek a könyv 228-232. oldalán találhatók.
  • 12. fejtörő: ez a tankönyv utolsó példája. A felvetett gondolat nagyon frappáns: az összehasonlító rész „ha fej, én nyerek, ha írás, te veszítesz” tüneteitől szenved. További részletek a könyv 232-233. oldalán találhatók.

 

Állásinterjúkon időnként visszaköszönnek hasonló fejtörők, de ezekkel óvatosan kell bánni. Egy programozási nyelv „joghézagainak”, buktatóinak, szélsőséges eseteinek ismerete a könyv szintjét elérő ismeretanyaggal nem lehet elvárt még egy meghirdetett senior pozíció esetén sem. Ezen fejtörők ismerete (vagy nem tudása) egy jelöltről nem árulja el a mindennapokban használható szakmai tudás meglétét/hiányát. De nyilván aki szakmailag folyamatosan fejlődik és mindenféle keretrendszert alkotó forráskódokban turkál, elemez, előbb-utóbb találkozik ezekkel/ilyenekkel.

Tanfolyamainkon nem kifejezetten foglalkozunk hasonló problémákkal, de azért időnként feszegetjük a határokat. Természetesen részletesen indokoljuk, ha előkerül valamilyen hasonló eset. Általánosságban nem célunk, hogy extrém eseteken keresztül, a programozási nyelv gyenge pontjaira kihegyezve oktassuk a Java programozási nyelvet.

Stream API lambda kifejezésekkel

lambda kifejezés logo

lambda kifejezés logoKorábban blogoltunk már a Stream API-ról és a lambda kifejezésekről: Ismerkedjünk lambda kifejezésekkel! Most másképpen közelítve újra foglalkozunk a témával.

Tanfolyamainkon szinte minden adatszerkezethez, tömbhöz, kollekcióhoz, fájlkezeléshez kötődő témakörben használjuk mindkettőt. Áttekintjük az ezekhez szükséges minimális verziószámot, a szintaktika fejlődését, az együttes használat elvi és gyakorlati lehetőségeit. A szükséges alapfogalmakat definiáljuk: hozzárendelési szabály, funkcionális interfész, metódus referencia, alapértelmezett metódusok, típus kikövetkeztetés képessége, generikus és funkcionális programozás. párhuzamos adatfeldolgozás lehetőségei.

Összehasonlításokat is végzünk: a lambda előtti verziók lehetőségei, korlátai, tipikus lambda hibák, mikor mit érdemes és mit nem érdemes használni, paraméterek típusait megadjuk vagy elhagyjuk, hagyományos kollekciós műveletek (azért a generikusság előtti időkre már nem térünk ki) és folyam feldolgozás (adatforrás meghatározása, közbenső és végső műveletek).

Most azokat a Stream API-hoz és lambda kifejezésekhez kötődő bevezető mintapéldákat ismertetjük, amiket részletesen elemzünk tanfolyamaink szakmai moduljának kontakt óráin. Ezek közül közösen meg is írunk néhányat, kombinálunk is néhányat egy-egy összetett adatfeldolgozó művelet megvalósítása során. Programozási tételenként specifikáljuk a feladatokat és megmutatunk néhány megoldást.

1. Adatforrás

100 db olyan véletlen kétjegyű számot állítunk elő generikus listában, amelyek között biztosan előfordul legalább egyszer a 80.

2. Elemi programozási tételek

2.1. Sorozatszámítás

Kiírjuk, hogy mennyi a listában lévő számok összege:

2.2. Eldöntés

Két kérdésre adunk választ. Van-e a listába lévő számok között 35 (konkrét elem), illetve páros (adott tulajdonságú elem)?

2.3. Kiválasztás

Kiírjuk, hogy a biztosan előforduló (legalább 1 db közül balról az első) 80, hányadik helyen (index) található meg:

2.4. Keresés

Keressük a 35-öt az eldöntés és a kiválasztás összeépítésével:

2.5. Megszámolás

Kiírjuk, hogy hány db öttel osztható szám (adott tulajdonságú elem) található a listában:

2.6. Szélsőérték-kiválasztás

Kiírjuk a listában lévő legkisebb számot (értéket, nem indexet):

3. Összetett programozási tételek

3.1. Másolás

Készítünk egy másolatot a lista elemeiről (közben esetleg mindegyiket meg is változtathatjuk):

3.2. Kiválogatás

A listában lévő számok közül kiválogatjuk az öttel osztható számokat:

3.3. Szétválogatás

Külön-külön szétválogatjuk a listában lévő páros és páratlan számokat:

3.4. Unió

A korábban szétválogatott páros és páratlan számokat tartalmazó halmazok unióját állítjuk elő:

3.5. Metszet

A korábban szétválogatott páros és páratlan számokat tartalmazó halmazok metszetét állítjuk elő:

3.6. Összefésülés

A korábban szétválogatott páros és páratlan számokat összefésüljük:

4. A program eredménye a konzolon

A bejegyzéshez tartozó teljes forráskódot ILIAS e-learning tananyagban tesszük elérhetővé tanfolyamaink résztvevői számára.

A feladat a Java SE szoftverfejlesztő tanfolyam, a Java EE szoftverfejlesztő tanfolyam és a Java adatbázis-kezelő tanfolyam szakmai moduljának több alkalmához és az orientáló moduljának 1-4. óra: Programozási tételek alkalmához is kötődik. A Stream API-val és a lambda kifejezésekkel sokszor foglalkozunk.

Korábban is blogoltunk már a Stream API-ról és a lambda kifejezésekről: Ismerkedjünk lambda kifejezésekkel!

Ismerkedjünk lambda kifejezésekkel!

lambda kifejezés logo

lambda kifejezés logoA Java 8-tól használhatunk lambda kifejezéseket, amivel hatékonyabban, rövidebben és könnyebben valósíthatunk meg tipikus műveleteket.

Korábban általában az eseménykezelést globálisan (interfészek implementálásával), vagy lokálisan (névtelen interfész implementálásával) oldottuk meg, illetve besegítettek adapterek is. Sok- és többféle eseménynél ez a forráskódunk elaprózódásához vezetett, ami nehézkes olvashatóságot és karbantarthatóságot eredményezett.

A lambda kifejezés egy olyan kódrészlet, amelyben valamihez hozzárendelünk valamit, például egy metódus paraméteréhez a végrehajtandó forráskódot ( x -> y). Ehhez építünk a funkcionális interfészekre és a metódus referenciákra (szintén Java 8-tól), illetve a típus kikövetkeztetés képességére is (Java 7-től).

A kiválogatás programozási tételt valósítjuk meg többféle implementációval, felhasználva a Java nyelv újdonságait, és a fentieken kívül még a Stream API-t is.

Adatforrás

Először is kellenek adatok, hiszen azokat dolgozzuk fel. Egy Termek osztályú egyszerű POJO-val dolgozunk, nev és ar tulajdonságokkal, generált konstruktorral, getter metódusokkal és toString()-gel. Az adatforrást biztosító absztrakt Lista ősosztályban a POJO-kból felépítünk egy termekLista nevű generikus listát (például CSV vagy XML fájlból beolvasva az összetartozó adatokat) – listaFeltolt() eljárás – és implementálunk egy univerzálisan használható listaKiir(String uzenet, List termekLista) eljárást is.

Örökítünk három utódosztályt a Lista osztályból, amelyek különbözőképpen dolgozzák fel a termekLista-t, bemutatva a fejlődés útját, illetve a rendelkezésre álló lehetőségeket.

Válogassunk a termékek közül négyféleképpen és adjuk vissza azon termékeket, amelyek:

  • limit alatti áron kaphatók,
  • ára limit1 és limit2 közé esik (zárt intervallumban),
  • neve adott szöveggel kezdődik (kis- és nagybetű különbözik),
  • neve adott szöveget tartalmaz (kis- és nagybetű nem különbözik)!

1. változat

Hagyományos megközelítéssel a fentiek megvalósításához külön egy-egy függvény tartozik, ahogyan az alábbiakban látható:

A termekListaLimitAr1() függvényben látható ötféle lehetőség a kiválogatásra a termekLista-ból:

  • //1: hagyományos, index alapú változat,
  • //2: iterátorra közvetlenül építő változat,
  • //3: bejáró ciklus, iterátorra közvetve építő változat,
  • //4: Stream API-ra építő változat, kiválogatás lambda-kifejezéssel ( filter), a megmaradó termékekre végrehajtandó forEach művelet lambda kifejezéssel,
  • //5: Stream API-ra építő változat, kiválogatás lambda-kifejezéssel ( filter), a megmaradó termékeket összegyűjtő/leképező collect művelettel.

Jól megfigyelhető, hogy négy függvény vázszerkezete megegyezik, és csupán a filter-ben található lambda-kifejezések különböznek. Ez a megoldás meglehetősen redundáns, nem általánosítható, valamint további műveletek megvalósítása további függvények implementálását igényli.

2. változat

Őrizzük meg a négyféle funkciót, sőt tegyük lehetővé, hogy ez tetszőlegesen bővíthető legyen. Használjunk saját generikus, funkcionális Feltetel interfészt saját döntés megvalósítását biztosítani tudó implementálandó teszt() függvénnyel, az alábbiak szerint:

A termekListaFeltetel() függvény paramétere a saját Feltetel interfészünket implementáló névtelen osztály példánya, amely felhasználható:

  • //6: ciklusban egyszerű feltételként,
  • //7: Stream API filter műveletében megadott lambda-kifejezésben,
  • //8: a listaKiir() metódusban paraméterként átadva névtelen osztály példányaként,
  • //9-től: a listaKiir() metódusban paraméterként átadva lambda-kifejezésként.

Látszik, hogy többféle kiválogató művelethez egyetlen implementált függvény szükséges. Ez a megoldás már jóval általánosabb.

3. változat

A saját interfész helyett használjuk fel a beépített Predicate generikus, funkcionális interfészt, építve annak test() függvényére az alábbiak szerint:

Belépési pont

Végül következzen a közös belépési pont, amelyben tetszőlegesen aktiválható és tesztelhető mindhárom változat működése:

Mit ír ki a program a konzolra?

A bejegyzéshez tartozó teljes forráskódot ILIAS e-learning tananyagban tesszük elérhetővé tanfolyamaink résztvevői számára.

A feladat a Java SE szoftverfejlesztő tanfolyam szakmai moduljának 21-24. óra: Objektumorientált programozás 2. rész, 25-28. óra: Objektumorientált programozás 3. rész, valamint a Java EE szoftverfejlesztő tanfolyam szakmai moduljának 9-12. óra: XML feldolgozás alkalmaihoz kötődik.

Máskor is blogolunk a témakörben: Stream API lambda kifejezésekkel.