IntegerW pracy musiałem przygotować prezentację na temat MonoDroid, czyli o pisaniu aplikacji na telefony z Androidem w C#. Częścią tej prezentacji były proste przykłady podkreślające, dlaczego C# jest lepszy od Javy. Prezentacja miała jednogłośnie podkreślić na jednym slajdzie, że programowanie w C# jest bardziej intuicyjne.

Gdy szukałem powodów, dla których Java jest " zła" natknąłem się na pewne dziwne zachowanie, które wykazuje brak konsystencji w samy języku Javy.

Mimo iż programuję od kilku lat w Javie nigdy nie natknąłem się na ten problem. Zapytałem weteranów od Javy z uczelni PJWSTK, ale nie bardzo wiedzieli o czym ja mówię.

Dlatego pomyślałem sobie , że napiszę ten wpis by zawsze mieć jakieś fajne anegdoty dotyczące języka Java..

Integer

Kod jest następujący:

public class JavaIsWeird {

    public static void main(String[] args) {
        
          int a = 1000;
          int b = 1000;
          
          System.out.println(a == b);
          
          Integer c = 1000;
          Integer d = 1000;
          
          System.out.println(c == d);
          
          Integer e = 100;
          Integer f = 100;
          
          System.out.println(e == f);
    }
}

Za pierwszym razem porównujemy dwa typy proste int, które przechowują wartość 1000. Funkcja przyrównująca, a i b powinna zwrócić prawdę, ponieważ obie zmienne przechowują tę samą wartość.

Później tworzę dwa typy referencyjne typu Integer nadje im wartość 1000 i je przyrównuje. Tym razem mam do czynienia z typami referencyjnymi więc, pomimo iż zmienne c i d przechowują tę samą wartość są dwoma różnymi obiektami. Funkcja porównania zwróci “false”.

Za trzecim razem tworzę dwa typy Integer i nadaję im wartość 100. Analogicznie do poprzedniego przykładu funkcja powinna zwrócić false.

No cóż, zobaczmy, co wydrukowała konsola:

true
false
true

Jak widać ostatnie porównanie zwróciło “true”. Dziwne co? To wciąż są typy referencyjne więc jakim cudem e i f są tym samym obiektem.

Funkcja Equals zwróci ten sam wynik. Diabeł tam nie siedzi. Diabeł

Typy c i d nie są równe natomiast e i f są. To tak jakby funkcja porównująca była zależna od przechowywanie wartości ,a raczej od pewnego zakresu wartości.

Okej, więc co tutaj się stało. W trakcie porównywania wartości następuje wywołanie takiej funkcji.

public static Integer valueOf(int i) {
    final int offset = 128;
    if (i >= -128 && i <= 127) { 
// must cache return IntegerCache.cache[i + offset];
    }
    return new Integer(i);
}

Jak widzicie, gdy wartość Integer znajduję się w zakresie od –128 do 127 wtedy nie jest zwracany nowy Integer jest zwracany zapisany w pamięci obiekt. Idąc tym tropem dla przypisanej wartości 100 zwracany jest ten sam obiekt. Sprawdźmy to:

public class JavaIsWeird {

    public static void main(String[] args) {
        
        Integer i = 100;
        Integer p = 100;
        if (i == p)  
            System.out.println("i i p są takie same. Mają ten sam obiekt");
        if (i != p)
            System.out.println("i i p są różne. Mają różne obiekty.");   
        if(i.equals(p))
            System.out.println("i i p zawierają tą samą wartość.");
    }
}

Wynik poniższego kodu to:

i i p są takie same. Mają ten sam obiekt
i i p zawierają tą samą wartość.

Warto tu zaznaczyć ,że “i” i “p” są tymi samymi obiektami dlatego funkcja przyrównania “==” zwróci true.

Gdy wartość Integrer jest spoza zakresu zapisane w pamięci obiekty nie są używane. Zwracany jest nowy obiekt.

Warto pamiętać , że wyrażenie “==” w Javie zawsze określa równość obiektów, czyli sprawdza czy obie zmienne referują się do tego samego obiektu. Nie ma on żadnego przeciążenia dla porównania wypakowanych obiektów.

Dokumentacja

Specyfikacja Javy nawet odnosi się do tej “funkcjonalności”. Byłem w lekkim szoku, dlaczego coś takiego jest akceptowane. Jaki to ma sens, jakie są zalety takiego zachowania z punktu widzenia pamięci. Osobiście wolałbym aby kod zawsze był spójny w działaniu niż oszczędzał śladowe ilości pamięci.

Java Language Specification section 5.1.7:

If the value p being boxed is true, false, a byte, a char in the range \u0000 to \u007f, or an int or short number between -128 and 127, then let r1 and r2 be the results of any two boxing conversions of p. It is always the case that r1 == r2.

Jeżeli wartość p, która jest zapakowana jest wartością prawdziwą, fałszywą, bajtem bądź znakiem w zakresie  \u0000 to \u007f lub jest int-em, lub short-em w zakresie od –128 do 127, wtedy r1 i r2 mogą zwrócić rezultat dwóch różnych konwersji p.

Tłumaczenie tego fragmentu tekst nie było przyjemnością.

Ten wpis w dokumentacji też zadaje ciekawe pytanie. JVM może buforować więcej wartości, co zatem idzie nigdy do końca nie możemy być pewni jaki wynik otrzymamy wykonując taką operację.

Integer i1 = 129;
Integer i2 = 129;
boolean bo = (i1 == i2);
System.out.println(bo);

Zachowanie kodu jest zależne od maszyny wirtualnej Javy.

Wersja Javy 6 wspiera zwiększenie maksymalnej liczby przechowywanych wartości niż 127.

Lekcja na dziś

Nie używaj operatora “==” przy typach referencyjnych to zawsze prowadzi do błędu. Zwłaszcza w Javie.