fbpx

Java 9 újdonságai – 5. rész

Collection factory metódusok

A Collection factory metódusok olyan új statikus metódusok, amik segítségével egyszerűbben, tömörebben és biztonságosabban inicializálhatunk collection adatstruktúrákat.

Előzmények

A Collections Framework létezése óta – vagyis már a JDK 1.2 óta (1998) – sok fejlesztőt elgondolkodtatott, hogyan lehet egy ArrayList-et egy sorban inicializálni. Bár a Java nyelvi támogatást nyújt a String literálok esetén, nem ez a helyzet a Collections Framework-nél.

Szimpla kóderek

Bár Collections literálok nincsenek a Java-ban, ahogy más referenciatípusoknál is, itt is jogos az igény az inicializálásra. Erre a legnyilvánvalóbb – mondhatni triviális – megoldás a következő:

List<String> shoppingList = new ArrayList<>();
shoppingList.add("1 kg kenyér");
shoppingList.add("1 l tej");
shoppingList.add("10 db tojás");

Bár ez a megoldás helyes, mégis nagyon bőbeszédű. Tudsz ennél jobbat?

Leleményes kóderek

A fenti megoldás kicsit túl bőbeszédű, indokolatlan mennyiségű kódot kell leírni ahhoz, hogy egyszerűen alapértékre állítsunk be egy listát. Létezik egyszerűbb megoldás:

List<String> shoppingList = Arrays.asList("1 kg kenyér", "1 l tej", "10 db tojás");
shoppingList.add("1 kg liszt"); // se hozzáadni
shoppingList.remove(0);         // se törölni nem tudunk

Így már sikerült egy sorossá tenni ezt az inicializációt, de érdemes megjegyezni, hogy az előző megoldással ez nem ekvivalens, van egy jelentős különbség az így alapértékre állított változó használatában. Ez a különbség névlegesen az, hogy az így példányosított lista nem méretezhető át, vagyis nem adhatunk hozzá újabb elemet és nem is törölhetünk belőle. Ha makacsak vagyunk és mégis megpróbálnánk, akkor futásidőben a virtuális gép egy UnsupportedOperationException-nel lep meg minket.

Figyelmesen elolvasva az Arrays.asList(T… a) metódus javadoc-ját fény derül ennek okára. Itt elmagyarázzák nekünk, hogy ez a metódus a könnyebb átjárhatóságot hivatott biztosítani a régebbi tömb alapú API-k és az újabb Collection alapú API-k között. A visszaadott lista objektumon végzett változtatások „átíródnak” a lista alapját szolgáló tömbbe. Érthető tehát, hogy mivel a tömbök is létrehozásuk után fix méretűek, így az Arrays.asList(T… a) metódus által visszaadott lista is fix méretű lesz.

Még leleményesebb kóderek

Egy egyszerű csavarral az előző megoldásból kerekíthetünk egy módosítható listás megoldást, ami első bőbeszédű változattal egyenértékű funkcionalitásban:

List<String> shoppingList = new ArrayList<>(Arrays.asList("1 kg kenyér", "1 l tej", "10 db tojás"));
shoppingList.add("1 kg liszt"); // tudunk hozzáadni
shoppingList.remove(0);         // tudunk törölni

Ez nagyszerű, de kezdünk megint kicsit túl bőbeszédűek lenni. Túl sok billentyűt kell leütni ahhoz, hogy inicializáljuk a listánkat. Vajon van jobb megoldás?

Túl leleményes kóderek

Ha alaposan ismered a Java nyelv alapvető építőköveit, akkor bizonyára hallottál a példány inicializátorokról (instance initializer). Ha ezt vegyítjük egy kis névtelen belső osztállyal, akkor eredményül egy extra trükkös, első ránézésre újszerű szintaxist kapunk:

List<String> shoppingList = new ArrayList<String>() {{add("1 kg kenyér"); add("1 l tej"); add("10 db tojás");}};
shoppingList.add("1 kg liszt");
shoppingList.remove(0);

