28 września 2008

SCJP – Serializacja

Wakacje się skończyły; czas zakasać rękawy i wrócić do pracy w zwyczajowym tempie (tj. więcej niż jeden artykuł na miesiąc). Zregenerowałem nieco siły i z nowym zapasem energii "wznawiam" serię o SCJP, zapoczątkowaną artykułem "Przygotowania do SCJP czas zacząć". Dziś będzie o serializacji.

Mechanizm serializacji służy do zapisywania obiektów do strumienia danych, oraz w drugą stronę, do rekonstruowania obiektów z ciągu bitów odczytanych ze strumienia. W typowej sytuacji, strumienie te "podłączone są" do plików dyskowych, a więc z pewnym uproszczeniem można powiedzieć, że serializacja służy do zapisywania obiektów do plików i odczytywania tychże z powrotem. Zacznijmy od przykładu kodu, który serializuje (zapisuje) obiekt do pliku a następnie go odczytuje.

class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
saveObject(new DataObject(123), "db.bin");

System.out.println(readObject("db.bin").value);
}

public static void saveObject(DataObject obj, String fileName) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(fileName);

ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
outputStream.writeObject(obj);

outputStream.close();
}

public static DataObject readObject(String fileName)
throws IOException, ClassNotFoundException {

FileInputStream fileInputStream = new FileInputStream(fileName);

ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
DataObject obj = (DataObject) inputStream.readObject();

inputStream.close();

return obj;
}
}

class DataObject implements Serializable {
int value;

public DataObject(int value) {
this.value = value;
}
}

Uruchomienie powyższego programu spowoduje wyświetlenie liczby "123", co pozwala nam przypuszczać, że obiekt rzeczywiście został zapisany i następnie poprawnie odczytany. Zastanówmy się teraz, co to dokładnie znaczy "zapisać obiekt". W powyższym przykładzie sprawa jest oczywista, ale co się stanie, gdy nasz obiekt będzie zawierał referencje do innych obiektów? Czy one także zostaną zapisane? Tak, zostaną zapisane! Serializacja zapisuje de facto cały graf obiektów, nie tylko obiekt zapisywany explicite.

Aby obiekt mógł być serializowany, jego klasa musi implementować interfejs java.io.Serializable. Ale uwaga! Nie tylko obiekt, który serializujemy explicite musi być serializowalny. Serializowalne muszą być wszystkie zapisywane obiekty, a więc także te, na które wskazują referencje obiektu, który zapisujemy. Zmodyfikujmy nieco nasz przykład, jak pokazano poniżej.

class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
saveObject(new DataObject(123), "db.bin");

System.out.println(readObject("db.bin").valueObject.value);
}

// metody saveObject(…) oraz readObject(…) bez zmian
}

class DataObject implements Serializable {
ValueObject valueObject;

public DataObject(int value) {
this.valueObject = new ValueObject(value);
}
}

class ValueObject {
int value;

public ValueObject(int value) {
this.value = value;
}
}

Kod się skompiluje, ale otrzymamy błąd czasu wykonania. Próba uruchomienia programu zakończy się wyjątkiem java.io.NotSerializableException.

Exception in thread "main" java.io.NotSerializableException: my.pckg.ValueObject
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1081)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1375)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1347)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1290)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1079)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:302)
at my.pckg.Test.saveObject(Test.java:21)
at my.pckg.Test.main(Test.java:12)

Przyczyną jest naturalnie brak deklaracji, że klasa ValueObject jest serializowalna – dodanie deklaracji ‘implements Serializable’ rozwiązuje problem.

Serializacja obiektu oznacza zapisanie stanu obiektu, a więc "wartości" zmiennych instancyjnych, przy czym "wartość" zmiennej referencyjnej to referowany obiekt. Ważne jest, aby uświadomić sobie, że stan obiektu to tylko zmienne instancji, w odróżnieniu od zmiennych klasowych, tj. statycznych, których serializacja nie dotyczy. Mamy ponadto możliwość wykluczenia poszczególnych zmiennych z serializacji, za pomocą modyfikatora ‘transient’. Wystarczy zmienną oznaczyć jako ‘transient’ i z punktu widzenia serializacji będzie tak, jakby jej nie było.

