2009
01.18

Jakiś czas temu na mojej uczelni mieliśmy kolokwium z przedmiotu “Podstawy programowania w Javie”. Kolokwium na pierwszy rzut oka wydawało się banalnie proste, więc po raz kolejny liczyłem na 100% punktów.

Podczas rozdania wyników niestety okazało się troszkę inaczej, a nasz ćwiczeniowiec (Kuba Staszczyk) powiedział nam “Nie mów hop, dopóki nie przeskoczysz!”. W sumie to i dobrze, bo 100% zwalniało z ćwiczeń, a koleś co ćwiczenia powie coś ciekawego, tym bardziej, że w javie piszę od października – właśnie na studiach, ponieważ wcześniej nie odczuwałem takiej potrzeby.

Pytania, na których się przejechałem wydały mi się na tyle ciekawe i zagadkowe, że postanowiłem o nich napisać na blogu. Warto jeszcze podkreślić, że kolokwium odbywało się “na kartce”, więc nie było możliwości przetestowania tego na kompilatorze, choć z drugiej strony pytania wydały się tak trywialne, że nie wiem czy po kompilator bym sięgnął. Tak więc do dzieła!

Pytanie: Co wyświetli poniższy kawałek kodu:

int x = 4;
System.out.println("Value is " + ((x > 4) ? 99.99 : 9));

Odpowiedzi:

  1. Value is 99.99
  2. Value is 9
  3. Value is 9.0

Jak działa operator trójargumentowy każdy wie: jeśli warunek (x > 4) jest spełniony to wynikiem będzie wartość po znaku zapytania (99.99), a jeśli warunek nie jest spełniony to wynikiem będzie wartość po dwukropku (9). Oczywiście dłużej się nie zastanawiając zaznaczyłem odpowiedź drugą a mianowicie, że program wyświetli “Value is 9″, ponieważ x nie jest większe od 4, tylko równe 4 (wyrażenie nie jest prawdziwe), więc zwrócona zostanie wartość po dwukropku, czyli właśnie 9.

Jakież było moje zdziwienie, gdy dowiedziałem się, że jestem w błędzie, ponieważ program wyświetli “Value is 9.0″! Pomyślałem sobie “Jak to możliwe!? Przecież wartość 9.0 nie jest nigdzie zdefiniowana!”.

Dziś siadłem do tego problemu i go głębiej przeanalizowałem. Jak się okazało java robi sobie niejawne rzutowanie i fakt, operator trójargumentowy zwróci 9, jednak nie jako integer, ale jako double. Dlaczego tak się dzieje? Ponieważ jednym ze zwracanych wartości (99.99) jest typu double, więc kompilator sobie rzutuję drugą wartość też na typ double, stąd mamy 9.0. Mam nadzieję, że poniższych kilka przykładów wyjaśni sprawę:

// Zdefiniujmy sobie metodę Foo, która będzie nam wyświetlać
// jakiego typu jest przekazany argument oraz wartość tego argumentu
public static void foo(Object obj) {
  System.out.println(obj.getClass() + " # Value = " + obj);
}

// Dalej wykonajmy kilka testów
int x = 4;

foo( x > 4 ? 99.99 : 9);
foo( x > 4 ? 'c' : 100.0);
foo( x > 4 ? 'c' : 100);
foo( x > 4 ? 'c' : "d");
foo( x > 4 ? "c" : 'd');

Poniżej wyniki:

class java.lang.Double # Value = 9.0
class java.lang.Double # Value = 100.0
class java.lang.Character # Value = d
class java.lang.String # Value = d
class java.lang.Character # Value = d

Co się stało?

  1. Nastąpiła niejawna konwersja z integera na typ double (typ double, jest pojemniejszy niż integer)
  2. Konwersja z chara na double (double jest pojemniejszy niż char)
  3. Konwersja z integera na chara (wartość 100 odpowiada znakowi ASCII “d”). Tutaj trochę nie rozumiem co się dzieje, ponieważ zgodnie z rozumowaniem idziemy, ku pojemniejszemu typowi (char ma 8 bitów, integer 32-bity) a tu się nie zgadza.
  4. Brak konwersji, zwracamy stringa, tak jak powinno być
  5. Brak konwersji (!) zwracamy chara, choć spodziewałem się stringa, gdyż jest pojemniejszy (jednak nie jest typem prostym?)

