Niezmienne kolekcje w JDK9

Niezmienne kolekcje w JDK9

O jednej z nowych funkcjonalności Javy 9 i o tym, dlaczego się nią rozczarowałem.

środa, 31 maja 2017

Informatyka

O sile języka programowania świadczy nie tylko liczba użytkowników i jakość składni, ale i jego biblioteka standardowa. Jest to zbiór podstawowych funkcji, klas, interfejsów itd., który stanowi niejako "wspólny" słownik dla wszystkich aplikacji i programistów. Biblioteka może też - poprzez swoją budowę - wyznaczać pewną filozofię programowania, promując konkretne rozwiązania i odradzając inne. Java w latach 90. przetarła szlaki dla wielu późniejszych języków, pokazując, jak należy to robić, jednak przez ostatnie lata to ona głównie goni innych. Przyczyny są złożone, wystarczy wspomnieć choćby ponad 5-letnią przerwę wydawniczą. Ukazanie się JDK8 było małą rewolucją i załatało wiele palących braków w bibliotece standardowej Javy, jednak wciąż są miejsca, gdzie trzeba sobie radzić samemu. Jednym z nich jest brak wsparcia dla niezmiennych kolekcji.

Niezmienność

Jestem wielkim fanem niezmiennych obiektów, czyli takich, które raz utworzone, nie zmieniają już później swojego stanu. Do ich stworzenia nie trzeba wiele - w Javie wystarczy zadbać, aby w obiekcie:

  1. wszystkie pola miały atrybut final (co w efekcie prowadzi do tego, że musimy ich wartość określić podczas tworzenia obiektu),
  2. wszystkie referencje również prowadziły do obiektów niezmiennych.

Do tworzenia niezmiennych kolekcji używam obecnie znakomitej biblioteki Google Guava, która wśród wielu oferowanych funkcjonalności posiada też zestaw klas ImmutableSet, ImmutableList, ImmutableMap itd. Dzięki nim stworzenie kolekcji, której zawartości nie można już zmienić, jest bardzo intuicyjne:

List<String> mutableList = new ArrayList<>();
mutableList.add("foo");
mutableList.add("bar");
mutableList.add("joe");

List<String> firstList = ImmutableList.of("a", "b", "c");
List<String> secondList = ImmutableList.copyOf(mutableList);

System.out.println(secondList.get(0));

Działanie tych kolekcji jest bardzo proste - wszystkie operacje, które zmieniają zawartość (np. metoda add()), są zablokowane i powodują rzucenie wyjątku. Twórcy Guavy, wiedząc, że kolekcja nie może się już zmienić, wprowadzili szereg optymalizacji. Niezmienne kolekcje potrzebują mniej pamięci niż ich zmienne odpowiedniki, oferują też często lepsze czasy dostępu, jako że pod spodem siedzi sobie zwykła tablica. Inną optymalizacją jest metoda copyOf() - jeżeli jako argument przekażemy instancję niezmiennej kolekcji, jest ona zwracana od razu, bez żadnych zmian (jeśli wiemy, że dana informacja się nie będzie zmieniać, wystarczy że w pamięci będziemy trzymali tylko jedną instancję używaną wszędzie, gdzie to możliwe).

W moich aplikacjach niezmienne klasy mogą stanowić nawet do 60-70% wszystkich klas. Większość z nich "pod spodem" używa jakiejś niezmiennej kolekcji. Wszystko jest super, kiedy tworzę aplikację lub wysokopoziomowy framework, jednak problemy zaczynają się, kiedy chcę zbudować coś mniejszego. Guava jest bowiem zewnętrzną biblioteką i nie całe jej API jest stabilne. Gdy w aplikacji mamy kilka zależności bazujących na innej wersji Guavy, może się okazać, że nie będą chciały one razem działać, właśnie z powodu zmian w eksperymentalnych interfejsach. Programiści radzą sobie z tym w różny sposób. Przykładowo, Jersey i Guice używają okrojonych wariantów Guavy będących częścią ich źródeł, lecz przepakowanych do innych pakietów (np. jersey.repackaged.*). Pozwala to na uniknięcie problemów z kompatybilnością, ale z drugiej strony używając ich razem, w aplikacji mamy kilka wariantów niemal tych samych klas (zużycie pamięci), które w dodatku nie potrafią ze sobą współpracować. Przepakowane klasy są także widziane przez nasze IDE i - o ile sobie go odpowiednio nie skonfigurujemy - używane w podpowiadaniu. A ilu programistów pamięta o ich wykluczeniu? Sądząc po tym, ile razy już widziałem w kodzie aplikacji importy z jersey.repackaged.*, śmiem sądzić, że mało.

