Java HR: obiekty niezmienne (immutable)
O obiekcie w Javie możemy powiedzieć że jest niezmienny, gdy – jak sama nazwa wskazuje – nie możemy zmienić jego wewnętrznego stanu po deklaracji. Ściślej mówiąc, po wykonaniu jego konstruktora i tylko wewnątrz niego możemy możemy zdefiniować jego wewnętrzny stan.
Później, wartość żadnego z pól nie może ulec zmianie, czy to poprzez zmianę którejś wartości wprost, czy też poprzez wywoływanie operacji mających na celu zmianę tego wewnętrznego stanu.
Cenna wskazówka:
Można sprecyzować tę zasadę dodając, że nie zmienia się obserwowalny stan obiektu – String przykładowo, dzięki swojej leniwej ewaluacji wartości zwracanej przez hashCode() (ostateczna wartość zostaje przypisana dopiero w momencie wywołania tej metody) tak naprawdę zmienia swój stan już po wywołaniu konstruktora, nie jest to jednak widoczne na zewnątrz obiektu i zachowuje się tak samo jak standardowy obiekt immutable.
Definicja od Oracle: https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html
Po co?
- Thread-safe – przy programach wielowątkowych i rozproszonych, nie trzeba się martwić o współbieżny dostęp do obiektu, ponieważ żaden wątek i tak nie może go zmienić. Jest to jedna z najważniejszych zalet, bo otrzymujemy istotną funkcjonalność niewielkim kosztem.
- Prostota – o wiele łatwiej jest zaplanować działanie kodu bazując na założeniu, że żaden obiekt nie może zmieniać swojego stanu.
- Większa stabilność – mniej będzie też awaryjnych sytuacji i błędów powstających przez błędne przekazanie referencji do obiektu, który w międzyczasie uległ zmianie. Co za tym idzie – dużo łatwiejsze debugowanie.
- Klucz w mapie – obiekt taki z powodzeniem może zostać użyty jako klucz np. w Hashmapie.
- Garbage Collector – posiada on optymalizacje pozwalające na skuteczniejsze zarządzanie pamięcią przy takim rodzaju obiektów.
- Odzwierciedlenie rzeczywistości – wiele elementów otaczającego nas świata jest w teorii niezmienna, więc może być to naturalne wymaganie przy budowaniu modelu domenowego.
Sugerowane dobre praktyki są takie, że każdy możliwy obiekt w Javie powinien być immutable i tylko w uzasadnionych przypadkach powinniśmy od tej zasady odstąpić.
Zaleca to m.in. Joshua Bloch w swojej książce „Effective Java”:
https://www.amazon.com/Effective-Java-3rd-Joshua-Bloch/dp/0134685997
Potencjalne wady
Największymi zaś problemami, które i tak są nikłe w porównaniu z zaletami, są zwiększona objętość kodu (zwłaszcza przy wykonywaniu głębokich kopii pól, które są obiektami) oraz w niektórych przypadkach może to się odbić na wydajności programu. W praktyce przy większości zastosowań nie powinniśmy mieć z tym problemu.
Jak utworzyć taki obiekt?
Zgodnie z tym co pisze Oracle:
https://docs.oracle.com/javase/tutorial/essential/concurrency/imstrat.html
- Klasa final – zastrzegamy naszą klasę przed jej rozszerzaniem. O samym final napiszę więcej w kolejnym artykule.
- Pola private i final – blokujemy wszystkie pola przed bezpośrednim dostępem oraz pilnujemy samych siebie przed omyłkową ich podmianą w późniejszym kodzie.
- Brak setterów – oczywiście nie możemy zmieniać tych wartości z zewnątrz.
Opcjonalnie: konstruktor prywatny + publiczne metody factory – unikniemy w ten sposób nadmiaru różnorodnych konstruktorów.
- Kopiowanie w konstruktorze – podczas przekazywania referencji do takiego obiektu w konstruktorze, musimy wykonać jego głęboką kopię na własne potrzeby. Odcinamy się w ten sposób od jego modyfikacji gdzieś na zewnątrz.
- Kopiowanie w getterach – tak samo nie możemy przekazać tej naszej własnej referencji w getterze. Wykonujemy kopię w getterze i zwracamy nowy obiekt o tych samych wartościach.
- Brak metod zmieniających te obiekty – tak jak wcześniej – nie udostępniamy na zewnątrz żadnych metod modyfikujących te obiekty.
Klasyczny przykład – String
Gdy padania pytanie o przykład obiektów immutable w standardowej bibliotece Jacy, zwykle od razu na myśl przychodzi String. I słusznie.
Jakiekolwiek działanie na Stringu zwraca zupełnie nowy obiekt, przykładowo:
Nie należy zapomnieć w tym momencie o ważnej optymalizacji, jaką jest String Pool – te same obiekty mogą zostać wykorzystane przez program wielokrotnie i to w zupełnie bezpieczny sposób, właśnie dlatego że są to obiekty niezmienne.
Równie dobrym przykładem są też obiekty klas opakowujących typy proste, takie jak Integer, Double, Float, Boolean.
1 2 |
Integer a = 0; a = 2; |
W takiej sytuacji powstaje zupełnie nowy obiekt klasy Integer przyjmujący wartość 2, a stary obiekt został pozbawiony referencji do siebie.
Uwaga na refleksje
Każdy obiekt immutable tak naprawdę można zmienić – mowa tu o mechanizmie refleksji, który jest jedną z bardziej widowiskowych rzeczy w Javie (nie w każdym języku istnieje podobna możliwość). Takie manipulacje kodem nie są jednak „uczciwym” chwytem i w większości przypadków jeśli mamy potrzebę uciekania się do takich rozwiązań, to znaczy że prawdopodobnie jest coś nie tak z architekturą programu. Warto jednak wiedzieć o istnieniu takiej możliwości, a temat samych refleksji poruszę w osobnym artykule.
Świetny przykład obrazujący to na Stringach można znaleźć na tej stronie: https://algs4.cs.princeton.edu/12oop/MutableString.java.html
Informatyk, programista. Obecnie Java Developer (Web Fullstack), właściciel studia Berrygames oraz prezes koła TK Games na Politechnice Wrocławskiej.