Kategória: Java külső könyvtárak.
Áttekintés
Amint azt láthattuk, a Java nyelv standard könyvtárkészlete messze többet tud, mint a programozási nyelvek többsége, pl. a C++. Viszont sok esetben ez sem elég: hiányos, félre csúszott. Gondoljunk pl. a következőkre:
- Jó, hogy van a gyűjtemény keretrendszer, de úgy tűnik, leragadt a kezdeti adatszerkezeteken. A gazdagsága ellenére számos egyéb hiányzik: az a halmaz, amely ugyanazt az elemet többször is tartalmazhatja; az olyan asszociatív tömb, melyben egy kulcshoz több értéket is rendelhetünk; a megfordítható asszociatív tömb stb. Ráadásul sok esetben nehézkes a használata; pl. egyetlen utasítással csak igen nyakatekerten lehet létrehozni bármilyen adatstruktúrát.
- A beépített könyvtárak gyakran aránytalanul hosszú, nehezen áttekinthető, nehezen megjegyezhető és sok esetben logikátlan felépítést eredményeznek. Gondoljunk csak a fájl kezelésre (ami ma sem egyszerű, kezdetben horror volt), az e-mail küldésre, az adatbázis kapcsolatra stb. Milyen jó lenne, ha tényleg csak annyit kellene írni, hogy ezt a szöveget ebbe a fájlba írd, azt a szöveget arra az e-mail címre küldd, stb. és az ismétlődő részek valamint a hibakezelés része el lenne fedve a programozó elől.
- A Java szövegkezelője alapvetően jó, de némi tapasztalattal rájövünk, hogy lehetne jobb is. Szinte minden projektben van egy StringUtil osztály, melyben a projekt specifikus eljárásokon túl szinte teljesen általános műveletek is ismétlődnek. Pl. a beépített feldaraboló a vegytiszta esetben jól működik, de ha ennél picivel is többet szeretnénk, már az eredményt kell módosítani.
A programozó azt gondolná, hogy ezek a hiányosságok, melyek közösek számos projekten, előbb-utóbb bekerülnek a standard könyvtárak közé. A gyakorlat sajnos nem ezt mutatja; a kiegészítés nehézkesebbnek tűnik mint azt gondolnánk. Az egyes projektek saját megvalósulásain túl megjelentek olyan megvalósítások is, melyek már szinte szabványnak tekinthetők olyan értelemben, hogy számos kérdésre az a válasz, hogy a Java nyújtotta alap keretrendszerben ezt sajnos nem lehet megvalósítani, viszont használjuk ezt vagy azt. Ebben a szakaszban az Apache Commons könyvtárgyűjteményt, valamint a Google által kifejlesztett Guava nevűt fogjuk megnézni. Ezeknek a könyvtáraknak a puszta léte a bizonyítéka annak, hogy a Java nem elég rugalmas.
A Guava a Google válasza a Java standard könyvtárainak nyilvánvaló hiányosságaira. Ahhoz, hogy használni tudjuk, elérhetővé kell tennünk a programunk számára a megfelelő jar fájlt. Ha Maven-t használunk, akkor a következő sorokat kell beszúrnunk a projektünk pom.xml-ébe (célszerűen az aktuálisan legfrissebb verziót használva, ami az írás pillanatában a 28):
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
Ennek a szakasznak a célja az, hogy ízelítőt adjon a Guava lehetőségeiről, és nem célja a Guava részletes ismertetése. Ez utóbbira kiváló forrás a követhező wiki oldal: https://github.com/google/guava/wiki.
Fájl műveletek
A standard Java-ban a fájl műveletek nehézkesek. Noha az idők folyamán fejlődtek, azért érdemes megismerni a Guava fájlműveleteit, és reménykedni abban, hogy ezek hamarosan a standard könyvtárakba is bekerülnek. Néhány példa:
- Szövegfájl minden sorának beolvasása: List<String> lines = Files.readLines(new File(fileName), Charsets.UTF_8);
- Szöveg kiírása fájlba: Files.write("banana, orange, lemon, apple, plum".getBytes(), new File("fruits.txt")); (hozzáfűzés: append())
- Üres fájl létrehozása: Files.touch(new File(fileName));
Matematika
Az IntMath, LongMath és BigIntegerMath olyan gyakori problémákra ad megoldásokat, mint például:
- Ellenőrzött műveletek: alapból nincs túlcsordulás vizsgálat, tehát pl. a Integer.MAX_VALUE + Integer.MAX_VALUE eredménye -2, mely nehezen felderíthető hibákhoz vezet. Ellenben a IntMath.checkedAdd(Integer.MAX_VALUE, Integer.MAX_VALUE) kivételt dob.
- Műveletek óriási számokkal, pl. BigIntegerMath.factorial(100).
- Gyakori műveletek, pl. legnagyobb közös osztó: IntMath.gcd(15, 10).
Új gyűjtemények
A Guava számos új, igen hasznos gyűjtemény típust vezetett be, többek között:
- Multiset: olyan halmaz, melyben az elemek ismétlődhetnek.
- Multimap: olyan asszociatív tömb, melyben ugyanaz a kulcs több értéket is felvehet.
- BiMap: kétirányú asszociatív tömb.
- Table: olyan asszociatív tömb, melyben a kulcs egy táblázat egy cellája (tehát gyakorlatilag két érték adja a kulcsot).
- ClassToInstanceMap: olyan asszociatív tömb, melyben a kulcs valamilyen típus.
- Range: szakaszt definiál, pl. a Range<Integer> range = Range.closedOpen(3, 8); egy balról zárt, jobbról nyitott egészek szakaszát. Olyan műveleteket lehet segítségével végrehajtani, mint pl. azt, hogy egy elem benne van-e a szakaszban (range.contains(5)), vagy két szakasz metszetét (range.intersection(Range.open(5, 10))).
Lássunk egy BiMap példát! (A típusok a com.google.common.collect csomagban találhatóak.)
BiMap<Integer, String> biMap = HashBiMap.create(); biMap.put(1, "apple"); biMap.put(2, "peach"); biMap.put(3, "banana"); System.out.println(biMap.inverse().get("banana"));
Gráfok
A gráfok igen gyakori adatszerkezetek, és elég jól általánosíthatóak; igen meglepő, hogy a szabvány könyvtárak nem tartalmaznak gráfokat. A Guava ezt is pótolta. A következő 3 gráf típust használhatjuk:
- Graph: egyszerű gráf, ahol az éleket adhatjuk meg. Tehát pl. két település között vezet-e közvetlenül út.
- ValueGraph: olyan gráf, ahol az élek értéket tartalmazhatnak, pl. két település közötti legrövidebb út hossza.
- Network: többszörös éleket is tartalmazhat. Ennek a központi eleme a csúcs helyett az él.
Az osztályok a com.google.common.graph csomagban találhatóak. Lássunk egy példát!
MutableValueGraph<String, Integer> roads = ValueGraphBuilder.directed().build(); roads.putEdgeValue("Budapest", "Szeged", 170); roads.putEdgeValue("Budapest", "Debrecen", 230); roads.putEdgeValue("Budapest", "Nyíregyháza", 230); roads.putEdgeValue("Debrecen", "Nyíregyháza", 50); System.out.println(roads.edgeValue("Budapest", "Szeged").get());
Talán egy következő verzióban az alap gráf algoritmusok megvalósítására is sor kerül (szélességi és mélységi bejárás, vagy akár a Dijkstra algoritmus).
Cache
Gyakori megoldandó feladat az, hogy egy drága művelettel beolvasunk adatokat, majd lehet, hogy nem sokkal később ismét beolvassuk. Ez esetben érdemes elmenteni a memóriában, vagy egy gyorsan elérhető helyen. Angolul ezt hívjuk cache-nek (ejtds: kes). Az egyébként, hogy mi a cache, elég viszonylagos, mert pl. egy hálózatról letöltött adat lokális diszkre mentése is cache-nek számít, és a modern memóriáknak létezik az a "legbelső bugyra", ami akár százszor gyorsabb elérést eredményez, mint a normál memóriából olvasás. A legtöbb esetben viszont cache alatt a memóriát értjük, ami már elég nagy és már elég gyors ahhoz, hogy erre a célra általában használni tudjuk.
Tehát a feladat az, hogy ha drágán beolvasunk valamilyen adatot, akkor miután feldolgoztuk, de dobjuk el rögtön, hanem mentsük el a memóriában, hátha szükség lesz még rá. Persze két dologra azért figyelni kell: ne mentsünk el túl sok mindent, mert a "jó lesz az még valamire" hozzáállás erőforrás pazarlásoz vezet, ill. túl sokáig se üljünk rajta, mert egy idő után a friss adat értékesebb, még ha drágábban is jutunk hozzá. A cache megvalósításánál tehát elég sok dologra figyelnünk kell, melyek kellően általánosak ahhoz, hogy érdemes legyen egy projektfüggetlen megvalósítást biztosítani. Ezt a Java nyelv karbantartói nem tették meg, viszont megtette a Guava! Lássunk egy példát!
LoadingCache<Integer, String> fruitCache = CacheBuilder.newBuilder() .maximumSize(10) .expireAfterAccess(5, TimeUnit.SECONDS) .build(new CacheLoader<Integer, String>() { @Override public String load(Integer carId) throws Exception { Thread.sleep(500); switch (carId) { case 0: return "apple"; case 1: return "banana"; case 2: return "peach"; case 3: return "pear"; case 4: return "grape"; default: return null; } } }); try { for (int i = 0; i < 10; i++) { System.out.println(fruitCache.get(i % 4)); } } catch (ExecutionException e) { e.printStackTrace(); }
A példában egy drága műveletet szimulálunk, aminek a lekérdezése fél másodpercig tart. Láthatjuk, hogy a későbbi lekérdezések sokkal gyorsabbak. Érdemes eljátszani az időzítéssel, pl. úgy, hogy a lekérdezés után legalább 5 másodpercig várjon; ez esetben ismét belassul. Ill. ha a méretet túl alacsonyra vesszük, akkor is lassú lesz.
Megjegyzés: számomra sem volt világos ennek a pontos működése, emiatt leírom, hátha más is belebotlik ebbe a hibába. A cahce logikus felépítése a következő: lekérdezzük, benne van-e az adat a cache-ben, és ha benne van, akkor kiolvassuk onnan, ha nincs benne, akkor lekérdezzük és beletesszük. Itt kicsit más a megközelítés: egyből a cache-ből kell kiolvasni, és beletenni a load() függvény fogja. Oda kell tehát írnunk a külső hívást, és nem kell törődnünk a cache-eléssel. Kliens oldalon pedig csak a cache-t kell elérnünk. A cache-be tehát explicit nem teszünk bele soha semmit, az a keretrendszer feladata.