Elértük, amit szerettünk volna, egy sorban inicializáltuk a listánkat. Még mindig van néhány feleslegesnek tűnő karakter és elég sok szintaktikai elem. Ezt a megoldást szokták dupla kapcsoszárójeles inicializálásnak (double brace initialization) hívni. Bár valóban egysoros lett ez a művelet, számos dolog történik a háttérben. Vegyük sorra:

  1. Először is létrejön az ArrayList osztályból leszármaztatott névtelen osztály.
  2. Ennek az új névtelen osztálynak létrejön egy példánya.
  3. Mivel a belső kapcsoszárójel az egy példány inicializátor blokk, ezért ez a kódblokk lefut a 2. lépésben történő példányosodás során.
  4. Az utasításblokk az add() példány metódust meghívja egymás után különböző paraméterekkel, ami mivel nincs override-olva, ezért a szülő osztályból (ArrayList) megörökölt publikus add() metódust hívja meg, ami hozzáadja a paraméterként kapott elemeket a listához.
  5. A most létrejött névtelen osztály példányának referenciáját hozzárendeli a shoppingList változóhoz.

Elég sok dolog végbemegy egyetlen sorban. Ráadásul a névtelen belső osztály miatt a Java fordító külön class fájlokat gyárt a fájlrendszeren. Ha sokat használjuk ezt a trükköt, akkor a túl sok plusz class fájl miatt a programunk lelassulásának lehetünk tanúi. Az objektumorientáltság egyik alapműveletét a leszármaztatást itt nem arra használjuk, amire hivatott, mondhatni visszaélünk vele.

Tudván ezeket kijelenthetjük, hogy bár érdekes módszer ez, mégis kerülendő, anti-patternnek tekintendő.

Java 9 Collection factories to the rescue!

A Collection factory metódusok, amiket a Java 9-ben vezetnek be, elegáns megoldást nyújtanak:

List<String> list = List.of("1 kg kenyér", "1 l tej", "10 db tojás");
Set<String> set = Set.of("1 kg kenyér", "1 l tej", "10 db tojás");

Ez már tényleg sokkal jobb, mint az eddigi megoldások. Nincs túl sok szintaktikai elem és nem is kell sokat gépelnünk. Mégis van egy kis probléma, ami persze a felhasználók szempontjából igazából lényegtelen, de jó tudni róla.

Ennek az új of metódusnak pontosan mennyi paramétert is tudunk átadni? A példánkban épp 3-at adunk át, de mi van, ha valakinek kevesebb vagy több kell?

A Java 5-ös verziójában bevezették a változó hosszúságú argumentumokat (varargs). Ez lényegében egy szintaktikai édesítőszer, ami lehetővé teszi, hogy egy metódusnak ne tömböt kelljen átadnunk, hanem vesszővel felsorolhassuk az argumentumokat. A következő két metódus egyenértékű:

void method() {
    String[] shoppingArray = { "1 kg kenyér", "1 l tej", "10 db tojás" };
    methodWithArray(shoppingArray);
    methodWithVarargs(shoppingArray);
    methodWithVarargs("1 kg kenyér", "1 l tej", "10 db tojás");
}

void methodWithArray(String[] shoppingArray) {
}

void methodWithVarargs(String... shoppingArray) {
}

A változó hosszúságú argumentumoknak persze vannak megkötései, például hogy az ilyen paraméter csak a paraméterlista végén szerepelhet, illetve egy metódusnak csak egy ilyen paramétere lehet. Amikor változó hosszúságú argumentumokat váró metódust hívunk, akkor a háttérben létrejön egy tömb, ami hordozza az elemeinket. Ez persze némi teljesítménybeli visszaesést okoz, főleg ha sokszor futtatunk ilyen kódot, például ciklusban.

