24 kwietnia 2015

equals() vs '=='

Częstą praktyką wśród młodych programistów jest korzystanie z operatora '==' do porównywania zmiennych typu String oraz chociażby Integer. Po pewnym czasie użytkowania tego mechanizmu często dochodzi do sytuacji, w której młody programista wpada w zakłopotanie przy porównywaniu dwóch identycznych wartości tekstowych. Dlaczego? Ponieważ spodziewając się wyniku true otrzymują oni niezrozumiałą dla nich wartość false. Dlaczego tak się dzieje?

Content:
  • Czym różni się operator '==' od metody equals()
  • Przykłady implementacji metody equals()

Czym różni się operator '==' od metody equals()


Najważniejszą różnicą jest porównywana informacja. W przypadku operatora '==' porównywana jest przestrzeń pamięci naszej aplikacji. A dokładniej sprawdzane jest czy wartość a i wartość b zapisane są w tym samym miejscu w pamięci naszej wirtualnej maszyny. Jeśli obie zmienne odwołują się do tego samego miejsca, wtedy wartość otrzymana będzie równa true. Jeśli natomiast wartości zmiennych będą identyczne, jednak zapisane w innych miejscach w pamięci - wtedy naszym wynikiem będzie false.

W przypadku metody equals()(przyjmijmy przykład dla zmiennej typu String) porównywana jest wartość (znak po znaku) zmiennej a i b. Niezależnie czy odwołują się one do tego samego miejsca w pamięci czy też nie (oraz przy założeniu że ich wartość jest identyczna) wynik wywołania metody equals() będzie równy true.

Może czas na przykład:

        String a = "test"; // tworzy obiekt typu String
        String b = "test"; // tworzy referencje do obiektu a
        
        String c = new String("test"); // tworzy nowy obiekt typu String 
        
        // ten if zwróci true
        if(a == b){

            System.out.println("True!");
        }
        else System.out.println("False!");
        
        // ten if zwróci false
        if(a == c){

            System.out.println("True!");
        }
        else System.out.println("False!");

Konsola:
run:
True!
False!
BUILD SUCCESSFUL (total time: 0 seconds)

W powyższym przykładzie jasno widać, iż operator '==' porównuje czy obie wartości zapisane są w tym samym miejscu w pamięci. Na wyjaśnienie zasługuje fakt, iż konstrukcja String c = new String("test") nie sprawdza w String pool istnienia obiektu typu String o wartości test. (Jeśli chcesz wiedzieć dlaczego tak się dzieje i o co chodzi z String pool zajrzyj tutaj).

Teraz sprawdźmy jaki wynik przyniesie metoda equals():
        String a = "test"; // tworzy obiekt typu String
        String b = "test"; // tworzy referencje do obiektu a
        
        String c = new String("test"); // tworzy nowy obiekt typu String 
        
        // ten if zwróci true
        if(a.equals(b)){

            System.out.println("True!");
        }
        else System.out.println("False!");
        
        // ten if również zwróci true
        if(a.equals(c)){

            System.out.println("True!");
        }
        else System.out.println("False!");

Konsola:
run:
True!
True!
BUILD SUCCESSFUL (total time: 0 seconds)

Jak widać metoda equals() porównuje bezpośrednio wartość podanych zmiennych, mimo iż wartości zmiennej a i c znajdują się w innej przestrzeni pamięci naszej aplikacji, wartość metody w obu przypadkach pozostaje równa true!
Ważna konkluzja: Do porównywania Stringów używamy tylko i wyłącznie metody equals(). Wyjątkiem od tej reguły może być bezpośrednie powiązanie wartości z przestrzenią w pamięci.


Przykłady implementacji metody equals()


Na początek przyjrzyjmy się implementacji metody equals() wewnątrz klasy string:
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

Jest to metoda zwracająca boolean, więc jej wynikiem zawsze będzie true albo false. Co dzieje się po kolei:

1. Metoda przyjmuje Object jako anObject (w naszym przypadku String) oraz "wrzuca" go do pierwszego bloku if. Tu bardzo ważna kwestia (potwierdzająca to co omawialiśmy w poście Niezmienność typu String - metoda sprawdza czy anObject nie odwołuje się do tego samego miejsca w pamięci naszej aplikacji co podany przez nas do porównania String. Jeśli tak, to automatycznie zostaje zwrócona wartość true. Jest to wyraźny przykład zwiększania wydajności metody - zdecydowanie warto stosować takie rozwiązania!

2. Jeśli anObject nie odwołuje się do tego samego miejsca w pamięci wtedy następuje kolejny blok if który to kolejno:
a) poprzez wywołanie operatora instanceof sprawdza czy anObject jest na pewno obiektem klasy String (wszak ciężko jest porównywać coś co nie jest tej samej klasy...)
b) następnie tworzy kolejną instancję klasy String - anotherString
c) tworzy zmienną typu int, która przyjmuje wartość równą ilości znaków zawartych w podanym przez nas Stringu (value - zmienna tablicowa typu char służąca do przechowywania poszczególnych znaków naszego Stringa; .length - zwraca ilość znaków zawartą w zmiennej value)
d) kolejny blok if sprawdzający czy długość obu podanych Stringów jest jednakowa - jeśli tak to przechodzimy dalej, jeśli nie zwracamy false (kolejny element poprawiający wydajność!)
e) następnie metoda tworzy dwie tablice typu char (v1 i v2) przyjmujące kolejno znaki obiektu, który podaliśmy oraz obiektu porównywanego anObject
f) tworzy int i jako licznik inkrementacji
g) blok while, który od wartości n(długość naszego Stringa - ilość char'ów) odejmuje przy każdym przebiegu 1 (dopóki n jest różne od 0) i sprawdza każdą literę obu Stringów (nasz String oraz anObject)po kolei pod względem równości - jeśli któraś z liter będzie odbiegać od litery o tym samym indeksie w drugim słowie, wtedy automatycznie zostanie zwrócona wartość false

To by było na tyle jeśli chodzi o metodę equals() wewnątrz klasy String. Mam nadzieję, że zauważyliście również zalety płynące z czytania kodu źródłowego Javy :)

