Kategorie
java

Guava – multibiblioteka od Google

Guava to jedna z tych bibliotek, która w pewnym momencie historii Javy, wyprzedziła ówczesne rozwiązania o krok pod względem wizji przyszłego stylu kodowania. Ale zacznijmy od początku i rzućmy okiem na świat przed Java 8.

Lecimy!

Junior Java Developer Handbook

A kiedyś było tak …

Dawno, dawno temu, za górami, za lasami … była sobie Java w wersji 5. Piąta edycja Javy była znacząca. Wprowadziła ważne dla nas rozwiązanie do kolekcji, czyli typy generyczne (ang. generics).

Rozwiązanie samo w sobie świetne dla programistów, choć, co trzeba przyznać, mające i swoje gorsze strony wynikające ze wstecznej kompatybilności języka. Ówczesna Java zrobiła spory krok w stronę lepszej jakości kodu.

Jakby jednak nie patrzeć API kolekcji i klasa pomocniczych w Javie 5 nie było duże. Można powiedzieć, że były to wręcz minimalistyczne rozwiązania, patrząc z dzisiejszej perspektywy.

Podstawowym problem ówczesnego programisty był wszystkiego rodzaju przekształcenia kolekcji, filtrowanie, wyszukiwanie, etc. Czyli wszystkie operacje, które w dużych ilościach występują w naszym oprogramowaniu.

I tutaj właśnie na scenę weszła Guava. Wypełniła ona olbrzymią dziurę w API Javy w tym obszarze.

Multimap na początek

Nie wiem, jak u Ciebie, ale mi bardzo często w projektach trafiały się fragmenty kodu, gdzie musiałem skorzystać z mapy z wieloma wartościami. Czasem dotyczyło to np. kolekcji uprawnień dla wybranego użytkownika, czasem chodziło o zebranie pseudo raportowych danych. Problemem zawsze stanowiło to, że w Javie, nawet teraz, nie ma ładnego sposobu, żeby taką mapę przygotować.

  private String LOGIN_1 = "user-1";
  private String LOGIN_2 = "user-2";  

  @Test
  public void one() {
    Multimap<String, String> multimap = MultimapBuilder
        .hashKeys()
        .arrayListValues()
        .build();

    multimap.put(LOGIN_1, "ROLE_1");
    multimap.put(LOGIN_1, "ROLE_2");
    multimap.put(LOGIN_1, "ROLE_3");

    multimap.put(LOGIN_2, "ROLE_8");
    multimap.put(LOGIN_2, "ROLE_9");

    assertEquals(2, multimap.keySet().size());
    assertEquals(3, multimap.get(LOGIN_1).size());
    assertEquals(2, multimap.get(LOGIN_2).size());
  }

Jak widać powyżej, stworzyliśmy sobie prostą multimapę. Builder, w liniach 6 – 9, pozwolił na określenie, w jaki sposób chcemy przechowywać klucze oraz jak mają być zachowywane wartości (lista). Następnie uzupełniliśmy dane (linie 11 – 16). Na końcu widzimy, że uzyskaliśmy oczekiwany efekt. Wstawiliśmy dwa klucze, odpowiednio z trzema i dwiema wartościami. Co przetestowaliśmy w liniach 18-20.

Teraz pragmatyczne pytanie do Ciebie? Ile kodu musiałbyś napisać, żeby uzyskać podobne zachowanie, nawet przy obecnych konstrukcjach języka?

Warto tu dodać, że powyższy test można jeszcze skrócić, bo nie musisz korzystać z Buildera, który daje różne opcje na przechowywanie kluczy i wartości. Możesz np. w jednej linijce uzyskać multimapę z podobnym zachowaniem:

Multimap<String, String> multimap = 
    ArrayListMultimap.create();

Wygląda nieźle. Prawda?

Inne kolekcje?

Zdecydowanie. Poza Multimap mamy też np Multiset, czyli multizbiór, gdzie możemy mieć wiele wystąpień elementu:

  @Test
  public void multisetTest() {
    Multiset<String> multiset = HashMultiset.create();

    multiset.add("one");
    multiset.add("one");
    multiset.add("four");
    multiset.add("five");
    multiset.add("one");

    assertEquals(5, multiset.size());
    assertEquals(3, multiset.count("one"));
    assertEquals(1, multiset.count("four"));
    assertEquals(1, multiset.count("five"));
  }

I w tym przypadku sprawdzamy właśnie liczbę wystąpień danego elementu.

Bimap, czyli różne rodzaje map z wiązaniem dwukierunkowym (ang. bidirectional map). Kiedy się przydają? Kiedy chcesz mieć mapę klucz – wartość, ale jednocześnie interesuje Cię relacja w drugą stronę.

  @Test
  public void bimapTest() {
    Integer value = Integer.valueOf(1);

    BiMap<String, Integer> bimap = HashBiMap.create();
    bimap.put("one", value);

    assertEquals(1, bimap.size());
    assertEquals(value, bimap.get("one"));
    assertEquals("one", bimap.inverse().get(value));
  }

I tak, w powyższym przykładzie wstawiliśmy klucz one i zmienną value do mapy. A na samym końcu w linii odwróciliśmy tę mapę i na podstawie wartości value znaleźliśmy klucz one.

Jasne, nie jest to zbyt wyszukane zastosowanie, ale, gdy np. pomyślisz o zastosowaniu w pamięciowej bazie danych, to już może siła tej kolekcji będzie bardziej wyraźna.

A jeśli chodzi o bazy danych, to zaciekawi Cię typ Table z Guavy.

  @Test
  public void tableTest() {
    Integer value = Integer.valueOf(100);
    Table<Integer, Integer, Integer> table = HashBasedTable.create();
    table.put(0, 0, value);
    assertEquals(value, table.get(0, 0));
  }