Ostatni punkt odnośnie typów prostych i złożonych mnie zaciekawił, więc postanowiłem to samo zrobić z typami złożonymi: Integer, Double, String, pomimo, że spodziewałem się tych samych wyników:

foo( x > 4 ? new Double(99.99) : new Integer(9) );
foo( x > 4 ? new Character('c') : new Double(100.0) );
foo( x > 4 ? new Character('c') : new Integer(100) );
foo( x > 4 ? new Character('c') : new String("d") );
foo( x > 4 ? new String("c") : new Character('d') );

I się nie myliłem:

class java.lang.Double # Value = 9.0
class java.lang.Double # Value = 100.0
class java.lang.Integer # Value = 100
class java.lang.String # Value = d
class java.lang.Character # Value = d

Wyniki takie same jak na typach prostych. Jedyne rzutowania to:

  • integer -> double
  • integer -> char

Szczerze to poza powyższą regułą (?) gubię się jeszcze w tym niejawnym rzutowaniu, ponieważ jestem przyzwyczajony do tego co mam w znanych mi językach: Ruby oraz C++.

Ruby

x = 4
puts "Value is " + (x > 4 ? 99.99 : 9).to_s
# wynik: Value is 9

C++

#include <iostream>

using namespace std;

int main() {
    int x = 4;

    // Wyświetli: "Value is 9"
    cout << "Value is " << (x > 4 ? 99.99 : 9) << endl;

    return 0;
}

Jak widzimy tylko w Javie jest inaczej, ponieważ kod w javie wyświetli nam “Value is 9.0″. Powoli się przekonuje, że w javie nie jest tak samo jak C++ czy Rubym ;) Tak miedzy nami to cienias ten Integer ;))) Każdy go rzutuje jak chce ;P

Ok, teraz czas na drugie ciekawe pytanie:

Pytanie: Co wyświetli poniższy kod:

String str1 = "xyz";
String str2 = "xyz";

if( str1 == str2 )
    System.out.println("Hello World");

Odpowiedzi:

  1. Hello World
  2. Nic nie wyświetli

Pierwsza myśl jaka przychodzi do głowy, to że nie wyświetli niczego, ponieważ w javie operator == (jeśli jego argumentami są typy złożone) zwraca true, jeśli obiekty “wskazują” na ten sam obiekt w pamięci. W naszym przypadku zmienne str1 oraz str2 wydawać się by mogło to dwie zupełnie różne zmienne.

Jest jednak inaczej, ponieważ kompilator optymalizuje nasz kod podczas procesu kompilacji i widzi, że oba stringi mają tą samą zawartość, więc robi tak, że str1 oraz str2 wskazują na ten sam obszar pamięci (są referencją do tego samego obiektu), więc operator == zwraca true i program na wyświetli “Hello World”.

Jak widzimy kompilator nie śpi i robi kupę dobrej roboty … przez którą na kolosie możemy mieć problemy ;)

Poniżej jeszcze kilka właściwości stringów w javie:

String str1 = "Hello World";
String str2 = "Hello World";
String str3 = "Hello ”+ "World";
String world = "World";
String str4 = "Hello" + world;
String str5 = new String("Hello World");

Co tu mamy? Zmienne str1, str2, str3 kompilator wrzuci do tego samego obszaru pamięci, przez co będą wskazywać na ten sam adres w pamięci. Zmienna str4 oraz str5 nie zostaną umieszczone w tej samej komórce mimo, że już podczas kompilacji można stwierdzić, że te stringi są sobie równe (niedoskonałość optymalizacji przez kompilator?). Prawdopodobnie dzieje się tak, że str4 zostało stworzone z połączenia dwóch różnych obiektów klasy String natomiast, zmiennej str5 “ręcznie” przydzieliliśmy pamięć przez wywołanie new String(“Hello World”);