Zastanówmy się teraz, jak wygląda deserializacja, a więc jak tworzone są obiekty i jak odtwarzany jest ich zserializowany stan. Zerknijmy na kolejny przykład.

class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
DataObject obj = new DataObject();

obj.value = 1;
obj.middleValue = 2;
obj.topValue = 3;

saveObject(obj, "db.bin");
obj = readObject("db.bin");

System.out.print(obj.value + " " + obj.middleValue
+ " " + obj.topValue + " " + obj.tValue);
}

// metody saveObject(…) oraz readObject(…) bez zmian
}

class TopDataObject {
int topValue = 345;
}

class MiddleDataObject extends TopDataObject implements Serializable {
int middleValue = 234;
}

class DataObject extends MiddleDataObject {
int value = 123;

transient int tValue = 999;
}

Uruchomienie tego programu spowoduje wyświetlenie liczb "1 2 345 0”. Klasa ‘MiddleDataObject’ deklaruje wprost, że jest serializowalna. Serializowalna jest także klasa ‘DataObject’, jako że dziedziczy z klasy ‘MiddleDataObject’, która jest serializowalna. Natomiast nie jest serializowalna klasa ‘TopDataObject’. Co się zatem będzie działo z zadeklarowaną w tej klasie, i odziedziczoną przez klasę ‘DataObject’ zmienną ‘topValue’? A cóż specjalnego miałoby się dziać? – Będzie ona zwyczajnie ignorowana przez mechanizm serializacji. Zmienna ‘topValue’ jest ignorowana, bo została zadeklarowana w klasie, która nie jest serializowalna. Zmienna ‘tValue’ jest ignorowana, bo została zadeklarowana jako ‘transient’; zatem w wyniku serializacji zostanie zapisana tylko wartość zmiennych ‘value’ i ‘middleValue’.

Zerknijmy teraz do artykułu "SCJP - Konstruktory i inicjalizacja", aby przypomnieć sobie (o ile nie pamiętamy), jak inicjalizowane są nowo tworzone obiekty. Generalnie rzecz biorąc, uruchamiane są konstruktory, począwszy od konstruktora klasy Object a skończywszy na konstruktorze klasy, której instancję tworzymy (w tym inicjalizowane są zmienne instancyjne, którym przypisaliśmy wartość w ramach deklaracji). I teraz pytanie. Co się dzieje, gdy obiekt tworzony jest w wyniku deserializacji a nie z użyciem operatora ‘new’? Otóż coś zupełnie innego. Odtworzenie stanu zserializowanego obiektu i inicjalizacja obiektu to dwie zupełnie różne rzeczy. Jak zatem wygląda odtworzenie obiektu w wyniku deserializacji? Przede wszystkim trzeba zapamiętać, że nie jest uruchamiany konstruktor i zmienne nie są inicjalizowane na wartości przypisane w trakcie deklaracji. Wartości zmiennych są przecież ustawiane na wartości odczytane ze strumienia a więc uruchamianie konstruktora nie ma sensu. No dobrze. A co z wartościami zmiennych, które przez serializacje są ignorowane, jak ‘topValue’ czy ‘tValue’ z powyższego przykładu? No więc:

- Jeśli pewna nadklasa klasy deserializowanego obiektu nie jest serializowalna, to w celu zainicjowania wartości zmiennych zadeklarowanych w tej klasie zostanie uruchomiony jej konstruktor. Tak więc w powyższym przykładzie, uruchomiony będzie konstruktor klasy ‘TopDataObject’ który ustawi wartość zmiennej ‘topValue’ na ‘345’ (inicjalizacja zmiennych w trakcie deklaracji to de facto część konstruktora). Nie zmienia to oczywiście faktu, że w żadnym wypadku nie zostanie uruchomiony konstruktor klas serializowalnych, a więc ‘MiddleDataObject’ czy ‘DataObject’.

- Zmienne ‘transient’ zadeklarowane w klasach serializowalnych nie są inicjalizowane ani wartościami odczytanymi ze strumienia (bo tych wartości tam nie ma), ani przez konstruktor (bo ten nie jest uruchamiany). Mają one zatem wartość domyślną dla danego typu, czyli np. 0 dla typu całkowitoliczbowego, ‘null’ w przypadku referencji. Więcej na temat wartości domyślnych w artykule "SCJP - Wartości domyślne zmiennych".

