9 maja 2008

SCJP - Operatory przypisania dla typów prymitywnych

W artykule "SCJP – Literały" pisałem o literałach i ich typach. Dziś będzie o typach wyrażeń arytmetycznych, konwersji typów prymitywnych i operatorach przypisania. Literały odgrywają tu dużą rolę, więc proponuję lekturę wspomnianego artykułu jako wstęp do dzisiejszego. No chyba, że tamte zagadnienia są skądinąd znane.

Literały całkowite domyślnie są typu int, jednak w języku Java mamy cztery typy całkowitoliczbowe: byte, short, int i long. Jeśli na końcu literału całkowitego dodamy literę L lub l, to będzie on miał typ long, ale nie ma sposobu by literał miał typ byte lub short. Jak więc przypisać wartość do zmiennej jednego z tych typów? Że się tak wyrażę - normalnie, wykorzystując konwersję domyślną.

byte b = -128; // poprawne dla wartości od -128 do 127
short s = 32767; // poprawne dla wartości od -32768 do 32767

Dlaczego o tym piszę, skoro to takie mało odkrywcze? Rozważmy analogiczną sytuację dla typów zmiennopozycyjnych: float i double. Literały zmiennopozycyjne są domyślnie typu double, ale możemy zapisać literał typu float dodając sufix F lub f. Trochę dziwna niekonsekwencja - nie możemy wymusić, by dany literał był typu byte czy short, ale typu float już tak. Literały zmiennopozycyjne są domyślnie typu double, a mimo to możemy explicite powiedzieć, że literał jest typu double dodając sufix D lub d. Nie możemy tego zrobić dla typu int, który jest domyślnym typem literałów całkowitych. Ale to nie koniec niekonsekwencji, mam nadzieję, że jednak jakoś uzasadnionej. Spójrzmy na poniższy kod.

float f = 1.1; // błąd!

Byłoby jeszcze nieźle, gdyby nie to, że powyższy kod się nie skompiluje. Literał 1.1 jest typu double, i kompilator zgłasza, że nie może wykonać implicite konwersji (potencjalnie) stratnej. Niby w porządku, ale czemu nie protestuje na tej samej zasadzie w przypadku typów całkowitych? Wie ktoś może, jaka była motywacja twórców języka? W każdym razie, powyższą niepoprawną instrukcję przypisania możemy naprawić na jeden z pokazanych poniżej sposobów.

float f = 1.1F; // może też być małe f zamiast F
float ff = (float) 1.1;

Ale i typami całkowitoliczbowymi nie możemy operować beztrosko. Przejdźmy do typów wyrażeń arytmetycznych. Zerknijmy na poniższy przykład.

byte a = 1;
byte b = a + 1; // błąd!

byte c = 1 + 1;
byte d = 64 + 64; // błąd!

byte e = 1 + 1L; // błąd!

Zacznijmy od tego, że jeśli wyrażenie arytmetyczne używa tylko operandów całkowitych to typ wyrażenia jest również całkowity i jest to zawsze int albo long. Typem takiego wyrażenia jest int jeśli żaden z operandów nie jest typu long. Jeśli choć jeden z operandów jest typu long to całe wyrażenie jest też typu long. Wyrażenie ‘1 + 1’ jest zatem typu int i kompilator godzi się wykonać konwersję automatyczną do typu byte, ale wyrażenie ‘1 + 1L’ jest już typu long, dla którego konwersja implicite nie jest przewidziana. Wartość wyrażenia, w którym nie występują zmienne jest wyliczana w czasie kompilacji. Wartością wyrażenia ’64 + 64’ jest 128 co jest poza zakresem wartości typu byte i stąd błąd w powyższym przykładzie. Ale czemu wartość wyrażenia ‘1 + 1’ można przypisać do zmiennej typu byte, a wartości wyrażenia ‘a + 1’ już nie? Oba te wyrażenia są typu int, ale w tym drugim występuje zmienna, a więc w czasie kompilacji nie wiadomo, jaka jest jego wartość. Skoro nie wiadomo jaka jest wartość to nie wiadomo czy jest ona z zakresu wartości typu byte i stąd błąd. Aby kod ten się skompilował należy zastosować explicite operację rzutowania. Z typami short oraz char – który można traktować także jak typ liczbowy – sytuacja jest analogiczna.

Jeśli wyrażenie arytmetyczne zawiera choćby jeden operand typu float a nie zawiera operandów typu double to typem takiego wyrażenia jest też float. Jeśli wyrażenie arytmetyczne zawiera choćby jeden operand typu double to typem wyrażenia jest double. Analogiczne przypisanie jak pokazane w ostatnim przykładzie dla typu byte dla typu float jest poprawne. Poniższy kod kompiluje się.

