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;

8 maja 2008

SCJP – Literały

Literały (ang. literals) to pierwsze zagadnienie z trzeciego rozdziału streszczanej książki "SCJP Sun Certified Programmer for Java 5 Study Guide (Exam 310-055)", aczkolwiek dla opracowania dzisiejszego artykułu korzystałem ze specyfikacji języka – książka jest tutaj jak dla mnie daleko za mało precyzyjna. Więcej informacji w artykule "Przygotowania do SCJP czas zacząć".

Literał to wartość wpisana bezpośrednio w kodzie źródłowym programu. W języku Java wyróżniamy 6 różnych typów literałów, są to literały liczbowe całkowite i zmiennopozycyjne, literały logiczne, znakowe, napisowe i literał null.

Literały liczbowe całkowite (ang. integer literals) mogą być wyrażone jako liczby dziesiętne, szesnastkowe i ósemkowe. Literały szesnastkowe oznaczamy przedrostkiem 0x lub 0X, po czym następuje liczba szesnastkowa, tzn. dowolny ciąg cyfr od 0 do 9 i liter od a/A do f/F. Dopuszczalne jest dowolne mieszanie liter dużych i małych. Liczba może mieć także dowolną ilość zer początkowych, co naturalnie nie wpływa na jej wartość. Zer początkowych nie może mieć natomiast liczba dziesiętna, ponieważ umieszczenie zera na początku liczby oznacza, że jest to liczba ósemkowa. Liczbę ósemkową oznaczamy właśnie w ten sposób, że umieszczamy cyfrę 0 jako przedrostek. Zer tych może być dowolnie wiele. Naturalnie cyfry ósemkowe to cyfry z zakresu od 0 do 7. Przykładowe literały dziesiętne to: 123, 0, 9809238, literały ósemkowe to: 0123, 00, 0000077143 a szesnastkowe: 0xABC, 0X123, 0xAbCd, 0X1A2b, 0x0, 0x00001. Egzamin na SCJP co prawda nie wymaga od nas biegłości w posługiwaniu się innymi niż dziesiętny systemami liczbowymi, ale warto wiedzieć, że literały szesnastkowe i ósemkowe mogą reprezentować liczby ujemne, np. literał 0x80000000 reprezentuje wartość -2147483648, zatem wyrażenie -0x80000000 reprezentuje wartość dodatnią 2147483648.

Domyślnie literały całkowite są typu ‘int’. Literały typu ‘long’ otrzymujemy poprzez dodanie sufixu L lub l. Literały 123 czy 0xAbCd są więc typu ‘int’ a literały 123L czy 0xAbCdl typu ‘long’. Ze względu na podobieństwo małej litery l do cyfry 1 proponuje się używanie tylko dużej litery L.

Literały liczbowe zmiennopozycyjne mogą być wyrażone jako liczby dziesiętne i szesnastkowe. Ogólnie, literały liczbowe zmiennopozycyjne składają się kolejno z części całkowitej, kropki, części ułamkowej, symbolu wykładnika i wartości wykładnika oraz litery oznaczającej typ. Uf, dużo tego i brzmi nużąco mądrze, a do tego to jeszcze nie koniec, ale trzeba się jakoś przegryźć. Wszystkie literały zmiennopozycyjne są domyślnie typu ‘double’. Literały typu ‘float’ oznaczamy poprzez dodanie na końcu litery F lub f. Możemy także explicite oznaczyć, że liczba jest typu ‘double’ dodając literę D lub d. Litera oznaczająca typ to zatem jedna z liter: F, f, D, d.

Jeśli chodzi o dziesiętne literały zmiennopozycyjne, to wszystkie składowe są opcjonalne, przy czy musi być obecne, co najmniej jedno z dwojga: część całkowita albo ułamkowa, oraz jedno z trojga: kropka albo symbol i wartość wykładnika albo litera oznaczająca typ. Część całkowita i ułamkowa to dziesiętne liczby naturalne. Symbol wykładnika to litera E lub e. Wartość wykładnika to dziesiętna liczba całkowita, dodatnia lub ujemna. Przykładowe, poprawne dziesiętne literały zmiennopozycyjne to zatem: 0D, 1.F, 123., .3450, 23e1f, 1e1, 3.0E-1F.