Hogy ezt elkerüljék, a Java fejlesztői az új of metódusnak nem csak egy változatát készítették el, hanem másik 10 overloadolt változatát is, ahol az első egy paramétert vár, a második kettőt, a harmadik hármat, és így tovább. Így amikor egy, kettő, három … tíz argumentummal hívjuk, akkor a megfelelő overloadolt változat fut le, és így elkerüljük a varargs esetén fellépő overheadet. Cserébe viszont „teleszemetelték” a Java core kódját egy csomó redundánsnak tekinthető résszel, így ezentúl több kódot kell majd karbantartaniuk az elkövetkezendő verziókban. A Java-t fejlesztő szakemberek ezt a megközelítést tartották célszerűnek.

De nem csak a List és Set kapott új factory metódusokat, hanem a Map is:

Map<String, Integer> shoppingMap = Map.of("kenyér", 1, "tej", 1, "tojás", 10);

Tíz argumentumig itt is overloadolt metódusokat használhatunk a kulcs-érték párok felsorolásához, de efelett szintén változó hosszúságú argumentummal rendelkező factory metódust tudunk hívni, amit Map.Entry<K, V> objektumokba kell csomagolnunk, amihez kapunk cserébe statikus entry() metódust:

Map<String, Integer> shoppingMap = Map.ofEntries(entry("kenyér", 1), entry("tej", 1), entry("tojás", 10));

Az új statikus Collection factory metódusok fontos tudnivalói

Nem csak a bőbeszédűség csökkentése volt a cél, bár kétségtelenül nagy előny, hogy tömörebben meg tudjuk fogalmazni a kódunkat és könnyebb is átlátni. A programozói hibák is csökkenthetők néhány általános érvényű szabály bevezetésével, amit az elmúlt években már eszközöltek is, és amire most is ügyeltek.

Megfigyelték, hogy a programhibák jelentős százaléka a null értékek helytelen használatából ered. Épp ezért a collection-ös adatstruktúrákban nem használhatjuk a null-t elemként. Ezt ezek az új statikus factory metódusok se engedélyezik.

A másik hibacsökkentési lehetőség az immutabilitás (módosíthatatlanság) bevezetése. Ezek a Collection factory metódusok módosíthatatlan adatstruktúrákat produkálnak. Ez azért jó, mert sok hiba abból fakad, hogy egy módosítható adatstruktúrát a program hívási láncolata mentén végigpasszolva valahol akaratlanul módosítjuk.

Egy másik nagy előnyt azt a Map.ofEntries() megvalósítása nyújtja, ugyanis itt nem tudjuk elrontani a kulcs-érték párok párosítását, ami csak futásidőben derülne ki, mert már fordítási időben hibát kapunk és hamar kijavíthatjuk azt.

A HashSet-ek és HashMap-ek elemei mindig is látszólag véletlenszerű iterálási sorrendet eredményeztek, de volt egy determinisztikus voltuk, hogy ugyanazt a sorrendet követték minden programfutás esetén. Az új immutábilis collection-ök viszont minden futásnál más és más sorrendben iterálnak végig az elemeiken, így ha hibásan egy kódrészlet a rendezetlen elemek valamilyen sorrendjére támaszkodott, akkor most az ilyen hibákra hamar fény fog derülni (ezt hívjuk fail fast viselkedésnek).

Konklúzió

Összességében az új Collection factory metódusok egy nagyszerű új lehetőséget biztosítanak a programozók számára, amivel tömören tudják a kódjukat megfogalmazni. Érdemes tisztában lenni a részleteikkel, mint például, hogy immutábilis példányokkal térnek vissza, tiltják a null használatát és az iterálási sorrendjük is változó lehet.
A JEP 269-ben olvashatod el angolul a motivációt a megvalósítás mögött.
Ha érdekel a Java 9 többi újdonsága is, akkor azokat a Java 9-es blog posztjainkban olvashatod.

Ha tetszett, oszd meg!

Szólj hozzá!