21 kwietnia 2015

String Immutable - niezmienność typu String

Witam.
Na pierwszy wpis wybrałem pewien fundamentalny fakt dotyczący Javy, jednak często pomijany przy głębszym analizowaniu zasad kierujących zachowaniem naszego "ukochanego" języka. Mianowicie chodzi o zmienne typu String, a dokładniej o niezmienność typu String.

Content:
  • Co daje niezmienność typu String
  • Jak ta niezmienność wpływa na wydajność

Na wstępie można by poddać pod wątpliwość samo stwierdzenie zmienna typu String. Wolałbym jednak nie zanurzać się zbyt głęboko w filozoficzne rozważania - jak wiadomo programiście nikt nie płaci za filozofowanie :) Wystarczy, że wyjaśnimy nietrafność tego sformułowania na podstawie najważniejszej cechy typu String: jest on niezmienny!


Co daje niezmienność typu String


Na początek powinniśmy zacząć od teorii. Każdy nowo stworzony String zostaje zapisany w puli Stringów (String pool), która zajmuje miejsce wewnątrz sterty* (heap). Do Javy 6 pula Stringów umieszczona była wewnątrz stałej generacji (Permanent Generation), od javy 7 została przeniesiona do głównej części sterty, która dysponuje większą powierzchnią i jest bardziej elastyczna na konfiguracje (więcej w innym poście). Gdy String zostanie stworzony w najbardziej naturalny dla programisty sposób (literal):
String name = "Ania";
JVM sprawdzi wewnątrz String pool czy na stercie istnieje już obiekt o wartości "Ania". Jeśli tak, to zostanie do niego przypisana referencja name, jeśli nie - wtedy zostanie stworzony nowy obiekt typu String na naszej stercie.
Dla przykładu -jeśli wykonamy operację:
String name = "Ania";
String name_2 = "Ania";
name_2 = name2+"x";
to po wykonaniu pierwszych dwóch linii kodu otrzymamy dwie referencje wskazujące na ten sam obiekt (wywołanie name i name_2 pokaże wartość tego samego obiektu). Jeśli jednak rozpatrzyć trzecią linię kodu, w tym przypadku zostanie stworzony nowy obiekt typu String na naszej stercie, a w String pool zostanie zapisana informacja o istnieniu nowego obiektu o wartości "Aniax". Gdyby Stringi były zmienne (nie były niezmienne) to taka operacja prowadziłaby do nieciekawej sytuacji - po wywołaniu name otrzymalibyśmy wartość "Aniax", co mogłoby skutkować poważnymi problemami w działaniu aplikacji. W tym miejscu natrafiamy pierwszą i chyba najważniejszą zaletę niezmienności typu String. String jest niezmienny, aby wiele referencji mogło odwoływać się do jednego obiektu. Pozwala nam to zaoszczędzić pamięć aplikacji poprzez nie powtarzanie instancji identycznych obiektów.


Dla formalności podam również drugi sposób tworzenia Stringów:
String name = "Ania";

String name_x = new String("Ania"); // tworzy nowy obiekt na stercie pomijając sprawdzanie w String pool

Powyżej mamy stworzone dwa obiekty typu String. Pierwszy zainicjalizowany literal, drugi natomiast poprzez stworzenie nowego obiektu klasy z pominięciem sprawdzenia w String pool, tzn. zostaje stworzony duplikat pod względem wartości obiektu - mamy dwa obiekty z wartością "Ania". Zgodnie z poprzednimi przykładami oraz przytoczoną zaletą niezmienności typu String można łatwo wywnioskować, dlaczego jest to praktycznie nie stosowana praktyka!


Powyższe można zweryfikować poprzez zastosowanie operatora == oraz metody .equals(). Operator porównania sprawdza czy oba (w tym przypadku zmienne typu String) odwołują się do tego samego miejsca w pamięci. Dla przykładu porównanie zmiennej name i name_2 da wartość true, natomiast name i name_x da wartość false. Dzieje się tak dlatego, iż name i name_2 wskazują na to samo miejsce w pamięci (ten sam obiekt), natomiast name i name_x wskazują na różne miejsca w pamięci (różne obiekty).
Inaczej będzie wyglądać sytuacja w przypadku metody equals(), gdyż sprawdza ona wartość porównywalnych zmiennych (różne klasy posiadają różne implementacje tej metody). W obu wyżej wymienionych przypadkach wynik będzie równy true.
Bardziej czytelny przykład tutaj.


