Google Guava i kolekcje

Guava to zbiór bibliotek dla Javy od Google, wspierających pracę z kolekcjami, cache’owanie, obsługę prymitywów, współbieżność, operacje wej./wyj. i wiele innych aspektów.

W tej notce chciałbym pokazać w jaki sposób możemy poprawić efektywność pracy z kolekcjami oraz uniknąć kilku pułapek.
Z założenia ma to być tylko wprowadzenie, mające wywołać u Ciebie tzw. ‘moment aha!‘.

1. Wygodniejsze tworzenie kolekcji

Podstawowymi klasami od których chyba każdy zaczyna swoją przygodę z częścią biblioteki odpowiedzialną za kolekcje (pakiet com.google.common.collect) są: Lists, Sets oraz Maps.

Każda z nich posiada szereg statycznych metod, których nazwy zaczynają się od prefiksu ‘new’ i zwracających instancję konkretnej implementacji kolekcji. Metody te są parametryzowane, dzięki czemu możemy uniknąć zbędnego dublowania informacji o typie. W praktyce pozwala nam to uniknąć spaghetti postaci:

Map<SomeImmutableType, SomeType> someMap = new HashMap<SomeImmutableType, SomeType>();

na rzecz:

Map<SomeImmutableType, SomeType> someMap = Maps.newHashMap();

Metody te mogą także przyjmować opcjonalne argumenty takie jak referencję do kolekcji (lub iteratora), której elementy ma zawierać nowo tworzona kolekcja, czy listę elementów, oddzieloną przecinkami (vararg):

List<Long> longsList = Lists.newArrayList(1L, 3L, 5L, 11L);
List<Long> longsListCopy = Lists.newArrayList(longsList);
List<Long> longsListCopyIter = Lists.newArrayList(longsList.iterator());

2. Działania na zbiorach

Wyznaczanie sumy, różnicy, czy iloczynu zbiorów jest banalnie proste i sprowadza się do użycia jednej metody:

Set<Long> firstSet = Sets.newHashSet(1L, 2L, 3L, 4L);
Set<Long> secondSet = Sets.newHashSet(3L, 4L, 5L, 6L);

// firstSet ^ secondSet
Sets.intersection(firstSet, secondSet);
// = [3, 4]

// firstSet v secondSet
Sets.union(firstSet, secondSet);
// = [1, 2, 3, 4, 5, 6]

// firstSet \ secondSet
Sets.difference(firstSet, secondSet);
// = [1, 2]

// secondSet \ firstSet
Sets.difference(secondSet, firstSet);
// = [5, 6]

// firstSet - secondSet
Sets.symmetricDifference(firstSet, secondSet);
// = [1, 2, 5, 6]

Jak zapewne już się domyślasz, wyznaczanie iloczynu kartezjańskiego również możemy zrealizować wywołując tylko jedną metodę biblioteki:

// firstSet X secondSet
Sets.cartesianProduct(firstSet, secondSet);
// = [[1, 3], [2, 3], [3, 3], [4, 3], [1, 4], [2, 4], [3, 4], ..]

Spróbujmy teraz zmierzyć się z następującym wyzwaniem:
Mamy 2 zbiory imion, niech będą to imiona znajomych. Chcemy znaleźć takie imiona, które występują w obu zbiorach oraz zaczynają się od ciągu “Ma”.
Definicja zbiorów wygląda następująco:

Set<String> firstNamesSet = Sets.newHashSet("Tomasz", "Mateusz", "Maria", "Krzysztof", "Mariola", "Alina", "Marcin", "Bartosz", "Artur");
Set<String> secondNamesSet = Sets.newHashSet("Bartosz", "Łukasz", "Marian", "Marcin", "Aleksandra", "Paulina", "Tomasz", "Maria");

Pierwsza część zadania to pestka – wiemy już jak efektywnie wyznaczyć część wspólną zbiorów:

Set<String> commonNamesSet = Sets.intersection(firstNamesSet, secondNamesSet);
// = [Bartosz, Tomasz, Marcin, Maria]

