31 lipca 2008

SCJP - Popularne typy wyjątków

W ostatnim artykule – "SCJP – Wyjątki" – przedstawiłem temat wyjątków w języku Java. Zapowiedziałem w nim także, że konkretne typy wyjątków omówię w kolejnym artykule i to właśnie robie dzisiaj. Będzie więc o popularnych typach wyjątków, przy czym popularne to te, których znajomość wymagana jest na egzaminie SCJP.

Bodaj najpopularniejszym wyjątkiem jest ‘NullPointerException’. Dziedziczy on bezpośrednio z klasy ‘RuntimeException’ a więc jest wyjątkiem niekontrolowanym (ang. unchecked). Wyjątek ten rzucany jest przez Wirtualną Maszynę Javy (ang. akr. JVM) przy próbie odwołania się do obiektu (np. uruchomienie metody) z użyciem referencji, która ma wartość ‘null’.

Pamięć operacyjna wykonującego się programu składa się z dwóch obszarów: stosu (ang. stack) i sterty (ang. heap). Na stercie znajdują się obiekty, a na stosie kontekst wykonywanego kodu, tj. zmienne lokalne. Gdy wywołujemy metodę, to jej zmienne lokalne, a więc i przekazywane parametry zapisywane są na stosie. Pamięć ta zwalniana jest dopiero po zakończeniu wykonania metody. A co jeśli metody nigdy się nie kończą, za to wywołują (rekurencyjnie) kolejne metody? Wtedy właśnie, po pewnym czasie wyczerpuje się pamięć stosu i Wirtualna Maszyna Javy rzuca wyjątek ‘StackOverflowError’. Wyjątek ten jest podklasą (nie bezpośrednią) klasy ‘Error’ a więc jest to również wyjątek niekontrolowany. Zresztą, wszystkie opisywane tu wyjątki są wyjątkami niekontrolowanymi, a więc podklasami klasy ‘Error’ albo ‘RuntimeException’.

W artykule "SCJP - Klasy opakowujące typów prostych" pisałem między innymi o metodach konwersji z typu napisowego ‘String’ na typy numeryczne. Jedną z nich jest metoda ‘parseInt(…)’ z klasy ‘Integer’. Jeśli napis nie może być przekonwertowany na liczbę, tj. jest to np. "sto jeden", a nie "101", to metody te rzucają wyjątek ‘NumberFormatException’. Wyjątek ten jest podklasą klasy ‘IllegalArgumentException’. Zauważmy tutaj, jak hierarchia dziedziczenia klas wyjątków odzwierciedla hierarchię rodzajów błędów. Wyjątek ‘IllegalArgumentException’ oznacza, że argument metody jest z pewnych powodów nieprawidłowy, a jej podklasa ‘NumberFormatException’ precyzuje, że argument jest nieprawidłowy, bo nie może być przekonwertowany do wartości liczbowej.

Wyjątek ‘ArrayIndexOutOfBoundsException’ rzucany jest, gdy nastąpi odwołanie do indeksu tablicy, który jest z poza dopuszczalnego zakresu – tj. jeśli indeks jest liczbą ujemną lub większą bądź równą (numerujemy od zera) od ilości elementów w tablicy.

Wyjątek ‘ClassCastException’ jest efektem próby rzutowania obiektu na typ z nim nie kompatybilny.

Wyjątek ‘IllegalStateException’ oznacza, że stan urządzeń bądź zewnętrznych systemów, których nasz program używa jest nieprawidłowy i wykonywana operacja nie może być z tego powodu zakończona.

Wyjątek ‘AssertionError’ informuje, że nie jest prawdziwa jedna z asercji. Asercje będą tematem następnego artykułu, tak więc do tego tematu jeszcze powrócimy.

Wyjątek ‘ExceptionInInitializerError’ jest rzucany, gdy wystąpi błąd w trakcie statycznej inicjalizacji zmiennej, bądź w trakcie wykonania bloku inicjalizacyjnego.