Literały zmiennopozycyjne szesnastkowe obowiązują inne reguły. Obowiązkową częścią jest symbol i wartość wykładnika, przy czym symbol wykładnika to litera P lub p. Obowiązkowa jest też – analogicznie jak dla systemu dziesiętnego – część całkowita albo ułamkowa. Pozostałe części są opcjonalne. Część całkowita zapisana być musi jako liczba szesnastkowa, a więc z prefixem 0X lub 0x. Przykładowe, poprawne szesnastkowe literały zmiennopozycyjne to: 0x3p1, 0x2.3p0f, 0x.1p4d, 0xab.cdp0d. Nie wiedzieć czemu ktoś miałby chcieć zapisywać liczby w takich postaciach, ale język Java to umożliwia a egzaminatorzy na SCJP mogą się o tę wiedzę upomnieć.

Literały logiczne to jeden z dwojga literałów: true albo false. Literały te są typu ‘boolean’ i są to jedyne wartości, które może przyjmować zmienna tego typu. Inaczej niż np. w języku C++, literały 0 czy 1 nie mogą być użyte jako wartości logiczne. Ale temat konwersji typów to nieco inna historia, póki co odłożona na później.

Literały znakowe to pewna reprezentacja pojedynczego znaku ujęta w apostrofy. Znaki mogą być reprezentowane wprost – np. litera a, symbol %, czy cyfra 1 –, w postaci kodu UTF-16 poprzedzonego prefixem \u – czyli od \u0000 do \uffff (albo \uFFFF) – albo jako sekwencje specjalne, tj. \t, \n, \r, \\, \’. Od tego co napisałem obowiązują jeszcze dwa wyjątki – nie można znaków reprezentować poprzez sekwencje \u000a ani \u000d. Odpowiadają one kolejno sekwencjom \n i \r i są niedopuszczalne ze względu na konstrukcję procesu kompilacji. Szczęśliwie w szczegóły nie potrzebujemy wnikać. Przykładowe literały znakowe to: ‘a’, ‘H’, ‘*’, ‘\u1234’, ‘\n’. Literały znakowe są zawsze typu ‘char’.

Literały napisowe to ujęty w cudzysłowy dowolny ciąg znaków, reprezentowanych – z małym wyjątkiem – w taki sam sposób jak dla literałów znakowych. Tym małym wyjątkiem jest, że w literałach napisowych można explicite umieścić znak apostrofu ‘ a nie można umieścić znaku cudzysłowu ", tak więc nie musimy – choć możemy – używać sekwencji \’ a z kolei musimy pisać \" zamiast ".

6 maja 2008

SCJP – Metody statyczne i przysłanianie metod

W artykule "SCJP - Redefiniowanie metod" pisałem o nadpisywaniu (ang. overriding) metod, choć wtedy nazwałem to trochę nie fortunnie redefiniowaniem. Dziś będzie o przysłanianiu (ang. redefinition), co chciawszy przetłumaczyć wprost z angielskiego mogłoby być nazwane właśnie redefiniowaniem, ale pozostanę przy przysłanianiu – wydaje mi się, że jest to bardziej jednoznaczne określenie.

Metody i zmienne statyczne nie przynależą do obiektu, przynależą one do klasy, z tego też względu powinny być wywoływane względem klasy, ale język Java dopuszcza także składnię, gdzie metodę statyczną wywołujemy względem zmiennej referencyjnej, a więc tak jakby względem obiektu. Poeksperymentujmy trochę z tą koncepcją. Pierwszy przykład poniżej.

class SomeClass {
static void printMessage() {
System.out.println("my message");
}
}

public class Test {
public static void main(String[] args) {
SomeClass obj = null;

obj.printMessage();
}
}

Jaki będzie rezultat uruchomienia tego programu? NullPointerException? Otóż nie! Rezultatem będzie wypisanie napisu "my message". Dzieje się tak dlatego, że kompilator w miejsce zmiennej referencyjnej podstawia dla statycznych odwołań klasę tejże zmiennej. Instrukcja ‘obj.printMessage()’ z powyższego przykładu jest de facto ekwiwalentem dla ‘SomeClass.printMessage()’. Nic więc dziwnego, że program wykona się poprawnie. Podkreślę jeszcze to, co powiedziałem być może nie dość wyraźnie – podstawiany jest typ zmiennej referencyjnej, a nie typ obiektu na jaki ta zmienna ewentualnie wskazuje. Wydaje się to stwierdzenie być dosyć trywialnym po tym jak powyżej pokazałem przykład, gdzie obiektu w ogóle nie było – do zmiennej obj przypisałem null – ale równie trywialne błędy mogą pozbawić nas punktów na egzaminie do SCJP.