Może dla porównania i potwierdzenia różnorodności implementacji metody equals() w różnych klasach weźmy na warsztat tę metodę z klasy Integer:
    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

W tym przypadku nie mamy za bardzo czego opisywać. Najprostsze w świecie porównanie dwóch wartości.

W tym miejscu moglibyśmy skończyć. Jak zwykle (od dwóch postów :) ) zachęcam do komentowania oraz uzupełniania zapisanych przeze mnie informacji. Mam nadzieję że wam się przydadzą.

23 kwietnia 2015

Pass by Value

Jedną z fundamentalnych rzeczy jaką warto przyswoić sobie na początku przygody z Javą, jest sposób przekazywania dwóch rodzajów zmiennych: zmiennych podstawowych (prymitywnych) oraz zmiennych referencyjnych (obiektowych).

Content:
  • Czym jest zmienna podstawowa i referencyjna
  • Przekazywanie wartości innym zmiennym i metodom


Zmienna podstawowa i referencyjna


Zmienna podstawowa jest zmienną typu prostego (prymitywnego). Przetrzymują one w sobie wartości przypisane im podczas ich deklaracji. Takie typy to np. byte, short, int, boolean, long, double czy float.
int i = 5;
double d = 0.0;
byte b = 3;
short s = 7; 
Zmienna referencyjna to zmienna typu obiektowego. Są one swego rodzaju pojemnikami przetrzymującymi referencje do stworzonych podczas deklaracji obiektów. W żadnym wypadku nie można mówić, iż przetrzymują one w sobie konkretne obiekty! Przykładem takich typów może być np. String lub każdy inny obiekt reprezentujący klasę opisaną przy deklaracji.
String imie = new String("Ania");
String imie_2 = "Andrzej";
Date data = new Date();
SomeObject = new SomeObject();

Przekazywanie wartości innym zmiennym i metodom


Zmienne podstawowe
int a = 5;
int b = a; // kopiuje wartość zmiennej a i przypisuje ją zmiennej b
           // od teraz zmienna b nie ma wpływu na zmienną a

public void doSomething(int c){
  // wykonaj operację na c
}

doSomething(a); // kopiuje wartość zmiennej a(w tym przypadku 5)
                // do metody doSomething()
                // nie ma wpływu na zmienną a i b

Prześledźmy po kolei powyższy, nie skomplikowany przykład. W pierwszych dwóch liniach mamy przykład przekazania wartości z jednej zmiennej do drugiej. W efekcie otrzymamy dwie zmienne typu prostego int o wartości 5. Wykonanie operacji na zmiennej a, nie powoduje wpływu na zmienną b i odwrotnie!
Sytuacja jest bardziej przewrotna w momencie przekazywania takiej zmiennej do metody. Przy przekazywaniu zmiennej typu prymitywnego do metody również wykorzystujemy w metodzie tylko jej wartość. Ważne jest aby zapamiętać, iż metoda wykonująca operacje na tej wartości nie ma żadnego wpływu na pierwotną zmienną (metoda doSomething tworzy sobie własną zmienną [w tym przypadku int c], której przypisuje wartość zmiennej a i to właśnie na niej [zmienna c] wykonuje wszelkie operacje.

Zmienne referencyjne
Cat a = new Cat();
Cat b = a; // kopiuje referencje zmiennej a i przypisuje ją zmiennej b
           // od teraz obie zmienne mają wpływ na obiekt klasy Cat

public void doSomething(Cat c){
  // wykonaj operację na c
}

doSomething(a); // kopiuje referencję a do wykorzystania w
                // metodzie doSomething()
                // ma wpływ na obiekt Cat tak samo jak zmienna a i b

Prześledźmy ponownie lekko odmieniony kod. W pierwszych dwóch liniach przekazujemy referencję zmiennej a do zmiennej b. Różnica w porównaniu do zmiennych podstawowych wynika ze stworzenia podczas deklaracji obiektu Cat. Dlatego właśnie jest to typ referencyjny, gdyż przechowuje referencję do nowo powstałego obiektu, a nie jest tym obiektem. Po tej operacji mamy jeden obiekt klasy Cat oraz dwie referencje wskazujące na ten obiekt - a i b. Wykonanie operacji na zmiennej a i b powoduje wykonanie zmian na obiekcie do którego te referencje prowadzą!
W kolejnych liniach mamy zadeklarowaną metodę przyjmującą za parametr obiekt klasy Cat. Po przekazaniu zmiennej referencyjnej do metody zostaje stworzona kolejna referencja (w tym przypadku zmienna c), która wszystkie operacje będzie wykonywała na tym samym obiekcie klasy Cat, który został zainicjalizowany w pierwszej linii powyższego kodu.

Wyżej wymienione zależności opisuje się poprzez Pass by Value. W Javie wszystkie przekazania wykonują się za pomocą wartości! Dlatego niepoprawnym sformułowaniem jest często spotykane stwierdzenie przekazania obiektu (Pass by Object).

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