Wyjątek ‘NoClassDefFoundError’ jest rzucany, gdy Wirtualna Maszyna Javy – a mówiąc precyzyjniej, mechanizm ładowania klas – nie może znaleźć odpowiedniej klasy. Przeważnie błąd ten spowodowany jest brakiem odpowiedniej biblioteki na ścieżce przeszukiwania (ang. classpath).

22 lipca 2008

SCJP – Wyjątki

Pod pojęciem wyjątków kryje się wiele zagadnień. Wyjątki są tworzone i rzucane a następnie łapane i obsługiwane. Pewne wyjątki trzeba deklarować a innych nie trzeba, ale można. Z perspektywy egzaminu SCJP – i nie tylko – ważna jest także znajomość sytuacji, które powodują rzucenie pewnych typowych wyjątków, takich jak ‘NullPointerException’. Dziś będzie o większości z tych rzeczy, tj. o wszystkim z wyjątkiem opisu konkretnych klas wyjątków. O tym w następnym artykule.

Wyjątek w języku Java to obiekt, który opisuje pewną sytuację błędną lub nieprawidłową – wyjątkową. Jest to obiekt odpowiedniego typu, tj. obiekt klasy ‘Throwable’ lub dowolnej podklasy. Poniższy diagram przedstawia trzon hierarchii dziedziczenia dla klas wyjątków.



Każda z klas ‘Error’, ‘Exception’ i ‘RuntimeException’ ma jeszcze wiele innych podklas, oznaczających konkretny rodzaj sytuacji wyjątkowej. Z punktu widzenia programisty wyjątki dzielą się na dwa rodzaje: kontrolowane (ang. checked) i niekontrolowane (ang. unchecked). Wyjątki kontrolowane tym różnią się od niekontrolowanych, że muszą być jawnie obsłużone w metodach, tj. albo muszą być złapane, albo zadeklarowane na liście wyjątków rzucanych przez metodę. Zerknijmy na poniższy przykład.

class SomeClass {
void someOp() {
throw new NullPointerException();
}

void someOtherOp() throws IOException {
throw new IOException();
}

void nextOp() {
try {
throw new IOException();
} catch (IOException ioExc) {
ioExc.printStackTrace();
}
}
}

W metodzie ‘someOp()’ rzucamy wyjątek ‘NullPointerException’. Jest to wyjątek niekontrolowany, więc nic specjalnego nie musimy robić. W metodzie ‘someOtherOp()’ rzucamy wyjątek ‘IOException’. Jest on wyjątkiem kontrolowanym, więc nie możemy go zignorować – deklarujemy, że metoda rzuca ten wyjątek. W metodzie ‘nextOp()’ również pojawia się wyjątek ‘IOException’, jednak w tym wypadku od razu go łapiemy i obsługujemy.

Wyjątki niekontrolowane to instancje klas ‘Error’ i ‘RuntimeException’ oraz ich dowolnych podklas. Wyjątki kontrolowane to instancje klas ‘Throwable’ i ‘Exception’ oraz ich podklas, oczywiście z wyłączeniem klas ‘Error’ i ‘RuntimeException’. Wyjątków niekontrolowanych nie musimy deklarować czy obsługiwać, ale możemy, poprawny jest więc poniższy kod.

class SomeClass {
void someOp() throws Error { // ta deklaracja jest nadmiarowa
throw new Error();
}
}

Obsługa wyjątków polega na tym, że kod, który może rzucić wyjątek ujmujemy w klauzulę ‘try’. Bezpośrednio po klauzuli ‘try’ następuje seria klauzul ‘catch’. Po klauzulach ‘catch’ następuje klauzula ‘finally’. Klauzula ‘finally’ jest opcjonalna. Opcjonalne są także klauzule ‘catch’, ale jeśli nie ma żadnej klauzuli ‘catch’, to wymagana jest klauzula ‘finally’. Jeśli nie ma klauzuli ‘finally’, to wymagana jest co najmniej jedna klauzula ‘catch’. W klauzuli ‘finally’ umieszczamy kod, który ma się wykonać zawsze, po wykonaniu kodu objętego klauzulą ‘try’. Jest to dobre miejsce na umieszczenie kodu sprzątającego, który powinien się uruchomić niezależnie od tego, co się stało w klauzuli ‘try’. To, że kod ten uruchamia się zawsze demonstruje dobrze poniższy przykład - uruchomienie programu spowoduje wyświetlenie napisu "klauzula finally".

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

