Java 9 – Unmodifiable Collections

Java się bardzo rozwija. Początkowe podejście, według którego nowa wersja Javy wychodziła co kilka lat, odeszło do lamusa. Obecnie w każdym roku mamy dwa wydania – w marcu oraz we wrześniu.

Nie znaczy to, że co pół roku musimy updatować nasze wszystkie biblioteki. Wraz z częstymi updatami w Javie wprowadzono pojęcia LTS (Long Term Support) oraz non-LTS. I tak, co pół roku dostajemy nową wersję Javy, jednakże co trzy lata mamy “duży” update, który ma długoterminowe wsparcie. Na ten moment najnowszy LTS to Java 17, która zostanie zmieniona we wrześniu tego roku. Oprócz tego, do 2030 roku wsparcie będzie utrzymywane dla Javy 8.

W Javie 9 mamy wprowadzonych kilka nowości, a jedną z nich, które zostaną podjęte w tym wpisie są metody fabryczne dla niemodyfikowalnych kolekcji. 

W Javie 8, aby utworzyć niemodyfikowalną listę, należało najpierw stworzyć listę, a następnie wykorzystać tę listę do stworzenia UnmodifiableList.

List<String> list = new ArrayList<>();
list.add("One");
list.add("Two");
list.add("Three");
List<String> unmod = Collections.unmodifiableList(list);

Drugim sposobem jest oczywiście skorzystanie z klasy Arrays, skróci to nam wywołanie do jednej linijki:

List<String> unmod = Collections.unmodifiableList(Arrays.asList("One", "Two", "Three"));

W Javie 9 wprowadzono natomiast statyczne metody fabrykujące of, które tworzą taką liste:

List<String> unmod = List.of("One", "Two", "Three");

Tworzy to (teoretycznie – o tym później) taką samą niemodyfikowalną listę jak dwa powyższe sposoby, jednakże w trochę czystszy sposób.

Próba zmiany stanu kolekcji – dodanie elementu, usunięcie go czy zamiana z wykorzystaniem metody set skutkuje UnsupportedOperationException.

Metoda ta (.of) została dodana także dla zbiorów i map. W przypadku Setów tworzenie wygląda tak samo jak w przypadku list, jednakże korzystamy oczywiście z klasy Set, a nie List. Mapy natomiast, kiedy są tworzone z użyciem metody fabrycznej, tworzone są według wzoru: .of(klucz1, wartość1, klucz2, wartość2, …). Konstrukcja metod w klasie bazowej też jest inna dla map oraz wcześniejszych typów.

W przypadku zbiorów i list, mamy dziesięć podstawowych metod, odpowiedzialnych za tworzenie obiektów o pojemności do dziesięciu elementów. W przypadku większej liczby – po prostu przyjmowana jest tablica argumentów. Jako przykład weźmy fragment javadoca dla klasy List:

// Returns an unmodifiable list containing zero elements.
static <E> List<E>	of()	
// Returns an unmodifiable list containing one element.
static <E> List<E>	of​(E e1)	
// Returns an unmodifiable list containing two elements.
static <E> List<E>	of​(E e1, E e2)	
// Returns an unmodifiable list containing three elements.
static <E> List<E>	of​(E e1, E e2, E e3)	
// Returns an unmodifiable list containing four elements.
static <E> List<E>	of​(E e1, E e2, E e3, E e4)	
// Returns an unmodifiable list containing five elements.
static <E> List<E>	of​(E e1, E e2, E e3, E e4, E e5)	
// Returns an unmodifiable list containing six elements.
static <E> List<E>	of​(E e1, E e2, E e3, E e4, E e5, E e6)	
// Returns an unmodifiable list containing seven elements.
static <E> List<E>	of​(E e1, E e2, E e3, E e4, E e5, E e6, E e7)	
// Returns an unmodifiable list containing eight elements.
static <E> List<E>	of​(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8)	
// Returns an unmodifiable list containing nine elements.
static <E> List<E>	of​(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9)	
// Returns an unmodifiable list containing ten elements.
static <E> List<E>	of​(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10)	
// Returns an unmodifiable list containing an arbitrary number of elements.
static <E> List<E>	of​(E... elements)	

Zmiana podejścia jest jednak w przypadku map. O ile do dziesięciu elementów obiekt tworzymy w ten sam sposób, o tym powyżej tej ilości – należy skorzystać z metody entry(key, value):

Map<Integer, String> unmod = Map.of(1, "First", 2, "Second", 3, "Third");
Map<Integer, String> bigUnmod = Map.ofEntries(
        entry(1, "First");
        entry(2, "Second");
        entry(3, "Third");
    );

Różnice między starym a nowym podejściem

W Javie tworząc zbiór czy listę można bezproblemowo umieścić w niej wartość nullową. Nic nie stoi zatem na przeszkodzie, aby stworzyć taką listę:

List<String> unmod = Collections.unmodifiableList(Arrays.asList("One", "Two", "Three", null));