Metody statycznej nie można nadpisać (ang. override), ale można ją przesłonić (ang. redefine). Przesłanianie metody, to definiowanie w podklasie metody statycznej, która została już zdefiniowana w nadklasie. Jeśli chodzi o kryteria, jakie musi spełniać metoda by być metodą przysłaniającą to są one takie same jak dla metod nadpisujących i opisałem je w artykule wspomnianym na początku, naturalnie z tym wyjątkiem, że dotyczy to metod statycznych. Aby zrozumieć różnicę, między nadpisywaniem a przesłanianiem przeanalizujmy poniższy przykład.

class Parent {
static String getDescr() {
return "parent";
}
}

class Child extends Parent {
static String getDescr() {
return "child";
}
}

public class Test {
public static void main(String[] args) {
Parent obj = new Parent();
System.out.println(obj.getDescr());

obj = new Child();
System.out.println(obj.getDescr());
}
}

Rezultatem tego programu będzie dwukrotne wypisanie napisu "parent". Tak jak pisałem powyżej, dla odwołań statycznych w miejsce zmiennej referencyjnej podstawiana jest klasa tej zmiennej, a więc w obu przypadkach Parent. Instrukcja ‘obj.getDescr()’ jest więc równoważna instrukcji ‘Parent.getDescr()’ niezależnie od tego, na jaki obiekt wskazuje zmienna ‘obj’. I to jest właśnie przysłanianie. Usuńmy natomiast słówka static z deklaracji metod getDescr(). Tym razem program wypisze "parent" a potem "child". I to jest właśnie nadpisywanie.

5 maja 2008

SCJP - Konstruktory i inicjalizacja

Kilka słów o konstruktorach było już w artykule "SCJP - Deklaracja konstruktora" a teraz nadeszła pora na więcej. Będzie mniej o deklaracji a więcej o implementacji.

Poprzedni, wspomniany wyżej artykuł zacząłem od stwierdzenia, że każda klasa ma konstruktor. Podkreślam to jeszcze raz – każda, w tym także abstrakcyjna! Konstruktory mają także typy wyliczeniowe, ale interfejsy nie. Po co konstruktor w klasie abstrakcyjnej, której instancji przecież nie można skonstruować? Wszystko wyjaśni się już za chwilę.

Konstruktory uruchamiane są, gdy tworzymy nowe instancje/obiekty a więc najczęściej w wyniku użycia operatora ‘new’ (wyjątkiem są stałe wyliczeniowe). Jednak, aby utworzyć instancję danej klasy nie wystarczy wywołać konstruktora z tej klasy, trzeba wywołać także konstruktory z wszystkich nadklas, do klasy Object włącznie. Poniższy przykład pomoże nam zrozumieć, dlaczego.

abstract class Parent {
private String str;

Parent() {
this.str = "some data";
}

public String getStr() {
return str;
}
}

class Child extends Parent { }

public class Test {
public static void main(String[] args) {
Child child = new Child();

System.out.println(child.getStr());
}
}

Cóż robi ten program? Oczywiście wypisuje napis "some data". Dzieje się tak tylko dlatego, że oprócz wygenerowanego przez kompilator, domyślnego konstruktora z klasy Child wywołany został także konstruktor z klasy Parent. W celu zapewnienia poprawnej inicjalizacji obiektów obowiązuje prosta zasada – pierwszą instrukcją każdego konstruktora musi być wywołanie super(…) albo this(…) (z dowolną liczbą parametrów), przy czym jeśli nie umieściliśmy jednego z tych wywołań kompilator automatycznie doda wywołanie super() (bez parametrów). Instrukcja super(…) to wywołanie konstruktora z nadklasy a this(…) to wywołanie innego, przeciążonego konstruktora z klasy bieżącej, aczkolwiek któryś z kolei konstruktor będzie musiał w końcu wywołać konstruktor z nadklasy. Ilustruje to poniższy przykład.

