30 maja 2008

JAVArsovia w Polskim Radiu Euro

Parę dni temu kapituła organizacyjna konferencji JAVArsovia 2008 otrzymała zaproszenie do Polskiego Radia Euro (od 26 maja 2008 tak właśnie nazywa się stare Polskie Radio BIS). Pisał o tym Jacek w artykule "3 dni do JAVArsovii 2008, audycja w Polskim Radiu". I byliśmy. Nadawaliśmy przez blisko godzinę, na żywo(!) od 9 do 10 dziś rano, naturalnie z przerwami na muzyczkę. Zaszczyt reprezentowania kapituły przypadł Jackowi Laskowskiemu, Michałowi Margielowi no i Mi. Jak było? Bardzo ciekawie! :) A oto my przed mikrofonami.

28 maja 2008

Nowości w MavenIDE dla NetBeans

Pamiętam, że ostatnio jak otworzyłem NetBeans'a (wersja 6.0) zainstalowała mi się aktualizacja MavenIDE (wersja 3.1.1), ale dopiero dziś zobaczyłem, że zmiany są istotne i ciekawe. A to co mnie urzekło, to dostępna lista archetypów. Nie dość, że lista "wbudowana" w plugin jest teraz większa, to jeszcze mamy do dyspozycji archetypy wprost z repozytoriów Mavena. Spójrzcie tylko na poniższy obrazek.

27 maja 2008

SCJP – Obiekty typu tablicowego

Dziś będzie parę słów wstępu o tablicach – deklaracje i podstawy konstrukcji; inicjalizacja w następnym artykule. Zanim opuścisz ten rozdział upewnij się, że wiesz na temat tablic w Javie wszystko; nie jest to bowiem temat aż tak banalny, jakim mógłby się wydawać.

Tablice w języku Java są obiektami przechowującymi listę zmiennych pewnego ustalonego typu. Deklarując tablicę podajemy właśnie typ zmiennych, które będzie ona przechowywała oraz wymiar (ang. dimension) – ale nie rozmiar(!) – tablicy. Więcej o deklarowaniu tablic w artykule "SCJP - Deklaracja zmiennych i stałych". Deklaracja tablicy, czyli mówiąc precyzyjniej deklaracja referencji na obiekt typu tablicowego to jednak tylko deklaracja referencji – nie powoduje utworzenia żadnego obiektu. Deklarując referencję typu tablicowego nie podajemy rozmiaru tablicy, bo jest zbędny. Jest on natomiast wymagany, gdy tworzymy właściwy obiekt tablicy; musi być bowiem wiadomym, jak duży obszar pamięci trzeba zaalokować. Poniżej dwa przykłady pokazujące deklaracje i utworzenie obiektów tablic.

int[] nums = new int[16];

Thread[] threads = new Thread[4];

Jak pisałem w artykule dotyczącym inicjalizacji zmiennych "SCJP - Wartości domyślne zmiennych" elementy tablic są zawsze inicjowane domyślnymi wartościami. Zauważmy jednak, że wartością domyślną dla zmiennej referencyjnej jest ‘null’, tak więc druga z pokazanych deklaracji tablic bynajmniej nie spowoduje utworzenia czterech instancji obiektów klasy ‘Thread’. Elementy tej tablicy to referencje a nie obiekty i będą one miały wartość ‘null’.

Tablice wielowymiarowe to de facto tablice tablic. Zdaje się, że stwierdzenie to jest banałem i zupełnie nic nie wnosi, ale jest wprost przeciwnie. Tablica dwu-wymiarowa dla przykładu nie musi być bowiem tablicą "prostokątną" a trójwymiarowa "kostką”. Wszystko powinien wyjaśnić poniższy przykład.

public class Test {
public static void main(String[] args) {
int[][] myTable = new int[4][];

myTable[0] = new int[2];
myTable[1] = new int[4];
myTable[2] = new int[6];
myTable[3] = new int[8];
}
}