Ciekaw jestem, czy w przyszłości zostanie to jakoś inaczej zaimplementowane.

PS. Z góry przepraszam, za wyrażenia typu “wskazuje”, “komórka pamięci” zamiast używania poprawnego słownictwa w javie. Tak jak mówię, z javą mam do czynienia 3 miesiące i nie wszystko jeszcze potrafię powiedzieć w “javowy sposób” :)

PS 2. Na uczelni często pojawiają się takie ciekawostki, które nawet w dobrych książkach jest trudno napotkać – po nie właśnie zdecydowałem się pójść na studia :) Będę się starał je wszystkie opisywać na blogu :)

PS 3. Oczywiście kolosa mam zaliczonego, bo reszta była ok poza jeszcze trzeba mniej ciekawymi błędami, których można by było się w sumie domyśleć, ale nie pamiętam już co to były za pytania :(

17 comments so far

Add Your Comment
  1. Widzie ze Twój wykładowca sprawdza Was w dość ciekawy sposób.
    Takie dość trywialne lecz podchwytliwe przykłady dostałeś na kolosie.
    Mam tylko jedną malutką uwagę nie najbezpieczniej jest używać do porównywania operatora ==.
    Nie na darmo w klasie String istnieje metoda equal, nieraz użycie == może skończyć się bardzo brzydkim i trudnym do znalezienia błędem.

    Jak kolejny kolos będzie tej klasy to polecam przestudiowanie książki do SCJP, tam też są podobne kruczki. :)

  2. Że jest niebezpieczne to wiem – nawet na stronach CERT-u to piszą w dziale “secure programming”.

    Rozwiń mi proszę tytuł tej książki (SCJP)

    PS. Ach i oczywiście wciąż są tajemnice z tym operatorem trójargumentowym -> czemu mamy tu automatyczną konwersję – nie ma przecież żadnych operacji na tych dwóch wartościach

  3. Sun Certified Java Programmer pierwszy z certyfikatów Sunowskich jaki należy zrobić.
    http://www.sun.com/training/certification/java/scjp.xml.
    Jeszcze dwa i pół miesiąca i sam będę musiał / mógł poszczycić się jego posiadaniem :D.
    Książki które warto przeczytać by poznać trochę kruczków z javy:
    SCJP Sun Certified Programmer for Java 5 Study Guide (Exam 310-055)
    Head First Java 2 nd Edition.

    Co do automatycznej konwersji typów to java będzie starała się dokonać rzutowania wszystkich typów w danym wyrażeniu jeżeli zachowane są zasady automatycznego rzutowania typów.
    Rzutować będzie oczywiście do najbardziej “pojemnego” typu. Nie wiem do końca czy ta zasada została zachowana tutaj, niestety nie znalazłem żadnego książkowego przykładu by go przytoczyć.

  4. Trochę zamieszania z tym operatorem, ale jego działanie jest rozsądne. Popatrz na ten problem w taki sposób:
    Wynikiem działania może być double albo int. Nie wiemy jaki dokładnie będzie typ ponieważ może to być określone w momencie uruchomienia programu (takie nie javowe to, ale istotne!). Kompilator musi jednak określić typ. Zakłada więc, że należy użyć typu ogólniejszego. Typem ogólniejszym, czyli takim którego użycie nie spowoduje utraty informacji jest tu double. Wykonane zostanie zatem rzutowanie całości do double.
    W przypadku Ruby typ jest określany “w locie” dzięki czemu nie ma potrzeby dokonywania rzutowania. Podobnie rzecz ma się z C++, które nie sprawdza typu w takim przypadku.

    Co do Stringa to polecam wzorzec Flyweight. Dużo wyjaśni

  5. Dzięki, poczytam :)

  6. (str4.equals(“HelloWorld”)) {
    nie będzie miał tej samej referencji
    }

    na chwile kompilacji nie ma jeszcze obiektu (nowy obiekt)
    String str5 = new String(“Hello World”);

    Tak mi się przynajmniej wydaje, też raczkuję.

  7. [...] źródło: blog.y3ti.pl Follow us on Twitter 26 śledzących RSS Feed 218 czytelników Jak oblać studenta z Javy? 1 głosuj! Jakiś czas temu na mojej uczelni mieliśmy kolokwium z przedmiotu “Podstawy [...]

  8. Nie podoba mi się przykład ze stringami. Musiałbym przejrzeć materiały i się upewnić, ale mam wrażenie że kompilator _może_ tak zrobić, ale nie _musi_. Czyli możnaby stworzyć kompilator Javy, który byłby zgodny ze standardem języka a powyższe zadanie nie wypisałoby ifa.

  9. Zadanie nr 1 pojawiło się jakiś czas temu na blogu Pawła Szulca jako “Java Killers”:
    http://paulszulc.wordpress.com/2010/01/13/java-killers-003-is-the-question/

    Ciekawy sposób egzaminowania ;)

  10. Również studiuję na PJWSTK. Gdyby kolega zdał SCJP doskonale by znał odpowiedź :) Takie pytania to standard. Cóż może nie są to podstawy programowania, ale 5 należą się najlepszym. Osoby, które mają wysokie mniemanie o swoich umiejętnościach mogą jedynie wylewać swoje żale na blogach, zamiast brać się do nauki :) no offence.

  11. lol: powiało ironią ;) A o takich rzeczach będę się “żalił” na blogu o ile będą tak ciekawe jak te, które pojawiły się w tym wpisie :)

  12. Ahh, cóż czas sesji jest czasem, w którym “trochę” bardziej reaguję frustracją :) ale jak napisałem no offence :)

  13. z serii ciekawostek mogę dodać np. :

    Integer i1 = 120;
    Integer i2 = 120;
    System.out.println(i1==i2); // zwróci true

    Integer i3 = 240;
    Integer i4 = 240;
    System.out.println(i3==i4); // zwróci false

    y3ti, widząc Twoje skłonności śledcze czekam na odpowiedź dlaczego :)

  14. Dokładnie :) no to y3ti kiedy zdajesz SCJP?

  15. Na co dzień nie programuje w Javie. Jave poznaje tylko na uczelni oraz czasem zdarza się naklikać coś małego w tym języku. Mimo wszystko interesuję się trochę tym tematem (szczególnie maszyną wirtualną) -> jruby, scala.

    Certyfikatów nie zamierzam robić ;)

  16. Po przeczytaniu tytułu się szyderczo uśmiechnąłem, ale z ciekawości zajrzałem i w sumie uważam, że to bardzo ciekawy artykuł :) porusza sprawy ukryte, niewidoczne gołym okiem.
    @koziołek: Piszesz, że to rozsądne, ale według mnie niekoniecznie logiczne (bo to programista powinien decydować o kodzie, a nie kompilator czy cokolwiek innego pośredniego) – zgadzam się co do rzutowania inta na double, ale nie do końca pojmuję dlaczego w takim razie w dwóch ostatnich przypadkach char nie został zrzutowany na Stringa, skoro klasa Integer została zrzutowana na klasę Double? Nie zastanawiam się czy jest to dobre, czy nie – po prostu nie rozumiem jaka jest zasada i cel takiego działania, bo skoro w przypadku char/String kompilator (lub jvm??) potrafi zwrócić char lub String, to dlaczego w przypadku int/double nie potrafi zwrócić dwóch różnych typów czyli int i double, tylko sobie ‘bezczelnie’ rzutuje?
    Chciałbym się dowiedzieć jak to niby działa i po co takie coś wdrożyli? Chyba tylko dla wygody początkujących, bo innego zastosowania to nie widzę. I w sumie muszę stwierdzić, że nie podoba mi się to, no ale może to dlatego, że nie jestem fanem Javy i być może jestem subiektywny. Być może nie podoba mi się to, bo też nie rozwiązałbym poprawnie tych zadań :D
    Pozdrawiam.
    PS: Ale gratuluję prowadzącego zajęcia, musi być pasjonatem :)