Wyobraźmy sobie teraz, że chcemy serializować obiekty naszej klasy, które mają referencję do obiektów klasy, którą zaimplementował ktoś inny, której nie możemy zmodyfikować i która nie jest serializowalna. Oczywiście nic nie stoi na przeszkodzie. Możemy oznaczyć taką referencję jako ‘transient’ i wszystko będzie działać… z tym tylko wyjątkiem, że możemy akurat nie chcieć ignorować tego obiektu. Chcielibyśmy jednak zapisywać stan także tej "cudzej" klasy, której musimy (bądź chcemy) używać. Otóż i to da się zrobić. Wystarczy zaimplementować metody wywołania zwrotnego (ang. callback):

private void writeObject(ObjectOutputStream os) throws IOException {
// kod, który wykona serializację "ręcznie"
}

oraz

private void readObject(ObjectInputStream os)
throws IOException, ClassNotFoundException {

// kod, który wykona deserializację "ręcznie"
}

w klasie, która ma się serializować inaczej niż standardowo. Poniżej przykład. Algorytm postępowania wygląda tak, że zmienną referencyjną, która nie może być serializowana w zwykły sposób oznaczamy jako ‘transient’, a jej stan zapisujemy "ręcznie", po czym wywołujemy metodę ‘defaultWriteObject()’ (może być też w odwrotnej kolejności, ale kolejność musi być ta sama), która wykona standardową serializację całej reszty.

class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
saveObject(new DataObject(123), "db.bin");

System.out.println(readObject("db.bin").valueObject.value);
}

// metody saveObject(…) oraz readObject(…) bez zmian
}

class DataObject implements Serializable {
transient ValueObject valueObject;

public DataObject(int value) {
this.valueObject = new ValueObject(value);
}

private void writeObject(ObjectOutputStream os) throws IOException {
os.defaultWriteObject();
os.writeInt(valueObject.value);
}

private void readObject(ObjectInputStream os)
throws IOException, ClassNotFoundException {

os.defaultReadObject();
valueObject = new ValueObject(os.readInt());
}
}

class ValueObject {
int value;

public ValueObject(int value) {
this.value = value;
}
}

30 sierpnia 2008

SCJP – Asercje

Dzisiaj będzie o asercjach, ostatnim temacie z rozdziału piątego Książki. Samo używanie asercji jest dosyć proste – nie ma tu żadnych haczyków –, trudność polega zaś na tym, aby wiedzieć, w jakich okolicznościach powinniśmy je stosować, a w jakich nie. Egzamin SCJP w tym zakresie skupia się na sprawdzeniu czy wiemy, jakie są zalecenia SUN’a.

Asercje to mechanizm pozwalający upewnić się, że nasze założenie, co do stanu programu w danej chwili jest prawidłowe. Przykładowo, implementując pewną metodę możemy zakładać, że wszystkie parametry wejściowe są liczbami dodatnimi, albo że referencje nie mają wartości ‘null’. Możemy przecież założyć, że metoda wywołująca przekaże odpowiednie wartości, a jeśli nie, to oznacza to błąd w kodzie. Właśnie dokładnie do tego służą asercje – do upewnienia się, że w kodzie nie ma błędów. Ważną cechą asercji jest, że mogą być one włączane i wyłączane. Intencją asercji jest, by były one sprawdzane tylko w fazie implementacji i testów, zaś w gotowej aplikacji, uruchomionej w systemie produkcyjnym już nie, tak więc domyślnie mechanizm asercji jest wyłączony – asercje nie są sprawdzane. Poniżej przykład użycia asercji.

private static float div(float x, float y) {
assert y != 0 : "y equals 0";

return x / y;
}

Po słowie kluczowym ‘assert’ umieszczamy wyrażenie typu logicznego, którego prawdziwość chcemy przetestować. Dwukropek i następujący po nim komentarz (tj. dowolne wyrażenie typu napisowego, np. wywołanie funkcji zwracającej String) to elementy opcjonalne. Jeśli asercja nie jest prawdziwa JVM rzuca wyjątek java.lang.AssertionError. Wspomniany komentarz zostanie wykorzystany jako opis wyjątku. Wywołanie powyższej funkcji z drugim argumentem równym 0 spowoduje wyświetlenie następującego komunikatu.