Powyższy kod jest jak najbardziej poprawny – kompiluje się i wykonuje, choć nic ciekawego nie robi. Zacznijmy analizę od pierwszej linii. Deklarujemy w niej referencję na obiekt tablicy. Jak już powiedzieliśmy, tablica przechowuje zmienne pewnego ustalonego typu – w naszym przypadku jest to typ tablicowy, przechowujący z kolei zmienne typu ‘int’. Tworzymy też obiekt tablicy i mówimy, że obiekt ten ma zawierać cztery referencje do obiektów typu ‘int[]’. Referencje te póki co mają wartość ‘null’; nie określiliśmy bowiem "drugiego wymiaru" tablicy i kompilator nie wiedział jakie obiekty utworzyć. Obiekty te tworzymy sami w kolejnych liniach programu. Naturalnie zadanie to możemy powierzyć kompilatorowi – wystarczy, że określimy "drugi wymiar", tak jak pokazuje poniższy przykład.

public class Test {
public static void main(String[] args) {
int[][] myTable = new int[4][2];

myTable[1] = new int[4];

for(int[] subTable : myTable) {
for(int i : subTable)
System.out.print(i + " ");

System.out.println();
}
}
}

Żeby przykład nie był zbyt banalny oraz żeby pokazać, że w istocie tablica ‘myTable’ zawiera nic więcej jak tylko cztery referencje na obiekty typu ‘int[]’ w drugiej linii kodu przypisuje do jednej z tych referencji nowy obiekt – tablicę zawierającą cztery zmienne typu ‘int’, w miejsce tablicy zawierającej dwie zmienne. Efektem uruchomienia powyższego programu będzie pokazana poniżej mozaika zer – domyślnych wartości zmiennych typu ‘int’.

0 0
0 0 0 0
0 0
0 0

Na koniec pytanie. Wiemy, że tablice w języku Java indeksowane są począwszy od zera, ale co się stanie, gdy odwołamy się w kodzie (explicite) do elementu tablicy o indeksie ujemnym? Czy wszystko będzie w porządku, o ile modulo z tej wartości jest poprawnym indeksem? A może zostanie zgłoszony wyjątek w czasie wykonania? A może kompilator wie, że niezależnie od tego, jaka tablica by nie była duża, to i tak indeks ujemny jest niepoprawny a więc zgłosi wyjątek już w trakcie kompilacji?

26 maja 2008

SCJP - Parametry metod

W ostatnim artykule z serii SCJP, tj. "SCJP – Zmienne typów prostych i referencyjnych" napisałem parę słów wstępu do tego, co będzie dzisiaj. Temat jest względnie prosty, więc nie rekomenduję usilnie uprzedniej lektury owego wstępu, ale jeśli coś z artykułu dzisiejszego okaże się być niejasne to pewnie tamten tekst pomoże. Dziś będzie o przekazywaniu parametrów do wywołań metod a zwłaszcza parametrów typów prostych kontra referencji.

Wywołując metodę przekazujemy pewną listę – potencjalnie pustą – parametrów typu prostego i/lub referencji do obiektów. Pierwszą i w zasadzie jedyną rzeczą, jaką w tym kontekście trzeba wiedzieć jest, że w języku Java wszystkie parametry są przekazywane przez wartość – reszta to już czysta konsekwencja tego prostego faktu. Przeanalizujmy to na przykładach.

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

triple(x);

System.out.println("x: " + x);
}

static void triple(int x) {
x *= 3;
}
}