float a = 1.1f;
float b = a + 1.2f;

Naturalnie wszystkie te problemy związane z konwersją wartości i typów liczbowych spowodowane są troską o poprawność kodu. Co się dzieje, jeśli zmusimy kompilator do wykonania konwersji poprzez rzutowanie, a wartość jest zbyt duża by móc być przypisana do zmiennej danego typu? Nastąpi konwersja stratna, czyli w przypadku liczby, zostaną odrzucone jej początkowe bity. Zerknijmy na poniższy program.

public class Test {
public static void main(String[] args) {
byte x = (byte)128;

int i = 1024;
byte y = (byte)i;

System.out.println("x = " + x + " y = " + y);
}
}

Rzutowanie jest określone explicite, więc kompilator nie protestuje, program się kompiluje. Ale jaki jest efekt jego uruchomienia? Otrzymujemy napis "x = -128 y = 0", a więc wartości naszych zmiennych są "nieco dziwne". Nie ma cudów i trzeba o tym pamiętać. Jeśli wartość jest zbyt duża by zmieścić się w danym typie, to się tam po prostu nie zmieści.

Język Java oferuje także złożone operatory przypisania, czyli; +=, -=, *= oraz /=. Przy inicjalizacji zmiennych nie mają zastosowania, ale przy późniejszych operacjach już tak. Poniższe fragmenty kodu są sobie równoważne, ale przewaga drugiego sposobu zapisu jest oczywista.

b = (byte) (b + 2); // rzutowanie jest konieczne!

b += 2;

6 komentarzy:

Kamil Wójcik pisze...

Witam!
Od jakiegoś czasu śledzę wpisy odnośnie przygotowań do SCJP. Sam przygotowuje się do egzaminu i Twoje posty są bardzo pomocne. Oby tak dalej. Pozdrawiam!

Anonimowy pisze...

Witam. Ja tez przygotowywuję się do tego egzaminu. Podstawą jest dla mnie książka sierry, ale na ten blog też często zaglądam, żeby przypomnieć sobie parę rzeczy, dowiedzieć czegoś nowego itp.. Teksty są naprawdę pomocne.Mam jedną prośbę/uwagę do osób przeglądających posty. Jeśli znacie jakieś "ciekawe przypadki" związane z danym postem, lub chcecie go uzupełnić to piszcie komentarze.

Anonimowy pisze...

Od razu umieszcze przykład, który mnie zaskoczył:
for(byte b=0;b<150;b++){\\jakies instrukcje}

Przypominam, ze maksymalna wartosc byte 127. Myslalem ze program nie skompiluje sie, ale dziala i daje nieskonczoną pętle!!! 128 jest rzutowane na -128 i mamy nieskończony przebieg!!!

Mariusz Lipiński pisze...

"Mam jedną prośbę/uwagę do osób przeglądających posty. Jeśli znacie jakieś "ciekawe przypadki" związane z danym postem, lub chcecie go uzupełnić to piszcie komentarze"

...baaaaardzo gorąco do tego zachęcam! O to mi właśnie chodzi, żeby czytelnicy dodając swoje komentarze uzupełniali mój wywód.

Dzięki "Anonimowy" :) za ciekawy przykład.

Anonimowy pisze...

Aha! To ja dodam kolejny raz swoje 3 grosze. Tak jak powiedziałeś

byte x = 10;
x += 20;

można traktować jako:

x = (byte)(x + 20);

tak samo jest z ++ i --

dla x++ mamy x += 1 czyli x = (byte)(x + 1)

wniosek dla += itp. oraz ++ i -- rzutowanie wykonywanie jast niejawnie. Co więc z operatorami unarnymi? Tu jest problem!

byte a = 10;
byte b = a; // OK
byte c = -a; // ZLE
byte d = +a; // o nie! tez ZLE

monsieur.lame

Anonimowy pisze...

Aha! Jeszcze jedna ciekawostka! Jesli metoda/konstruktor przyjmuje argument typu byte lub short, to gdy wywolamy ja z literalem int jako argumentem to kompilator nie jest w stanie sam zrobic konwersje mimo, ze potrafi oszacowac czy wartosci miesci sie w zakresie!

mozna

byte b = 10; // 10 miesci sie w byte

ale dla metody

void foo(byte x) {

}

wywolanie foo(10) sie nie skompiluje!