public static int someOp() {
try {
return 1;
} finally {
System.out.println("klauzula finally");
}
}
}

Klauzul ‘catch’ może być dowolna ilość. Co prawda moglibyśmy zadeklarować jedną klauzulę ‘catch’ która łapie wszystkie wyjątki, ale przeważnie chcemy wykonać różne akcje w zależności od typu wyjątku, przynajmniej dla niektórych z nich. Klauzule obsługujące konkretne wyjątki umieszczamy jako pierwsze a w dalszej kolejności występują klauzule bardziej ogólne. Demonstruje to poniższy przykład.

class SubException extends Exception {}

class NextException extends Exception {}

public class Test {
public void someOp() {
try {
throwingOp();

} catch (SubException subExc) {
// jakieś specyficzne operacje
} catch (Exception exc) {
// operacje ogólne, np. logowanie błędu
}
}

public void throwingOp() throws SubException, NextException {}
}

Wyjątek ‘SubException’ obsługujemy w pewien szczególny sposób, więc umieściliśmy dedykowaną klauzulę ‘catch’. Następna klauzula obsługuje wszystkie inne wyjątki, w tym wyjątki klasy ‘NextException’. Umieszczenie tych klauzul w odwrotnej kolejności byłoby błędem czasu kompilacji – klauzule bardziej ogólne muszą znajdować się za tymi wyspecjalizowanymi.

19 lipca 2008

SCJP - Pętle

Rozdział piąty Książki – patrz artykuł "Przygotowania do SCJP czas zacząć" – wydawał się być bardzo długi a przez to straszny, ale nie jest źle. Idzie dosyć szybko, a to pewnie z tego powodu, że poruszane w nim tematy nie są przesadnie skomplikowane. Dziś będzie właśnie jeden z nich, tj. pętle. Pętle ‘while’ i ‘do while’ nie zaskakują zupełnie niczym, więc nic o nich nie będę pisał. Przejdę od razu do pętli ‘for’, oraz instrukcji ‘break’ i ‘continue’.

Pętla ‘for’ występuje w dwu odmianach: tradycyjnej, ogólnego przeznaczenia i nowej – obecnej od Javy 1.5 – służącej do iteracji po elementach kolekcji lub tablicy. O drugim wariancie będę pisał w osobnym artykule, kiedyś w przyszłości. Dziś zobaczmy tylko prosty przykład, jak taka pętla wygląda. Aby więc wykonać jakąś akcję dla kolejnych elementów tablicy, w tym przypadku dla tablicy liczb typu ‘int’, napiszemy

for (int i : new int[] { 1, 3, 5, 7, 11 }) {
System.out.print(i + " ");
}

Generalnie, przed dwukropkiem deklarujemy zmienną, do której będą przypisywane kolejne wartości z kolekcji, a po dwukropku umieszczamy dowolne wyrażenie, którego wartością jest kolekcja lub tablica. W naszym wypadku jest to instrukcja tworząca nową tablicę, ale może to być także wywołanie metody czy zmienna referencyjna odnosząca się do obiektu pewnej kolekcji, np.

public static void main(String[] args) {
List<Integer> someList = new LinkedList<Integer>();

someList.add(1);
someList.add(2);
someList.add(3);

for (int i : someList) {
System.out.print(i + " ");
}
}

Tradycyjna pętla ‘for’ składa się z trzech, oddzielonych średnikami sekcji w części sterującej pętli, oraz oczywiście z kodu umieszczonego w tejże pętli. Przyjęło się mówić, że pierwsza sekcja służy do deklaracji zmiennych, druga to warunek pętli, a więc wyrażenie o typie ‘boolean’ a trzecia to instrukcja inkrementacji zmiennych. Co prawda tak się tego zazwyczaj używa, ale trzeba wiedzieć, że pierwsza i trzecia sekcja może zawierać dowolną instrukcję. Sekcja druga to dowolne wyrażenie typu ‘boolean’. Dodatkowo, każda z sekcji może być zupełnie pusta. Przykładowo, poprawną jest pętla