Efektem uruchomienia powyższego programu będzie wyświetlenie napisu "x: 2". Przeanalizujmy krok po kroku jak to się dzieje. W pierwszej linii metody ‘main(…)’ deklarujemy zmienną ‘x’ i przypisujemy jej wartość literału ‘2’. Następnie wywołujemy metodę ‘triple(…)’ przekazując jako argument zmienną ‘x’. Co to dokładnie znaczy przekazać zmienną ‘x’ jako argument? Zerknijmy na funkcję ‘triple(…)’. Widzimy, że deklaruje ona argument typu ‘int’ o nazwie ‘x’ – naturalnie zbieżność nazwy ze zmienną zadeklarowaną w metodzie ‘main(…)’ nie ma żadnego znaczenia. Zmienna zadeklarowana jako argument metody traktowana jest dokładnie tak samo jak zmienne zadeklarowane w jej ciele – jest to zmienna lokalna metody. Mamy zatem zmienną typu ‘int’ o nazwie ‘x’ w metodzie ‘main(…)’ oraz w metodzie ‘triple(…)’ i mimo, że nazywają się one tak samo to są to zupełnie inne zmienne, tj. odpowiadają innym obszarom pamięci. Jak już sobie powiedzieliśmy, zmienne w języku Java zawsze przekazywane są przez wartość, zatem wywołanie metody ‘triple(…)’ powoduje skopiowanie wartości zmiennej ‘x’ z metody ‘main(…)’ do zmiennej ‘x’ w metodzie ‘triple(…)’. I właśnie to skopiowanie jest przekazaniem parametru wywołania! Jaki zatem efekt ma wykonanie przypisania "x *= 3" w metodzie ‘triple(…)’? Naturalnie, potrojenie wartości zmiennej ‘x’ zadeklarowanej w metodzie ‘triple(…)’, ale nie zmiennej ‘x’ z metody ‘main(…)’. Nic więc dziwnego, że program wypisze "x: 2" – wypisujemy przecież wartość zmiennej lokalnej dla metody ‘main(…)’ a ta nie zmieniła się od czasu inicjacji liczbą 2. W powyższym przykładzie celowo nazwałem obie zmienne tak samo, w końcu moją intencją było wprowadzenie w błąd, zupełnie tak samo jak to ma miejsce na egzaminie SCJP, na co trzeba się wyczulić. Zerknijmy na kolejny przykład.

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

triple(obj);

System.out.println("x: " + obj.getX());
}

static void triple(SomeClass obj) {
obj.setX(obj.getX() * 3);
}
}

class SomeClass {
int x = 2;

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}
}

Tym razem przekazujemy referencję do obiektu, a dokładniej, kopię tej referencji – jest to bardzo duża różnica, co pokażę w kolejnym przykładzie. Jak pisałem w artykule wspomnianym we wstępie – referencje to nic magicznego, zwykła zmienna przechowująca pewną wartość, tyle że ta wartość to "wskaźnik" na obiekt. Przeanalizujmy, co się dzieje w przedstawionym powyżej programie. W pierwszej linii metody ‘main(…)’ deklarujemy zmienną referencyjną oraz tworzymy nowy obiekt. Zmienna ta inicjowana jest wartością reprezentującą wskaźnik na ten obiekt. Wywołując metodę ‘triple(…)’ nie przekazujemy bynajmniej obiektu, tylko referencję. Przekazanie referencji oznacza skopiowanie wartości zmiennej ‘obj’ zadeklarowanej w metodzie ‘main(…)’ do zmiennej o tej samej nazwie zadeklarowanej w metodzie ‘triple(…)’. Obie zmienne nazywają się tak samo i mają tą samą wartość, jednak są to zupełnie inne zmienne, których wartości możemy zmieniać niezależnie, analogicznie jak wartości zmiennych typów prostych. Ale co oznacza instrukcja "obj.setX(…)" umieszczona w metodzie ‘triple(…)’? Nie jest to bynajmniej przypisanie nowej wartości zmiennej ‘obj’. Jest to wywołanie metody obiektu "wskazywanego" przez tą zmienną. Ale chwileczkę, przecież zmienna ‘obj’ lokalna dla metody ‘triple(…)’ ma tą samą wartość, co zmienna ‘obj’ lokalna dla metody ‘main(…)’, zatem "wskazuje" dokładnie ten sam obiekt. Nic więc dziwnego, że uruchomienie programu spowoduje wyświetlenie napisu "x: 6". Zmodyfikowaliśmy przecież dokładnie ten obiekt, który utworzyliśmy w metodzie ‘main(…)’ i którego stan następnie wyświetlamy. I jeszcze jeden przykład. Klasa SomeClass pozostała bez zmian, więc nie pokazuję jej ponownie.

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

triple(obj);

System.out.println("x: " + obj.getX());
}

static void triple(SomeClass obj) {
SomeClass localObj = new SomeClass();

localObj.setX(obj.getX() * 3);

obj = localObj;
}
}

Nadal przekazujemy referencję do obiektu, metoda ‘main(…)’ wcale się nie zmieniła, a mimo to program wypisuje "x: 2". Dlaczego? Właśnie dlatego, że referencje zupełnie tak jak zmienne typów prostych przekazywane są przez wartość, tj. przekazywane są kopie referencji, zatem przypisanie "obj = localObj" modyfikuje zmienną lokalną dla metody ‘triple(…)’ i nie ma żadnego wpływu na zmienną ‘obj’ z metody ‘main(…)’.