Ja stosuję inne podejście w projektach, gdzie wskazany jest brak zależności od zewnętrznych narzędzi - z bólem serca, ale po prostu rezygnuję z Guavy i wszystkiego, co oferuje.

JDK9

Bardzo ucieszyłem się, gdy na liście nowości w JDK9 zobaczyłem niezmienne kolekcje. Mając wciąż w pamięci szereg ulepszeń wprowadzonych przez JDK8, spodziewałem się rozwiązania silnie wzorowanego na Guavie, albo proponującego zupełnie nowe, świeże podejście do tematu. I tu przyszło rozczarowanie. Po zajrzeniu do dokumentacji okazało się, że całe wsparcie ogranicza się do dodania jednej metody of() w trzech interfejsach:

  • Set.of(...) - tworzy niezmienny zbiór z podanych w argumentach wartości lub tablicy wartości,
  • List.of(...) - tworzy niezmienną listę z podanych w argumentach wartości lub tablicy wartości,
  • Map.of(...) / Map.ofEntries(...) - tworzy niezmienną mapę z podanych w argumentach par klucz-wartość lub tablicy takich par.

Ich zastosowanie wygląda następująco:

List<String> firstList = List.of("a", "b", "c");
System.out.println(firstList.get(0));

Już na tym przykładzie widać podstawową wadę zaproponowanej implementacji. Nie ma tutaj bowiem żadnego odpowiednika metody .copyOf() pozwalającego zrobić kopię już istniejącej kolekcji. Co więcej, nawet nie da się czegoś takiego zaimplementować, ponieważ twórcy ukryli przed nami dokładny typ zwracanej instancji, co uniemożliwia stworzenie warunku w stylu x instanceof .... W moich projektach jednym z podstawowych rozwiązań jest następujący konstruktor:

public class Foo {
    private final List<Bar> myList;

    public Foo(List<Bar> instances) {
        this.myList = ImmutableList.copyOf(instances);
    }
}

Dzięki niemu, mam pewność, że w obiekcie znajdzie się niezmienna kolekcja. Jeżeli programista w argumencie poda mi np. instancję LinkedList, zostanie zrobiona kopia, zaś jeżeli trafi tam instancja ImmutableList, zostanie użyta bezpośrednio. W JDK9 nie będzie to możliwe - nie tylko będziemy zmuszeni do zrobienia kopii, ale też będziemy musieli ręcznie wypakować wszystko do tablicy. Nie da się także zrobić łatwo migawki już istniejącej, ale zmiennej kolekcji:

public List<String> getBarSnapshot() {
    return ImmutableList.copyOf(bars);
}

Nie wspomnę już o świetnym rozwiązaniu z Guavy, jakim jest ImmutableXXX.builder(), czyli API do budowania większych kolekcji, które potem zamrażamy. W zasadzie jedyne zastosowanie dla niezmiennych kolekcji JDK9, jakie znalazłem, ogranicza się do sytuacji, gdzie wywołujemy jakąś metodę i chcemy do niej łatwo i szybko wrzucić np. kolekcję trzech określonych elementów. A ile razy nam się coś takiego trafia?

Podsumowanie

Gwoli wyjaśnienia - czekam z niecierpliwością na JDK9 i uważam nadchodzące wydanie za kolejny ważny krok w rozwoju języka. Niezmienne kolekcje to coś, na co długo czekałem, gdyż bardzo często z nich korzystam i jest to chyba obecnie dla mnie chyba największy brakujący klocek w moim arsenale podstawowych narzędzi JDK. Guava jest świetna, ale jej przydatność ogranicza się głównie do kodu, nad którym mam kontrolę. Co więcej, gdy tworzę bibliotekę dla innych projektów, dla ich dobra nie powinienem z Guavy korzystać. Na zmiany w JDK9 jest już za późno, ale osobiście liczę na to, że poruszenie jednego kamyka wywoła całą lawinę, dzięki której porządna implementacja pojawi się np. w JDK10. Szkoda tylko, że oznaczałoby to dla nas kolejnych kilka lat czekania...

-- Tomasz Jędrzejewski

Autor zdjęcia nagłówkowego: Ben Stassen, CC-BY-2.0

Komentarze (0)

Skomentuj

Od 3 do 40 znaków.

Wymagany, nie będzie publikowany.

Odpowiedz na pytanie.

Edycja Podgląd

Od 10 do 8000 znaków.

Wszystkie komentarze są moderowane i muszą być zatwierdzone przed publikacją.