Teraz potrzebujemy odrzucić wszystkie elementy kolekcji, które nie spełniają naszego predykatu (‘zaczyna się od “Ma”‘).
Każdy kto próbował jednocześnie iterować po kolekcji z użyciem pętli foreach i usuwać z niej elementy wie czym to śmierdzi: ConcurrentModificationException.
Co więc możemy zrobić? Hmm.. może użyjemy iteratora:

Set<String> commonNamesSet = Sets.intersection(firstNamesSet, secondNamesSet);
Iterator<String> commonNamesIterator = commonNamesSet.iterator();
while (commonNamesIterator.hasNext()) {
    String name = commonNamesIterator.next();
    if (!name.startsWith("Ma")) {
        commonNamesIterator.remove();
    }
}

No to teraz gra gitara?
Niestety tę gitarę trzeba będzie jeszcze nastroić:

Exception in thread "main" java.lang.UnsupportedOperationException
	at com.google.common.collect.UnmodifiableIterator.remove(UnmodifiableIterator.java:37
        ...

Mamy tutaj do czynienia z niemodyfikowalną kolekcją. A tak naprawdę to niemodyfikowalnym widokiem zbioru (com.google.common.collect.Sets.SetView).
Zaletą stosowania widoku jest niewątpliwie oszczędność zasobów – zamiast tworzyć nową instancję zbioru zawierającego wspólne elementy dostajemy maskę, czy perspektywę przez którą uzyskujemy dostęp do elementów z obu zbiorów.
Wszelkie zmiany dokonywane w oryginalnych zbiorach są natychmiast uwzględniane w ich widokach (co w kontekście powyższej definicji jest logiczne):

Set<String> commonNamesSet = Sets.intersection(firstNamesSet, secondNamesSet);
commonNamesSet.contains("Marcin"); // = true
firstNamesSet.remove("Marcin");
commonNamesSet.contains("Marcin"); // = false

Zastosujmy się teraz do wskazówki zawartej w dokumentacji Sets.SetView i użyjmy metody copyInto(set) (zwróć uwagę, że zmieniłem typ commonNamesSetView na SetView):

SetView<String> commonNamesSetView = Sets.intersection(firstNamesSet, secondNamesSet);
Set<String> commonNamesModifableSet = commonNamesSetView.copyInto(Sets.<String> newHashSet());
Iterator<String> commonNamesIterator = commonNamesModifableSet.iterator();
while (commonNamesIterator.hasNext()) {
    String name = commonNamesIterator.next();
    if (!name.startsWith("Ma")) {
        commonNamesIterator.remove();
    }
}
// commonNamesModifableSet = [Maria, Marcin]

Kod działa, założenia zostały spełnione.
Ale co jeśli będziemy mieli naprawdę duże zbiory z liczną częścią wspólną?

3. Filtrowanie kolekcji

W klasach Collections2 oraz Sets znajdziemy metodę filter, zwracającą (podobnie jak poprzednie) widok kolekcji. W przypadku Maps mamy do czynienia z 3 metodami: filterKeys, filterValues, filterEntries, filtrującymi odpowiednio” klucze, wartości oraz całe pozycje (pary klucz-wartość). Posługujemy się nimi tak samo jak metodami filter, dlatego nie będę ich opisywał.

Wracając do naszego zadania z pkt. 2 oraz pytania “Co jeśli..”, możemy wykorzystać filtry w celu uniknięcia tworzenia nowej instancji kolekcji:

Set<String> commonNamesSet = Sets.intersection(firstNamesSet, secondNamesSet);
Sets.filter(commonNamesSet, new Predicate<String>() {

    @Override
    public boolean apply(final String name) {
        return name.startsWith("Ma");
    }

});
// = [Maria, Marcin]

Pierwszym argumentem metody filter jest kolekcja, którą chcemy wyfiltrować.
Drugim argumentem jest obiekt implementujący interfejs com.google.commin.base.Predicate.
Ja użyłem klasy anonimowej (dla uproszczenia; w normalnym zastosowaniu trzymałbym jej instancję w statycznym, finalnym polu klasy), ale Guava dostarcza potrzebny mi mechanizm out-of-box w postaci Predicates.containsPattern(String pattern). Z jej użyciem kod będzie jeszcze zwięźlejszy:

Set<String> commonNamesSet = Sets.intersection(firstNamesSet, secondNamesSet);
Sets.filter(commonNamesSet, Predicates.containsPattern("^Ma"));
// = [Maria, Marcin]

Co zyskaliśmy?
Po pierwsze wciąż operujemy na widokach, dzięki czemu unikamy tworzenia pośrednich kolekcji, niepotrzebnych poza danym krokiem przekształcania.
Po drugie nie grozi nam ConcurrentModificationException, nie grozi nam też literówka w nazwie zmiennej przechowującej iterator skutkująca odczytem/modyfikacją zupełnie innej kolekcji niż zamierzaliśmy.

4. Transformacje kolekcji

Kolejną często powtarzającą się operacją wykonywaną na kolekcjach są przekształcenia ich elementów.

Przykład: Dla pewnej listy liczb chcemy otrzymać listę zawierającą ich wartości bezwzględne.
Definicja listy jest następująca:

List<Integer> numbers = Lists.newArrayList(-1, 4, 5, -2, -3, 8, 45);

Możemy spróbować podejścia podobnego jak w pkt. 2, (czyli stworzyć nową, pustą kolekcję wynikową, iterować po numbers przekształcając wartości (wyznaczać wartość bezwzględną) i dodając je do kolekcji wynikowej) jednak jak już się domyślasz po tytule punktu za chwilę pokażę jak to zrobić efektywniej za pomocą Guavy.

Klasy Collections2 oraz Lists dostarczają nam statyczne metody transform(collection, transformFunction).
Pierwsy argument to kolekcja, której elementy chcemy przekształcić.
Drugi argument to funkcja (obiekt implementujący interfejs com.google.common.base.Function, gdzie F to typ z którego chcemy dokonywać transformacji (From), a T to typ docelowy (To)).
Typem wyniku wcale nie musi być różny od typu elementów na wejściu, czego przykładem jest nasz przypadek z wartościami bezwzględnymi:

List<Integer> numbers = Lists.newArrayList(-1, 4, 5, -2, -3, 8, 45);
Lists.transform(numbers, new Function<Integer, Integer>() {

    @Override
    public Integer apply(final Integer from) {
        return Math.abs(from);
    }
});
// = [1, 4, 5, 2, 3, 8, 45]

5. Porównywanie elementów kolekcji

Kolejną interesującą klasą jest com.google.common.collect.Ordering, serwująca nam metody realizujące najczęstsze operacje wykonywane z użyciem komparatorów (“porównywaczy”?).

Przykład: Chcemy wyznaczyć kres górny dla następującego zbioru liczb: -1, 4, 5, -2, -3, 8, 45 (jak w pkt. 4).
Ponieważ będziemy operaować na liczbach, które posiadają swój naturalny porządek możemy skorzystać ze statycznej metody-fabryki Ordering.natural() zamiast dziedziczyć po Ordering i nadpisywać metodę compare. Dysponując tak uzyskaną instancją użyjemy metody max(Iterable) w celu wyznaczenia największej wartości zbiotu:

Set<Integer> numbers = Sets.newHashSet(-1, 4, 5, -2, -3, 8, 45);
Ordering.natural().max(numbers);
// = 45

Podsumowanie

Przedstawiłem tutaj ledwie czubek góry lodowej (a raczej gigantycznego rogu obfitości ;)) jaką jest Google Guava. Moim zdaniem zbyt często skupiamy się na kolejnych frameworkach traktując wszelakie commonsy po macoszemu – a to błąd.
Zachęcam do lektury wiki na stronie projektu http://code.google.com/p/guava-libraries/wiki/GuavaExplained – jest przejrzyście napisane i zawiera masę przykładów.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s