23 maja 2008

SCJP – Zmienne typów prostych i referencyjnych

Miało być dziś o czymś innym a to, co powstało miało być tylko wstępem do planowanego artykułu, ale treść zaczęła rosnąć i rosnąć i tak oto uzyskała własną tożsamość. Będzie dziś zatem o zmiennych. Właściwie to o czymś, czemu trudno wymyślić zwięzły tytuł. W każdym razie po przeczytaniu całości powinno być jasne, o czym było.

Zmienne w języku Java mogą być zasadniczo dwojakiego rodzaju – typu prostego albo referencyjnego. Zmienne typu prostego to pewna ilość bajtów pamięci – np. 4 bajty dla typu ‘int’ – przechowujących explicite wartość zmiennej. Zmienne typu referencyjnego to również pewna ustalona ilość bajtów pamięci, ale zapisana tam wartość nie jest bezpośrednio dla nas użyteczna. Możemy o tej wartości myśleć jak o wskaźniku na obiekt, ale bynajmniej nie musi to być adres obszaru pamięci, w którym przechowywany jest obiekt. To, jaka dokładnie wartość jest przechowywana w zmiennej referencyjnej zależy od implementacji JVM (akr. Java Virtual Machine) a my potrzebujemy wiedzieć tylko tyle, że wartość ta jednoznacznie reprezentuje pewien obiekt, albo referencję pustą, czyli ‘null’. Zerknijmy na poniższy przykład.

class SomeClass {
private String descr = "default value";

public String getDescr() {
return descr;
}

public void setDescr(String descr) {
this.descr = descr;
}
}

public class Test {
public static void main(String[] args) {
testPrimitives();
testReferences();
}

static void testPrimitives() {
int x = 1;
int y = x;

x = 2;

System.out.println("y: " + y);
}

static void testReferences() {
SomeClass a = new SomeClass();
SomeClass b = a;

a.setDescr("new value");

System.out.println("b descr: " + b.getDescr());
}
}

Przypatrzmy się metodzie ‘testPrimitives()’. Deklarujemy zmienną ‘x’ typu prostego ‘int’ i przypisujemy jej wartość literału ‘1’. Powoduje to alokację 4 bajtów pamięci i zapisanie w tym obszarze bitowej reprezentacji liczby 1. Następnie deklarujemy zmienną ‘y’ i przypisujemy jej wartość zmiennej ‘x’ – wartość zmiennej ‘x’, a nie "zmienną x", cokolwiek by to mogło znaczyć. Mamy zatem kolejne 4 bajty pamięci i zapisaną w nich bitową reprezentację liczby 1, bo taka była wartość zmiennej ‘x’. Następnie do zmiennej ‘x’ przypisujemy wartość literału ‘2’. Powoduje to zapisanie w obszarze pamięci, który reprezentuje zmienną ‘x’ reprezentacji bitowej liczby 2. Co dzieje się w tym czasie ze zmienną ‘y’ i jej wartością? Nic, a cóż by się miało dziać. Na końcu wpisujemy wartość zmiennej ‘y’ i widzimy, że rzeczywiście nic się z nią nie stało – program wyświetla napis "y: 1".

A teraz zerknijmy na metodę ‘testReferences()’ – jest ona skonstruowana analogicznie do metody poprzedniej, ale operuje na zmiennych referencyjnych i obiektach. W pierwszej linii deklarujemy zmienną referencyjną ‘a’, co powoduje alokację przestrzeni pamięci potrzebną do reprezentacji "wskaźnika" do obiektu. Tworzymy także nowy obiekt, co pociąga za sobą w skutku wiele operacji, między innymi alokację pamięci niezbędnej do reprezentowania stanu obiektu oraz inicjalizację tego stanu, tj. uruchomienie konstruktorów. W końcu "wskaźnik" obiektu jest zapisywany jako wartość zmiennej referencyjnej. W drugiej linii kodu deklarujemy kolejną zmienną. Jako wartość zmiennej ‘b’ przypisywana jest wartość zmiennej ‘a’ a więc "wskaźnik" na obiekt utworzony uprzednio. Następnie, używając referencji ‘a’ wywołujemy metodę setDescr(…) naszego obiektu, tego samego, na który wskazuje zmienna ‘b’. Nic zatem dziwnego, że program wypisze "b descr: new value".