Dzięki niemu uzyskasz strukturę danych podobną do tabeli w arkuszu kalkulacyjnym. Być może Ci się kiedyś przyda.

Prawie każdy trafił kiedyś na problem mapowaniu klasy do np. napisu. W tym przypadku rzuć okiem na ClassToInstanceMap. Szybko i sprawnie uzyskasz takie zachowanie.

Ciekawy jest też RangeSet. Czyli taki zbiór z otwartymi zakresami. Jest też RangeMap, czyli mapa zakresów do wartości. O Range za chwilę.

Guava ma też wsparcie dla kolekcji

Guava zawiera dużo klas pomocniczych dla kolekcji. W większości dostępne są one poprzez dodanie literki „s” do typu kolekcji. I tak, mamy Lists dla List, Sets dla Set, itp.

Tutaj mała dygresja. Jeśli masz do czynienia z kodem przed Javą 8 to warto tutaj szukać pomysłów na rozwiązanie problemów.

Wprowadzenie stream’ów w Javie 8 sprawiło jednak, że wiele operacji można zrealizować w ramach standardowego JDK.

Warto jednak cały czas zwrócić uwagę na metody Sets.union(), Sets.intersection(), Sets.difference(). Nie raz przydawały mi się w moich projektach.

Tutaj link do całego opisu poszczególnych klas pomocniczych.

Klasa Range

Jest taka klasa w Guavie, którą niezmiernie lubię. Kilka lat temu miałem okazję brać udział w projekcie, który bardzo mocno bazował na regułach biznesowych. Reguły te określały zakresy dat do wyszukiwania w usłudze zewnętrzne i musiały uwzględniać różne przypadki łączenia zakresów, rozdzielania, etc.

Jeden z developerów, piszący kawałek kodu odpowiedzialny za przygotowanie tych zakresów, męczył się przez kilka godzin z implementacją. Niestety nie znał jeszcze Guavy, a jego kod był dosyć dziurawy, co szybko wyszło podczas testów.

Wykorzystanie Range z Guavy bardzo szybko rozwiązało problem. Kod stał się czytelniejszy, działania na zakresach prostsze, jednocześnie język w tej domenie problemowej dużo lepiej odpowiadał temu, czym posługiwał się ekspert, z którym pracowaliśmy.

Poniżej przykład sprawdzanie, czy dane przedziały na siebie zachodzą, czy też nie:

  @Test
  public void rangeTest() {
    assertTrue(
        Range.closed(8, 10)
            .isConnected(
                Range.open(9, 25)));
    assertFalse(
        Range.closed(0, 9)
            .isConnected(
                Range.closed(10, 20)));
  }

Prawda, ze bardzo jasne API? Nie trzeba iterować, sprawdzać punktów końcowych przedziału. Właśnie takie powinno być API. Definiować spójny język pojęć, który będzie zrozumiały dla piszącego kod, ale i później dla jego czytelnika.

A przy okazji w Guavie Range też zawiera ważne metody, czyli intersection() oraz span(). Pierwsza szuka maksymalnego wspólnego zakresu, a druga minimalnego zawierającego oba podane zakresy. Bardzo pomocne metody.

Na co jeszcze można rzucić okiem

Przed Caffeine parę razy w projektach produkcyjnych korzystałem właśnie z implementacji Guavy. Protoplasta Caffeine w tym przypadku zawsze sprawował się znakomicie. Jeśli Guava jest w Twoim starszym projekcie, to warto spróbować.

Jeśli interesuje Cię zagadnienie cache, to rzuć okiem na artykuł Caffeine cache, super szybka pamięć podręczna. Znajdziesz tam więcej informacji i przypadków użycia.

Dla osób zajmujących się grafami ciekawy będzie rozdział Graphs, Explained z dokumentacji. Zawiera on dużo szczegółów na temat budowania grafów, wyszukiwania w nich elementów i cech. Guava ma bardzo fajne API w tym zakresie.

Warto też zwrócić uwagę na klasy pomagające przetwarzać łańcuchy znaków. Joiner, Splitter i CharMatcher zawierają bardzo bogate API, które pomoże każdemu, kto będzie przetwarzał dane oparte o klasę String.

Guava – podsumowanie

Widać, że Guava to biblioteka o wielu twarzach. Potrafi pomóc w rozwiązywaniu przeróżnych problemów. Klasyk. Swego czasu jedna z najpopularniejszych bibliotek dodawanych w pierwszej kolejności do projektów. Wraz z rozwojem JDK rzadziej znajdziemy ją w kodzie projektowym. Ale mam nadzieję, że kilka powyższych przykładów pokazało, że warto cały czas się nią interesować.

Zachęcam Cię do dalszego przeglądu możliwości Guavy. Na stronie głównej projektu możesz znaleźć bardzo bogaty opis pozostałych funkcji. Świetne zestawienie materiałów przygotował również Thomas Ferris Nicolaisen.

A jakie jest Twoje doświadczenie z wykorzystaniem Google Guava ❓

5 1 vote
Article Rating
Subscribe
Powiadom o
guest
2 komentarzy
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Szymon Przedwojski
2 miesięcy temu

Dzięki Bartek, ciekawy artykuł. Ja wszedłem do świata Javy trochę później i nie wykorzystywałem Guavy aż tak mocno, bo dość szybko na horyzoncie pojawiła się już Java 8. Ale tak jak piszesz – jest to klasyk, więc warto wiedzieć, do czego służyła.

Sądzisz, że wciąż jest dla niej miejsce w nowoczesnym Javowym projekcie? A może jest jakaś nowsza biblioteka uzupełniająca braki JDK 1.8 w tym zakresie?