Próba wydrukowania takiej kolekcji wyświetli nam na konsoli elementy One, Two, Three oraz null. Wykorzystanie metody fabrycznej uniemożliwia nam dodanie nulla do listy lub seta, natomiast w przypadku mapy null nie może być ani kluczem ani wartością. Próba dodania elementu nullowego skutkuje wyrzuceniem NullPointerException. Próba naruszenia właściwości mapy lub seta poprzez dodanie duplikatu do tych kolekcji skutkuje natomiast wyjątkiem IllegalArgumentException z komunikatem Duplicate element.

Główną zaletą stosowania unmodified lists jest ich rzekome bezpieczeństwo wątkowe. Niemodyfikowalne kolekcje (teoretycznie) nie powinny być narażone na to, że któryś z wątków zmieni ich stan. I choć kolekcja jako taka się nie zmieni, to elementy tej kolekcji… no tu już jest gorzej.

W starym podejściu, niezależnie od tego, czy korzystamy z ręcznego tworzenia i dodawania elementów do listy, czy też tworzymy ją z wykorzystaniem Arrays.asList – najpierw tworzymy zwykłą, modyfikowalną listę. Arrays.asList jest w tym przypadku trochę lepsze, jednak na potrzeby tego przykładu zróbmy to z wykorzystaniem zwykłej implementacji:

List<String> list = new ArrayList<>(); 
list.add("One"); 
list.add("Two");
list.add("Three"); 
List<String> unmod = Collections.unmodifiableList(list); 

unmod.forEach(System.out::print);

Sprawdźmy co zostanie wydrukowane, gdy unmodunmod2 prześlemy do zwykłej funkcji printującej:

One
Two
Three

Zrozumiałe.
Zróbmy zatem eksperyment. ZMODYFIKUJMY kolekcję, z której utworzone zostały unmody:

List<String> list = new ArrayList<>(); 
list.add("One"); 
list.add("Two"); 
list.add("Three"); 
List<String> unmod = Collections.unmodifiableList(list2); 

list.add("MODIFICATION");  

unmod.forEach(System.out::print); 

W tym wypadku po uruchomieniu zobaczymy… 

One
Two
Three
MODIFICATION

, co oznacza, że  zmodyfikowaliśmy… niemodyfikowalną listę. Lista utworzona przez Arrays.asList jest odporna na dodawanie elementów do kolekcji, jednak nadal można ją modyfikować z użyciem metody set, która podmienia element na danym indeksie.

Dzieje się tak dlatego, że kolekcja stworzona przez Collections.unmodifiableList nie jest kolekcją jako-taką, a jest rodzajem niemodyfikowalnego widoku, lub referencji do w pełni modyfikowalnej kolekcji, z której widok ów został utworzony. Wykorzystanie metody fabrycznej chroni nas przed takim niebezpieczeństwem:

List<String> unmod = List.of("One", "Two", "Three");
unmod.add("MODIFICATION");
unmod.set(1, "MODIFICATION");
unmod.remove(2);

Można komentować linie modyfikujące w zależności od potrzeb, ale mimo wszystko – każda próba wykonania tej operacji kończy się na rzuceniu wyjątku.

Bezpieczeństwo wielowątkowe

Widzimy już, że tworzenie list niemodyfikowalnych przez wykorzystanie metody fabrycznej of pozwala nam zachować bezpieczeństwo stanu kolekcji w środowisku wielowątkowym. Natomiast niebezpieczeństwa zauważone we fragmencie z modyfikowaniem kolekcji schodzą niżej i jeśli obiekty w Javie nie będą immutable – umożliwi to zmianę stanu poszczególnych elementów kolekcji, a co za tym idzie – de facto zmianę tej kolekcji. Aby być w 100% thread safe – do niemodyfikowalnej kolekcji należy włożyć niemodyfikowalne obiekty – takie działanie dopiero da nam 100% pewności, że stan kolekcji będzie zawsze taki sam.

Na koniec

 Nadchodzi kilka postów z różnic, jakie przyniosły kolejne wersje Javy w stosunku do wciąż najpopularniejszej wersji 8. W przypadku Javy 9 to, oprócz metod fabrycznych w kolekcjach, zmiany w Garbage Collectorze, jlink, jshell, kolejna zmiana w interfejsach czy wreszcie wprowadzenie modułów. W ciągu najbliższych kilku tygodni regularnie będą się zatem pojawiały wpisy w tym temacie.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Strona używa danych zapisanych na komputerze odwiedzającego. Kliknij przycisk AKCEPTUJ COOKIES, aby nie widzieć więcej tego komunikatu Polityka prywatności

Gniado IT wykorzystuje cookies do przechowywania informacji na TWOIM komputerze. Cookies wykorzystywane przez tę witrynę to standardowe cookies wykorzystywane przez silnik Wordpress. Możesz ustawić swoją przeglądarkę w ten sposób, aby blokowała wszelkie próby użycia cookies, jednakże może to skutkować nieprawidłowym działaniem witryny. Dane nie są wykorzystywane do profilowania reklamowego, czy pozyskania wrażliwych / poufnych informacji.

Zamknij