20 maja 2008

SCJP - Wartości domyślne zmiennych

Minęło parę dni od czasu ostatniego artykułu z serii SCJP, a to za sprawą alokowania mojego wolnego czasu na sprawy organizacyjne związane z konferencją JAVArsovia 2008, ale pora powrócić do pracy – więc oto jestem. Kontynuuję, co zapowiedziałem w artykule "Przygotowania do SCJP czas zacząć". Będzie o wartościach domyślnych nadawanych zmiennym nie zainicjowanym explicite.

Co się dzieje, kiedy deklarujemy zmienną? Czy musimy przypisać jej jakąś wartość? Czy jeśli tego nie zrobimy, zmienna będzie zainicjowana jakąś wartością domyślną? To zależy, zaraz dowiemy się, od czego. Prosta zasada brzmi – zmienne zadeklarowane w klasie są inicjowane wartością domyślną zawsze a zmienne lokalne nigdy. Dodatkowo, elementy tablic (ang. array) są zawsze inicjowane wartością domyślną, niezależnie od tego czy sama tablica została zadeklarowana lokalnie czy w klasie. Zerknijmy na poniższy przykład.

public class Test {
static int x;

public static void main(String[] args) {
System.out.println(x);
}
}

Zmienna ‘x’ została zadeklarowana w klasie i nie została zainicjowana explicite, wobec tego zostanie jej przypisana wartość domyślna. To, że zmienna ta jest statyczna nie ma znaczenia – istotne jest, że nie jest to zmienna lokalna, tj. zadeklarowana w metodzie lub konstruktorze. Wartość domyślna jest różna dla różnych typów. Dla typów całkowitoliczbowych, tj. ‘byte’, ‘short’, ‘int’ i ‘long’ wartością domyślną jest liczba 0. Dla typów liczbowych zmiennopozycyjnych, tj. ‘float’ i ‘double’ wartością domyślną jest liczba 0.0. Dla typu prostego ‘boolean’ wartością domyślną jest ‘false’. Dla typu znakowego ‘char’ jest to wartość ‘\u0000’. Zmienne referencyjne są domyślnie inicjowane wartością ‘null’. Powyższy program skutkuje więc wypisaniem liczby 0. Zerknijmy dla kontrastu na kolejny przykład.

public static void main(String[] args) {
int x;

System.out.println(x); // błąd!
}

Nie tylko kod ten się poprawnie nie wykona, ale nawet się nie skompiluje. Próba użycia niezainicjowanej zmiennej lokalnej jest błędem czasu kompilacji. Co ważne, błąd wynika z próby użycia zmiennej nie zainicjalizowanej, a nie z faktu deklaracji zmiennej bez przypisania wartości. Wartości można przecież przypisać później, aczkolwiek przed pierwszym użyciem. I kolejny przykład.

public static void main(String[] args) {
int x;

if(args.length > 0)
x = 1;

System.out.println(x); // błąd!
}

Co prawda w powyższym kodzie występuje instrukcja inicjalizacji zmiennej ‘x’, ale jest ona wykonywana warunkowo i kompilator nie jest w stanie stwierdzić, czy rzeczywiście zostanie ona zawsze wykonana, a więc kod również się nie skompiluje. Gdyby wartość dozoru instrukcji warunkowej dała się wyliczyć statycznie i było by to ‘true’ (np. ‘1 < 2’) to sprawa wyglądałaby inaczej – kompilator wiedziałby, że zmienna będzie zawsze zainicjalizowana i kod by się skompilował. A teraz zobaczmy jak sprawa się ma z tablicami.

public static void main(String[] args) {
int[] array = new int[16];

for(int x : array)
System.out.println(x);
}

Program się skompiluje i poprawnie wykona, wypisując w rezultacie uruchomienia ciąg szesnastu cyfr 0. Tak jak wcześniej napisałem – elementy tablic są zawsze inicjalizowane na wartości domyślne, niezależnie od miejsca deklaracji samej tablicy. Oczywiście samą tablicę musieliśmy utworzyć explicite. I jeszcze ostatni przykład.

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

System.out.println("someFloat: " + sc.someFloat
+ " doubleValue: " + sc.doubleValue);
}
}

class SomeClass {
Float someFloat;

double doubleValue;
}