abstract class Parent {
private String str;

Parent(String str) {
this.str = str;
}

public String getStr() {
return str;
}
}

class Child extends Parent {
Child() {
this("some text");
}

Child(String str) {
super(str);
}
}

public class Test {
public static void main(String[] args) {
Child child = new Child();
System.out.println(child.getStr());

child = new Child("another text");
System.out.println(child.getStr());
}
}

Efektem działania tego programu będą napisy kolejno "some text" i "another text". Podkreślę jeszcze, że konstruktora nie można wywołać tak jak metody, posługując się jego nazwą, zawsze posługujemy się konstrukcją super(…) lub this(…). Konstruktora nie można także wywołać explicite inaczej jak tylko z innego konstruktora, tak więc instrukcje super(…) i this(…) nie są dozwolone w metodach. Wiem, że wydaje się to być oczywiste, ale warto sobie pewne rzeczy uświadomić wprost, pod kątem SCJP. To, że instrukcje super(..) i this(…) muszą być pierwszymi instrukcjami w konstruktorze oznacza również, że w każdym konstruktorze możemy użyć tylko jednej z nich i tylko co najwyżej raz. W przeciwnym wypadku któraś z nich nie byłaby pierwsza. Skądinąd, wielokrotne występowanie tych instrukcji nie miałoby sensu.

Wywołanie przeciążonego konstruktora z klasy bieżącej, albo konstruktora z nadklasy musi być bezwzględnie pierwszą instrukcją i dopóki instrukcja ta nie zostanie wykonana obiekt nie jest jeszcze skonstruowany. Z tego względu, dopóki nie zostanie wykonana instrukcja super(…) nie jest możliwe wykonywanie operacji na właśnie konstruowanej instancji. Mówiąc wprost, dopóki nie zostanie wykonana (nie wywołana, wykonana!) instrukcja super(…) nie można wywoływać nie statycznych metod ani używać nie statycznych zmiennych. Parametrami tych wywołań mogą być natomiast zmienne statyczne i wartości zwracane z wywołań statycznych metod. Poniżej przykład, który przy okazji pokazuje, jak wykonać pewne operacje przed wywołaniem super-konstruktora, aczkolwiek na SCJP przydatne będą raczej inne triki.

class Child extends Parent {
Child() {
super(doSomethingBefore());
}

static String doSomethingBefore() {
System.out.println("before call to super()");

return "some text";
}
}

I jeszcze słówko, co do domyślnego konstruktora generowanego przez kompilator w przypadku, gdy programista nie zapewnił żadnego. Oprócz tego, że jest to konstruktor bez argumentowy i zawierający jedynie bezargumentowe wywołanie super() trzeba wiedzieć, że ma on modyfikator dostępu taki jak jego klasa. I ostatni przykład.

class Parent {
String str = "some value";
}

class Child extends Parent {
String childStr = "some other value";
}

Jak można się domyślić chodzi o inicjalizację zmiennych instancyjnych. Instrukcje przypisania wartości tym zmiennym też muszą się przecież kiedyś i gdzieś wykonać. I jak najbardziej, wykonują się one jako część konstruktora. Aby zrozumieć, kiedy to się dzieje prześledźmy kolejne operacje wykonywane w efekcie utworzenia nowej instancji klasy Child, tj. wywołania ‘new Child()’. Oto one:

- wywoływany jest domyślny konstruktor klasy Child, jego pierwszą instrukcją jest wywołanie super()

- wywoływany jest domyślny konstruktor klasy Parent, jego pierwszą instrukcją jest wywołanie super()

- wywoływany jest konstruktor w klasie Object, ta nie ma już nadklas, więc po wykonaniu kodu konstruktora następuje powrót do konstruktora z klasy Parent

- wykonywana jest inicjalizacja zmiennych instancyjnych w klasie Parent, a więc przypisanie ‘str = "some value"’ po czym następuje powrót do konstruktora z klasy Child

- wykonywana jest inicjalizacja zmiennych instancyjnych w klasie Child, a więc przypisanie ‘childStr = "some other value"’ co jest ostatnim krokiem, obiekt został utworzony i zainicjalizowany