for (System.out.print("sekcja pierwsza") ; ; System.out.print("sekcja trzecia")) {
break;
}

Jeśli druga sekcja jest pusta, to pętla zachowuje się tak, jakby był tam umieszczony literał ‘true’, a więc jest to pętla nieskończona, a właściwie to byłaby to pętla nieskończona, gdyby nie instrukcja ‘break’, umieszczona w ciele pętli. Sekcja pierwsza jest to kod, który uruchamiany jest na samym początku, zawsze dokładnie raz, niezależnie od wartości sekcji drugiej. Przykładowo, uruchomienie programu

public static void main(String[] args) {
int x = 1, y = x;

for (System.out.print("sekcja pierwsza") ; x != y ;
System.out.print("sekcja trzecia")) {

System.out.print("ciało pętli");
}
}

spowoduje wyświetlenie napisu

sekcja pierwsza

Po wykonaniu sekcji pierwszej, przeznaczonej na inicjalizację zmiennych sterujących pętli, obliczana jest wartość wyrażenia z sekcji drugiej; jeśli jest to ‘true’, to wykonuje się ciało pętli. Na końcu wykonywany jest kod z sekcji trzeciej nagłówka pętli, przeznaczonej na kod inkrementujący wartości zmiennych sterujących.

Instrukcja ‘break’ służy do natychmiastowego przerwania wykonania pętli – sterowanie zostaje wówczas przeniesione do pierwszej instrukcji za pętlą. Efektem uruchomienia programu

public static void main(String[] args) {
int i = 0;

for (System.out.println("sekcja pierwsza") ; condition() ;
System.out.println("sekcja trzecia")) {

System.out.println("ciało pętli");

if(i++ > 0)
break;
}

System.out.println("po pętli");
}

public static boolean condition() {
System.out.println("sekcja druga (true)");

return true;
}

jest wyświetlenie sekwencji

sekcja pierwsza
sekcja druga (true)
ciało pętli
sekcja trzecia
sekcja druga (true)
ciało pętli
po pętli

Instrukcja ‘continue’ przerywa bieżące wykonanie pętli, a więc tylko wykonanie ciała pętli w bieżącej iteracji. Zwróćmy uwagę, że instrukcja ‘continue’ nie powoduje zaniechania wykonania trzeciej sekcji nagłówka pętli. Efektem uruchomienia programu

public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
if (i % 2 == 1)
continue;

System.out.println(i + " jest liczbą parzystą");
}
}

jest wyświetlenie sekwencji

0 jest liczbą parzystą
2 jest liczbą parzystą
4 jest liczbą parzystą
6 jest liczbą parzystą
8 jest liczbą parzystą

Instrukcje ‘break’ i ‘continue’ odnoszą się domyślnie do pętli, bezpośrednio w której się znajdują, ale możliwe jest wskazanie, poprzez etykietę, pętli bardziej zewnętrznej. Przykładowo, uruchomienie programu

public static void main(String[] args) {
outerLoop: for (int j = 0;; j += 100) {
for (int i = 0; i < 5; i++) {
if ((i + j) % 2 == 1)
continue;

if (j > 100)
break outerLoop;

System.out.println(i + j + " jest liczbą parzystą");
}
}
}

skutkuje wyświetleniem sekwencji

0 jest liczbą parzystą
2 jest liczbą parzystą
4 jest liczbą parzystą
100 jest liczbą parzystą
102 jest liczbą parzystą
104 jest liczbą parzystą

10 lipca 2008

SCJP - Instrukcje warunkowe

Zacząłem ostatnio nowy projekt, połączony ze zwiedzaniem nowego miasta, więc czasu na SCJP zostało niewiele, ale dzisiaj akurat pada, więc siadam do komputera i tym samym zaczynam przerabiać piąty rozdział Książki. Będzie o instrukcjach warunkowych ‘if’ i ‘switch’.