Uruchomienie powyższego programu spowoduje wyświetlenie napisu "someFloat: null doubleValue: 0.0". Zauważmy jeszcze, że to, iż wartość wyrażenia ‘sc.someFloat’ została przekonwertowana do napisu "null" wynika z implementacji funkcji ‘println(…)’, a właściwie z tego, że zanim wywoła ona operację ‘toString()’ na przekazanej referencji sprawdzane jest, czy referencja ta nie ma właśnie wartości ‘null’.

19 maja 2008

Ruszyła rejestracja na konferencję JAVArsovia 2008

Zapraszam na konferencję JAVArsovia 2008, która odbędzie się w dniu 31 maja w gmachu Wydziału Biologii Uniwersytetu Warszawskiego, przy ulicy Ilji Miecznikowa 1 w Warszawie. Zarejestruj się już dziś. Wstęp wolny!

14 maja 2008

SCJP - Zasięg widoczności zmiennych

Dzisiaj będzie o zasięgu widoczności zmiennych (ang. variable scope) – kontynuacja serii zapowiedzianej w artykule "Przygotowania do SCJP czas zacząć". Temat prosty, więc nie będę się rozpisywał. Właściwie to ograniczę się do dwu przykładów, które demonstrują pewne możliwości popełnienia błędu, a raczej możliwości bycia wprowadzonym w błąd na egzaminie SCJP.

Pierwszy przykład pokazuje błąd często popełniany przez początkujących programistów, tj. próbę odwołania się do zmiennej instancji (bez użycia instancji) z metody statycznej.

public class Test {
String str = "Some text";

public static void main(String[] args) {
System.out.println(str); // błąd!
}
}

Jest dosyć oczywiste, że to nie może działać, więc zdaje się, że nie powinniśmy popełnić takiego błędu, ale weźmy pod uwagę sytuacje, w których najczęściej taki kod powstaje. Są to pisane na szybko programiki mające za zadanie przetestowanie czegoś prostego – w takiej sytuacji wiele się nie myśli, po prostu się pisze.

Zerknijmy na kolejny przykład. Przedstawia on funkcję, która wylicza zadaną potęgę dla zadanej podstawy. Patrząc na taki kod przypuszczalnie skupimy uwagę na algorytmie – analizując jego poprawność – i nie weźmiemy pod uwagę, że program się nawet nie skompiluje. Nie ma problemu, jeśli programujemy z użyciem IDE, ale na egzaminie SCJP nie ma tak dobrze.

static long power(long base, long exponent) {
for(int x = 1, y = 0; y < exponent; y++) {
x *= base;
}

return x; // błąd!
}

Zmienna x została zadeklarowana w pętli i do tejże pętli ograniczony jest zakres jej widoczności – jak napisano w Książce, podczas egzaminu na SCJP będziemy wielokrotnie testowani z umiejętności wykrywania tego typu błędów.

13 maja 2008

Rezerwujcie sobotę 31 maja - nadchodzi JAVArsovia 2008

Pamiętam dobrze pierwsze spotkanie Warszawa JUG. W małej salce, w gmachu Wydziału Matematyki Informatyki i Mechaniki Uniwersytetu Warszawskiego przywitał nas Jacek Laskowski, założyciel i leader Grupy. A było to w dniu 07 listopada 2006. Zaczynało się skromnie, ale Jacek to gość z charakterem, ogromną wiedzą i entuzjazmem… przyciąga tłumy – spotkania już wkrótce przeniosły się do auli wykładowej. Super! Ale to nie koniec historii! Jak już pisałem, Jacek to gość z charakterem i szybko zaczął mówić o konferencji. I stało się, w dniu 23 czerwca odbyła się JAVArsovia 2007, którą Jacek opisał później w artykule "Relacja z konferencji JAVArsovia - 1. konferencji Warszawa JUG". Był to prawdziwy sukces! Sukces Jacka, Warszawa JUG, sponsorów, naszego patrona Wydziału MIMUW i wszystkich zaangażowanych.

A teraz? Teraz idziemy na całość. Konferencja JAVArsovia 2008 to będzie wydarzenie roku. To znaczy – jako współorganizator – mam taką nadzieję. W każdym razie… rezerwujcie sobotę 31 maja - nadchodzi JAVArsovia! Szczegóły już niebawem.

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