Java HR: obiekty niezmienne (immutable)

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.
Uwaga – jeśli polami są inne, mutowalne obiekty:
  • 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.

Wykonujemy oczywistą na pierwszy rzut oka operację:

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.

Wszystkie artykuły autora>>

Dodaj komentarz