Google Guava

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.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License