Dlaczego immutability jest takie wazne

0

Witam,
Nie do końca rozumiem znaczenie immutability (niezmienności) w Javie. Jak wiadomo, klasa niezmienna nie może zmieniać swojego stanu. Jak wiadomo stan to coś skomplikowanego, co może powodować problemy, a prostota jest dobra.

Moje pytania są następujące:

  1. Jakie są bezpośrednie korzyści z uczynienia klasy niezmiennej?
  2. Dlaczego korzystne jest, aby klasy String były niezmienne?

Rozumiem, ze nie posiadanie stanu jest korzystne z punktu widzenia wspolbieznosci: jak nie ma stanu zawartosc obiektu na pewno nie zmieni sie. Wymusza to rowniez lepszy styl programowania. Ale na pewno chodzi o cos wiecej. Bede wdzieczny jak podzielicie sie ta wiedza.

Pozdrawiam,

1

Ale na pewno chodzi o cos wiecej.

Wcale nie, najwazniejsze jest thread-safety w tej calej zabawie. Reszta (prostota, czytelnosc, zwiezlosc, debuggowanie) to drobiazgi w porownaniu z tym.

0

Jesli obkekty sa immutable to sa bezpieczne do uzywania z wielu watkow - nie moga sie zmienic, nie ma race conditions itp. co w czasach wielu prockow / rdzeni moze byc wazne. Duzo latwiej rowniez jest wnioskowac jesli ma sie tylko obiekty immutable - np. jesli jakiejs metodzie podasz referencje to obiektu mutowalnego, nigdy nie wiesz co sie z nim moze wydarzyc; jak podasz immutable, to nie ma uja, nie zmieni sie (ok, w Javie moze sie zmienic za pomoca refleksji itp. ale pomijamy anomalie ;d).

0

Obiekt niemutowalny łatwiej jest też cache'ować. Gdy chcemy przechowywać obiekty mutowalne w cache to przy zwracaniu trzeba robić kopie by było bezpiecznie.

W przypadku String niemutowalność ma też inną zaletę - można robić deduplikację przy GC, obecnie trwają nad tym prace: http://openjdk.java.net/jeps/192

0

Stringi sa dosc czesto uzywane jako klucze w mapach, a fakt ze sa niemutowalne jest w tym wypadku bardzo dobra charakterystyka. Gdzies czytalem rowniez, ze powodami rowniez bylo bezpieczenstwo, poniewaz stringi sa uzywane w wielu miejscach ww jvm, ale tutaj nie potrafie powiedziec nic wiecej, niestety...

0

To nie niemutowalnosc obiektu jest 'dobra charakterystyka' a niemutowalnosc stanu na podstawie ktorego generowany jest hash/wykonywany equals jest 'niezbedna charakterystyka'.

0

Masz racje. Jednakze, hasz powinien byc liczony z tych pol, ktore sa uzywane do equals. Mysle ze posiadanie obiektow w ktorych sa pola 'ignorowane' przez equals / hashcode sa dosc niecodzienne (moze sie myle?), i w znacznej wiekszosci wypadkow hashcode i equals uzywaja wszystkich pol obiektu, co sprawia ze niemutowalny stan do hashcode == niemutowalny obiekt. A przynajmniej takie sa moje doswiadczenia.

0

Zalezy od logiki porownania obiektu, przykladowo obiekt klasy Name { first: String, last: String } powinien miec generowany hashcode/equals na podstawie swoich atrybutow bo obiekt ten ma nic nie znaczacy czas zycia dla logiki, wiec mozna go ot tak wymienic. Natomiast jak mam obiekt Client { id: Int, name: Name, something: ... } to zazwyczaj korzystam tylko z id, bo niezaleznie od wartosci innych pol, tylko id jest znaczaca informacja w porownywaniu.

0

Czytałem kiedyś o takich argumentach dotyczących bezpieczeństwa. W kodzie jest próba otwarcia pliku - nazwa pliku jest Stringiem. Rusza SecurityManager by sprawdzić czy plik jest dostępny. Jednocześnie inny wątek zmienia String z nazwą pliku.

0

No to mozna isc dalej: refleksja ladujesz klase ktorej nazwa jest stringiem, jest sprawdzane czy klasa moze byc zaladowana i z jakimi uprawnieniami (jaki codabase i jakie permissions itp.) i po tym sprawdzeniu gdzies w kodzie nazwa klasy jest zmieniana na jakas inna, ktora jest bardzo zla...