Exception in thread "main" java.lang.AssertionError: y equals 0
at next.Test.div(Test.java:12)

Asercje zostały wprowadzone do języka Java począwszy od wersji 1.4. Oznacza to, że do tego momentu ‘assert’ nie było słowem kluczowym a więc w szczególności słowo to mogło być używane jako identyfikator… i pewnie czasami było. Próba kompilacji takiego kodu kompilatorem w wersji 1.5 się naturalnie nie powiedzie, chyba że powiemy explicite, że mamy do czynienia ze starym kodem, tj. dodamy do polecenia ‘javac’ flagę ‘–source 1.3’. Kod wówczas skompiluje się poprawnie, aczkolwiek wygenerowane będą ostrzeżenia.

Aby uruchomić program w trybie sprawdzania asercji należy do polecenia ‘java’ dodać flagę ‘-enableassertions’, lub w formie skróconej ‘-ea’. Asercje można włączać także selektywnie, dla konkretnych klas lub pakietów, podając nazwę klasy lub pakietu po fladze. Polecenia te dotyczą wszystkich managerów ładowania klas (ang. class loader) a więc wszystkich klas, z wyjątkiem klas systemowych, które obsługiwane są przez inny mechanizm ładowania. Aby włączyć sprawdzanie asercji dla klas systemowych należy użyć flagi ‘-enablesystemassertions’ lub ‘-esa’.

Asercje wyłączone są domyślnie, ale można je wyłączyć także explicite – ma to sens wówczas, gdy włączamy asercje globalnie, lub dla pewnego pakietu klas, a wyłączamy selektywnie dla niektórych klas lub pod pakietów. Do wyłączenia asercji używamy flagi ‘-disableassertions’ lub ‘-da’, oraz ‘-disablesystemassertions’ lub ‘-dsa’ dla klas systemowych. Kilka przykładów poniżej.

java –ea my.package.MainClass

java –ea:my.package.MyClass my.package.MainClass

// uwaga na trójkropek po nazwie pakietu!
java –ea:my.package… my.package.MainClass

java –esa –ea –da:my.package.MyClass my.package.MainClass

Wiemy już jak pracuje się z asercjami, przejdźmy więc w końcu do tego, no z największym prawdopodobieństwem może pojawić się na egzaminie SCJP – wytycznych SUN’a, kiedy asercji używać, a kiedy nie. Oto garść prostych reguł:

Nie używaj asercji do walidacji parametrów wywołania funkcji publicznych. Walidacja ta powinna być wykonywana zawsze, więc mechanizm asercji nie jest odpowiedni, jako że można go włączać i wyłączać, a co więcej, w gotowych aplikacjach przeważnie jest wyłączony.

Nie używaj asercji do walidacji parametrów przekazanych z linii poleceń. Powód jest dokładnie taki sam, jak dla wywołań funkcji publicznych – w końcu funkcja main(…) to funkcja publiczna.

Używaj natomiast asercji do walidacji parametrów wywołania funkcji prywatnych. W przeciwieństwie do funkcji publicznych, te wywołania są w pełni kontrolowane przez osobę, która zaimplementowała klasę. Walidacja parametrów ma tu znaczenie tylko kontrolne, na etapie testowania, więc asercje są OK.

Używaj asercji do upewnienia się, że nie wystąpiła sytuacja, która nie powinna była wystąpić w żadnych warunkach - jeśli wystąpiła to znaczy, że program zawiera błąd. Przykładowo, jeśli z naszych kalkulacji wynika, że nie jest możliwe, aby wykonanie programu dotarło do pewnego miejsca (np. gąszcz if-else) to umieśćmy w tym miejscu instrukcję ‘assert false;’ aby zwalidować poprawność naszych kalkulacji.

Wyrażenia używane przez asercje nie powinny mieć efektów ubocznych. Zarówno wyrażenie logiczne jak i komentarz mogą być wywołaniami funkcji, a te mogą mieć skutki uboczne. Mogą mieć, ale zdecydowanie nie powinny.

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.