Instrukcja warunkowa ‘if’ nie należy do szczególnie zagadkowych. Czytając Książkę natrafiłem właściwie tylko na jedną rzecz wartą powtórzenia – zerknijmy na poniższy przykład.

public static void main(String[] args) {
if (1 == 2)
if (2 == 3)
System.out.println("1 == 2 & 2 == 3");
else
System.out.println("1 != 2");
}

Na pierwszy rzut oka wygląda na to, że uruchomienie programu spowoduje wyświetlenie napisu "1 != 2", ale jest inaczej. Niezależnie od tego, jakie wcięcia zrobimy w naszym kodzie, ‘else’ dotyczy drugiej, zagnieżdżonej instrukcji ‘if’. Prawidłowo sformatowany kod wygląda tak

public static void main(String[] args) {
if (1 == 2)
if (2 == 3)
System.out.println("1 == 2 & 2 == 3");
else
System.out.println("1 != 2");
}

Możemy tłumaczyć się przed samymi sobą, że wiedzielibyśmy, jak wykona się powyższy program, gdyby nie moje wprowadzające w błąd wcięcia, ale to jest właśnie to, na co musimy się uczulić przed egzaminem na SCJP – na to, że będziemy celowo wprowadzani w błąd. W każdym razie, obowiązuje następująca zasada – ‘else’ należy do najbardziej zagnieżdżonej instrukcji ‘if’, do jakiej może należeć.

Instrukcja ‘switch’ jest z jednej strony dobrze znana, ale z drugiej, raczej rzadko stosowana, więc kilka słów przypomnienia może się przydać. Zacznijmy od poniższego przykładu.
 
public static void main(String[] args) {
int x = 5;

switch (x) {
case 1:
System.out.println("jeden");
break;
default:
System.out.println("jakaś inna cyfra");
case 2:
System.out.println("dwa");
break;
case 3:
case 4:
System.out.println("co najmniej trzy");
}
}

Efektem uruchomienia tego programu jest

jakaś inna cyfra
dwa

Zapamiętajmy kilka faktów, jak ten, że wariant domyślny (tj. etykieta ‘default’) nie musi być umieszczony jako ostatni, na końcu instrukcji ‘switch’. Pamiętajmy także, że wariant, który zostanie wybrany w trakcje wykonania jest tylko punktem początkowym, punktem wejścia. Kod będzie wykonywany aż do napotkania instrukcji ‘break’. Instrukcja ‘switch’ jest w pewnym sensie podobna do instrukcji ‘goto’ – jest to skok do pewnej etykiety, w zależności od wartości testowanej zmiennej.

Instrukcja ‘switch’ może operować na zmiennych i wartościach typu char, byte, short lub int oraz na typach wyliczeniowych (‘enum’). Argumenty dla poszczególnych wariantów (tj. dla etykiet ‘case’) muszą także być odpowiedniego typu, tj. dla typów numerycznych muszą mieścić się w odpowiednim zakresie. Przykładowo, kod

public static void main(String[] args) {
byte x = 4;

switch (x) {
case 127:
System.out.println("sto dwadzieścia siedem");
break;
case 128: // błąd
System.out.println("sto dwadzieścia osiem");
break;
}
}

się nie skompiluje, jako że zmienna ‘x’ jest typu ‘byte’, który przyjmuje wartości z zakresu od -128 do 127. Argumenty dla wariantów muszą być wartościami znanymi w czasie kompilacji, a więc poza literałami, mogą to być tylko zmienne finalne, z wartością przypisaną w czasie deklaracji. Przykładowo, kod

public static void main(String[] args) {
final byte a = 64;
final byte b;
b = 32;

byte x = 127;
switch (x) {
case a:
case b: // błąd
System.out.println("potęgi dwójki");
}
}

się nie skompiluje, jako że zmienna ‘b’ nie jest zainicjalizowana w czasie deklaracji, a została użyta jako argument wariantu.