0

Zgoda. Przypomniałem sobie źródło informacji, książka Ian F. Darwin, Java Cookbook.

1

Niemutowalność w przypadku String wynika przede wszystkim ze sposobu ich implementacji:

String a = "test";
String b = "test";

W takim wypadku a == b zwróci true, ponieważ "pod spodem" znajduje się wzorzec flyweight. Bez ograniczenia zmiany stanu wywołanie przykładowo a.toUpperCase() spowodowało by też zmianę w b ponieważ obie zmienne wskazują na dokładnie ten sam obiekt. Omijamy to poprzez np. b = new String("test");.
Wzorzec flyweight jest zastosowany z dwóch powodów:

  1. wydajność - kiedyś naprawdę liczono RAM i możliwość współdzielenia pamięci była rzeczą dobrą. Współdzielenie pamięci jest fajne o ile to co w niej zapisano nie zmienia się. Dodatkowo można np. cachować wyliczony hashCode co znacząco wpływa na wydajność użycia kolekcji korzystających z tej wartości.
  2. optymalizacja - kompilator "wie", że ma do czynienia z obiektem niezmiennym i może zastosować pewne optymalizacje.
    Kolejnym elementem jest sposób w jaki startuje JVM. Klasy ładowane się "po nazwie", a ta jest przechowywana w postaci stringa. Zapewnienie niezmienności nazwy jest tu całkiem dobrym pomysłem.
    Z takiego też punktu należy też uniemożliwić zmianę stringów przechowujących np. adres bazy danych, loginy czy hasła.
    Podsumowując niemutowalność klasy String wpisuje się w koncepcję języka, który ma zabezpieczyć programistę przed różnymi błędami. Tu wynikającymi z możliwości zmiany zawartości pamięci.

Niemutowalność w bardziej ogólnym podejściu pozwala na wprowadzenie wielu ciekawych rozwiązań na poziomie zarówno języka jak i architektury aplikacji. Java jest tu dość słabym przykładem, bo jednak nie ma domyślnej niemutowalności. Dużo lepszym przykładem jest chociażby Scala ze swoimi kolekcjami, które można obrabiać skalując w dowolny sposób ilość jednostek obliczeniowych (rdzeni, procków czy też całych serwerów), bo wiadomo, że w środku obliczeń nic się nie zmieni.

Jednak najlepszy przykład na to dlaczego niemutowalność jest OK, miałem okazję poznać na LambdaDays. Otóż niezależnie czy dana pamięć jest współdzielona czy też nie, to w przypadku gdy wystąpi wyjątek jeżeli masz gwarancję, że dany fragment jest niezmienny możesz:

  1. powtórzyć obliczenia, bo wiesz co masz na początku - uzyskujesz coś w stylu transakcji.
  2. jego awaria na pewno nie ma wpływu na całość aplikacji, zatem można spokojnie "zresetować" dany fragment pamięci.
  3. wynikające z 2. możesz zastosować znacznie "mocniejsze" scenariusze failover np. przełączyć rozmowę telefoniczną "w locie" na działający BTS. W najgorszym wypadku klient "zgubi" jakiś niewielki fragment danych, ale jako, że całość idzie w sekwencji zatem mechanizm kontroli poradzi sobie z tym problemem.

W dodatku nie ma znaczenia czy środowisko jest tu wielo czy jedno procesorowe. Po prostu masz gwarancję, że nic ci nie zmieni danego kawałka danych tym samym możesz "w ciemno" usuwać wąskie gardła w stylu "synchronizatorów". Nie są potrzebne w takiej ilości.

0

Immutability przydaje się nawet w programach jednowątkowych.

Np. ostatnio miałem następujący problem:
Do pewnej metody przekazywana była lista. W założeniach metoda nie miała zmieniać tej listy, a jedynie operować na obiektach w niej się znajdujących.
Okazało się, że przy pewnych okolicznościach, metoda usuwa cześć elementów z listy.
Kod, który działał po wykonaniu tej metody, nie funkcjonował już poprawnie (z powodu braku niektórych elementów w liście).

Niestety takie błędy są trudne do wykrycia.

Problem rozwiązany poprzez opakowanie listy w Collections.unmodifiableList, a w problematycznej metodzie zrobienie kopii listy.

0

Dziekuje wszystkim za wyczerpujace odpowiedzi. :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1