Inną zaletą niezmienności typu String jest jego bezpieczeństwo ogólne oraz ThreadSafe.
Bezpieczeństwo aplikacji w dużej części u podstaw jest uzależnione od niezmienności Stringów. Jeśli by się głębiej zastanowić nad funkcją tego typu w każdej aplikacji, okazałoby się, że fundamentem każdej z nich (obok ogólnej architektury i rozplanowania) jest właśnie zmienna typu String. Jest ona podstawowym narzędziem do nadawania nazw / identyfikacji / wywoływania plików, połączeń sieciowych, adresów URL, przekazywania zapytań do baz danych, nadawania uprawnień, a co najważniejsze - jest ona wykorzystywana przez mechanizm ładowania klas (class loading mechanism). Nie trudno sobie wyobrazić sytuację, gdy podczas ataków na naszą aplikację wykorzystana byłaby zmienność naszej zmiennej tekstowej w wyniku czego załadowalibyśmy niechciane zapytani do bazy danych lub chociażby niechcianą klasę mil.vogoon.DiskErasingWriter zamiast java.io.Writer. Tak fundamentalne dla każdej aplikacji czynności muszą polegać na pewnym źródle danych - niezmiennym i nie narażonym na ingerencję z zewnątrz. Dlatego klasa String jest klasą finalną (public final class), co dodatkowo zapewnia jej niezmienność. Nie muszę chyba przypominać, iż klasa finalna pomaga optymalizować aplikację - informuje ona kompilator, iż jej postać jest ostateczna i nie będzie rozszerzana (extends) przez żadną inną klasę.

Powyższe nie tyczy się jednak przechowywania haseł i loginów, lub innych informacji które są wrażliwe na wgląd ze strony osób niepożądanych. Twórcy Javy sami rekomendują w takiej sytuacji nie korzystać ze Stringa tylko z tablicy char'ów. Więcej na ten temat tutaj.


W wielu miejscach w internecie oraz w książce Brian'a Goetz'a Java Concurrency in Practice, można znaleźć informację: Immutable objects are thread safe. Idąc za tym powtarzanym jak mantra stwierdzeniem, dochodzimy do stwierdzenia: String is thread safe. I w dużej części przypadków tak właśnie jest, jednak znów tyczy się to raczej prostych aplikacji. Należy pamiętać iż niezmienność (immutable) jest narzędziem ułatwiającym projektowanie współbieżne (wielowątkowe), nie jest jednak jego gwarantem! Wystarczy spojrzeć na niebezpieczne wątkowo metody klas niezmiennych - chociażby klasy String. Po więcej informacji odsyłam do artykułu z DZone, oraz do mojego postu o tworzeniu obiektów niezmiennych (Immutable Objects).

Ostatnim według mnie powodem dla którego obiekt typu String jest niezmienny, jest umożliwienie temu typowi buforowania swojego hashcode. Dzięki czemu zyskujemy duży skok wydajności przy wielokrotnym wykorzystaniu hashcode danej zmiennej. Jest to oczywiście spowodowane brakiem powtarzających się w nieskończoność wywołań metody obliczającej takowy hashcode. Dzięki temu wykorzystanie Stringów w hashMap lub hashTable staje się dużo bardziej efektywne. Dlaczego akurat w tych przypadkach? Ponieważ w większości z tych konstrukcji jako klucza (hashMap działa jako lista dwuargumentowych wpisów: klucz-wartość) używamy właśnie typu String.


To by było na tyle jeśli chodzi o niezmienność typu String oraz wpływ tej cechy na wydajność aplikacji. W najbliższym czasie przygotuję informację na temat złego wpływu niezmienności Stringa na operacje subString w Javie do wersji 7.

Zachęcam do kontaktu oraz do podania w komentarzu waszych przykładów na użyteczność niezmienności typu String w naszej codziennej pracy!


* - sterta (heap) - wikipedia

Brak komentarzy:

Prześlij komentarz