5 grudnia 2008

Jak z kodu wtyczki do Eclipse dostać obiekt wybranego pliku

W końcu pojawiła się dobra okazja, by napisać wtyczkę (plug-in) do Eclipse; coś, czego chciałem, albo wręcz o czy marzyłem już od dość dawna. Sprawa jest dosyć prosta; chcę, aby po kliknięciu prawym guzikiem myszy na obiekcie pliku w widoku Package Explorer pojawiła się – między innymi – opcja wykonania na tymże pliku jakiejś operacji. Paradoksalnie, najtrudniejsze w tym przedsięwzięciu okazało się odnalezienie informacji o tym, w jaki sposób można z kodu wtyczki dostać obiekt pliku, na którym kliknięto. W końcu udało mi się znaleźć odpowiedź. Zrobi to dla nas poniżej pokazana metoda (przy założeniu, że wtyczka jest skonfigurowana tak, że uaktywnia się tylko przy wyborze jednego obiektu IFile).

private IFile getSelectedFile() {
ISelection selection = PlatformUI.getWorkbench().getActiveWorkbenchWindow()
.getSelectionService().getSelection();

return (IFile) ((IStructuredSelection) selection).getFirstElement();
}

18 listopada 2008

SCJP - Stany wątków

Czytam sobie 9. rozdział książki "SCJP Sun Certified Programmer for Java 5 Study Guide (Exam 310-055)", tj. o wątkach, a konkretnie fragment o stanach w jakich wątek może się znajdować i... oczom nie wierzę. Stany o których piszą, w szczególności rozróżnienie ‘Running’/’Runnable’ po prostu nie istnieją! A na stan 'Terminated’ mówią – chyba tylko po to by wprowadzić zamęt – ‘Dead’. Kto nie wierzy, że jest inaczej niż piszą w Książce niech sprawdzi JavaDoca dla typu Thread.State. To już drugi poważny błąd jaki wykryłem w tej książce. O pierwszym pisałem niespełna miesiąc temu w artykule „SCJP - Statyczne klasy zagnieżdżone”. Trochę mnie to martwi, bo nie wiem ile tego typu nie wykrytych „psikusów” znajduje się w książce, na której bądź co bądź oparłem swój proces edukacyjny dla certyfikatu SCJP.

16 listopada 2008

SCJP - Tworzenie i uruchamianie wątków

Czasem spotyka się z opinią, że programowanie w języku Java jest na tyle proste, że studia informatyczne są zupełnie nie potrzebne by móc to robić dobrze – wystarczy kurs w stylu „Java dla opornych”. Oczywiście wszystko zależy od tego jak zdefiniujemy słowo „dobrze”. Można argumentować że aplikacja – by działać – nie musi być dobrze zaprojektowana, tj. z uwzględnieniem paradygmatów obiektowości i wzorców projektowych, że nie trzeba rozumieć pojęcia złożoności obliczeniowej żeby implementować algorytmy; ale nie da się zaprzeczyć temu, że aby tworzyć bezpieczne aplikacje współbieżne trzeba znać podstawowe zagadnienia programowania współbieżnego. Tu już nie chodzi przecież ani o elegancję ani nawet o wydajności. Tu chodzi o poprawność. W dzisiejszym artykule napiszę o podstawach pracy z wątkami w języku Java czyli właściwie o tym, jak sprawić by problemy współbieżności zaczęły się pojawiać – tj. jak uruchomić kod w osobnym wątku wykonania.

Wątek to nic innego jak pewien kod, który wykonywany jest przez Wirtualną Maszynę Javy niezależnie od kodu innych wątków, przy czym owo niezależnie oznacza współbieżnie. Aby wykonać pewien kod w osobnym wątku wykonania należy przede wszystkim określić jaki kod chcemy wykonać; należy więc nowy wątek zdefiniować. Definicja wątku to nic innego jak implementacja podklasy klasy java.lang.Thread.

Implementacja programu w języku Java jest równoznaczna z implementacją metody main(…). Uruchomienie programu napisanego w Javie to de facto uruchomienie metody main(…). Zakończenie się metody main(…) jest równoznaczne z zakończeniem się programu. Metoda main(…) to nic innego jak implementacja głównego wątku wykonania naszej aplikacji – wątek ten uruchamiamy uruchamiając aplikację. Każdy inny wątek implementujemy tworząc podklasę klasy java.lang.Thread i nadpisując jej metodę run() a więc analogicznie – implementując metodę, tyle że run() a nie main(…). Definicja wątku to jednak nic więcej jak tylko pewna klasa zawierająca fragment kodu w metodzie run(). Aby utworzyć nowy wątek i uruchomić w jego ramach kod zdefiniowany we wspomnianej metodzie run() należy wywołać metodę start() na instancji tejże klasy – dopiero wówczas tworzony i równocześnie uruchamiany jest wątek. Przykład poniżej.

public class TestClass {
public static void main(String[] args) {
System.out.println("Kod wykonany w wątku głównym");

Thread newThread = new MyThread();

// dopiero wywołując metodę start() tworzymy nowy wątek
newThread.start();
}
}

// ta klasa to tylko definicja wątku
class MyThread extends Thread {
public void run() {
System.out.println("Kod wykonany w nowym wątku");
}
}

Definiowanie wątków poprzez implementację podklas klasy Thread jest jak najbardziej poprawne, jednak zalecane jest aby robić to nieco inaczej. Metoda start() z klasy Thread pokazana w powyższym przykładzie tworzy nowy wątek i uruchamia w jego ramach kod zdefiniowany w metodzie run(). Oczywiście kod ten może nie robić nic innego, jak tylko uruchamiać kolejną metodę zdefiniowaną w innej klasie, w szczególności metodę run() zdefiniowaną w klasie implementującej interfejs java.lang.Runnable. Klasa Thread udostępnia także konstruktor jednoargumentowy, akceptujący instancje klasy implementującej interfejs Runnable. Implementacja metody run() w klasie Thread jest właśnie taka, że jeśli tworząc jej instancję przekazaliśmy obiekt Runnable, to w ramach wątku zostanie uruchomiona metoda run() tego właśnie obiektu. Wszystko powinno stać się jasne po przestudiowaniu kolejnego przykładu.

public class TestClass {
public static void main(String[] args) {
System.out.println("Kod wykonany w wątku głównym");

// wątek tworzymy na bazie instancji klasy implementującej Runnable
Thread newThread = new Thread(new MyRunnable());

newThread.start();
}
}

class MyRunnable implements Runnable {
public void run() {
System.out.println("Kod wykonany w nowym wątku");
}
}

Oprócz metody start() klasa Thread udostępnia jeszcze szereg innych, mniej lub bardziej kluczowych metod. Jedną z nich jest metoda getName(), która zwraca nazwę wątku. Wątek główny zawsze nazywa się ‘main’. Wątki tworzone przez programistę mają nazwy wygenerowane automatycznie przez Wirtualną Maszynę Javy, ale naturalnie nazwa ta może być zmieniona. W tym celu można użyć metody setName(…) albo konstruktora, który jako argument wywołania (drugi jeśli przekazujemy instancję Runnable lub pierwszy i jedyny, jeśli nie) akceptuje nazwę nowo tworzonego wątku. Nazwy te nie muszą być unikalne – różne wątki mogą mieć tą samą nazwę. Unikalny natomiast jest identyfikator wątku, który możemy odczytać z użyciem metody getId() i którego nie możemy zmienić. Przydatna jest także statyczna metoda currentThread(), która zwraca instancję klasy Thread opisującą aktualnie wykonywany wątek.

Ważną właściwością wątku jest jego stan. Aktualny stan wątku możemy sprawdzić posługując się metodą getState(), która zwraca jedną z wartości wyliczeniowych typu java.lang.Thread.State. Nowo utworzonej instancji wątku, który jeszcze nie został uruchomiony odpowiada stan NEW. Gdy wątek jest startowany, jego stan automatycznie zmienia się na RUNNABLE, by w końcu – potencjalnie przechodząc przez różne stany przejściowe – zakończyć się w stanie TERMINATED. Bardzo ważne jest by zapamiętać – zwłaszcza z perspektywy egzaminu SCJP – że wystartować można tylko i wyłącznie wątek który znajduje się w stanie NEW. Oznacza to, że każdy wątek może być uruchomiony tylko raz! Każde następne – tj. nie pierwsze – wywołanie metody start() na instancji reprezentującej wątek spowoduje zwrócenie wyjątku IllegalThreadStateException. Zerknijmy na poniższy przykład.

public class TestClass {
public static void main(String[] args) {
Thread newThread = new Thread(new MyRunnable(), "newThread");

System.out.println(
"Wątek " + newThread.getName() + " w stanie " + newThread.getState());

newThread.start();
}
}

class MyRunnable implements Runnable {
public void run() {
Thread thread = Thread.currentThread();

System.out.println(
"Wątek " + thread.getName() + " w stanie " + thread.getState());
}
}

Zauważmy, że w metodzie run() implementowanej w klasie MyRunnable nie mamy dostępu do instancji klasy Thread – bo też póki co kod ten nie jest powiązany z żadną instancją reprezentującą wątek – a więc musimy się posłużyć metodą statyczną currentThread(). Uruchomienie tego programu spowoduje wyświetlenie napisu:

Wątek newThread w stanie NEW
Wątek newThread w stanie RUNNABLE

Wiemy już, że wątek możemy uruchomić tylko i wyłącznie raz (także wątek który się zakończył nie może być uruchomiony ponownie), ale czy możemy utworzyć i uruchomić wiele wątków wykonujących jednocześnie (współbieżnie) ten sam kod? Oczywiście możemy – przykład poniżej.

public class TestClass {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();

// wszystkie wątki będą wykonywały dokładnie ten sam kod
Thread threadA = new Thread(runnable);
Thread threadB = new Thread(runnable);
Thread threadC = new Thread(runnable);
Thread threadD = new Thread(runnable);

threadA.start();
threadB.start();
threadC.start();
threadD.start();
}
}

class MyRunnable implements Runnable {
private int val = 0;

public void run() {
for (int i = 0; i < 100; i++)
System.out.println(
"id == " + Thread.currentThread().getId() + ", val == " + getVal());
}

private synchronized int getVal() {
return ++val;
}
}

Powyższy program tworzy i uruchamia cztery nowe wątki, z których każdy, współbieżnie z pozostałymi wykonuje metodę run() dokładnie tej samej instancji klasy MyRunnable, a więc wyświetla i inkrementuje wartość tej samej zmiennej ‘val’. Poniżej fragment tego, co wypisał ten program po uruchomieniu w trakcie moich testów. Jak widać, wątki rzeczywiście przeplatają się między sobą (choć często występują też długie fragmenty, gdy nieprzerwanie wykonywał się jeden z wątków). Co ciekawe, w drugiej linii pojawia się wartość 150, mimo że w linii pierwszej pojawiła się wartość 224. Dowodzi to, że wątek o identyfikatorze 9 wykonał operację getVal() w momencie gdy wartość zmiennej ‘val’ wynosiła 149 poczym przestał się wykonywać przed wypisaniem komunikatu „id == 9, val == 150” i wznowił swe wykonanie, tj. wypisał ten komunikat, dopiero po tym jak inne wątki zwiększyły wartość tej zmiennej grubo ponad 200.

id == 10, val == 224
id == 9, val == 150
id == 10, val == 226
id == 11, val == 225
id == 10, val == 228

11 listopada 2008

SCJP - Sortowanie list i tablic oraz wyszukiwanie binarne

Dziś będzie o sortowaniu list i tablic oraz o wyszukiwaniu binarnym w tychże, a więc o klasach java.util.Arrays i java.util.Collections (nie mylmy z interfejsem java.util.Collection) oraz interfejsach java.util.Comparator<T> i java.lang.Comparable<T>.

Klasy Arrays oraz Collections to klasy narzędziowe implementujące szereg operacji statycznych ułatwiających pracę z kolekcjami i tablicami. Różnych ciekawych metod jest tam kilka, jednak z perspektywy egzaminu SCJP interesują nas głównie operacje sort(…) i binarySearch(…) zaimplementowane w wielu wersjach w obydwu klasach.

Operacja sort(…) z klasy Arrays służy do sortowania tablic a analogiczna operacja zaimplementowana w klasie Collections do sortowania list. Podstawowa wersja operacji sort(…) z klasy Arrays akceptuje jeden argument – tablicę wartości prymitywnych lub tablicę obiektów. Mamy także wersje sortujące tylko pewien fragment tablicy. Wtedy jako drugi argument wywołania podajemy indeks tablicy od którego (włącznie) należy zacząć sortowanie a jako trzeci argument indeks końca (nie włącznie) sortowanego fragmentu. Mamy więc do dyspozycji następujące funkcje:

  // dla tablic byte[], short[], int[], long[], float[], double[], char[] i Object[]
public static void sort(int[] tab)

// dla tablic byte[], short[], int[], long[], float[], double[], char[] i Object[]
public static void sort(int[] tab, int fromIndex, int toIndex)

Operacje te sortują elementy tablicy w porządku rosnącym. Oczywiste jest, jak wygląda ten porządek w przypadku liczb – znaki (tablica char[]) sortowane są alfabetycznie – co jednak z sortowaniem tablicy obiektów? Żeby posortować elementy musimy wiedzieć, jak porównać je między sobą (który z dwu obiektów jest większy?) – obiekty w sortowanej tablicy muszą być więc porównywalne. W języku Java obiekty są porównywalne jeśli implementują interfejs Comparable<T>. Interfejs ten definiuje jedną metodę:

int compareTo(T obj) 

Metoda ta zwraca liczbę ujemną, jeśli obiekt dla którego ją wywołano jest mniejszy od obiektu przekazanego jako argument; liczbę 0 jeśli obiekty są równe; liczbę dodatnią jeśli jest większy.

Interfejs Comparable<T> implementowany jest przez wiele klas z Javy SE, w tym przez wszystkie klasy opakowujące typów prostych (np. Integer, Double, Character) a także klasy Calendar, Date, Time czy String.

A co jeśli chcemy posortować tablicę zgodnie z innym porządkiem niż rosnący? A może tablicę Stringów chcemy posortować inaczej niż leksykograficznie (standardowy porządek dla Stringów to porządek leksykograficzny)? Może ważniejsza jest dla nas w danym zastosowaniu długość Stringa? A może chcielibyśmy posortować ignorując wielkość liter? Da się? Da! Wystarczy zdefiniować interesujący nas porządek implementując interfejs Comparator<T>. Klasa Arrays oprócz dwu wersji operacji sort(…) pokazanych powyżej implementuje jeszcze dwie wersje różniące się tym, że jako kolejny argument wywołania dodajemy obiekt implementujący interfejs Comparator<T>, który to określa porządek sortowanych elementów. Poniżej przykład.

public class TestClass {
public static void main(String[] args) {
String[] strings = { "Urszula", "Ola", "Agata", "Jola" };

Arrays.sort(strings, new LengthComparator());

for(String s : strings)
System.out.print(s + " ");
}
}

// określa sortowanie Stringów od najkrótszego do najdłuższego
class LengthComparator implements Comparator<String> {
public int compare(String strA, String strB) {
return strA.length() - strB.length();
}
}

Interfejs Comparator<T> wymaga implementacji metody compare(…), która zwraca liczbę ujemną, jeśli pierwszy argument wywołania jest mniejszy niż drugi; liczbę 0, jeśli są równe; liczbę dodatnią jeśli pierwszy argument jest większy. Efektem uruchomienia powyższego programu będzie wyświetlenie napisu:
Ola Jola Agata Urszula

Klasa Collections implementuje dwie wersje metody sort(…). Pierwsza służy do sortowania listy elementów które są porównywalne same z siebie, tj. implementują interfejs Comparable<T> zaś druga do sortowania listy dowolnych obiektów, ale za to musimy określić porządek sortowania przekazując jako drugi argument wywołania instancję komparatora, tj. instancję klasy implementującej interfejs Comparator<T>. Sygnatury tych metod wyglądają nieco odstraszająco i są następujące:

public static <T extends Comparable<? super T>> void sort(List<T> list)

public static <T> void sort(List<T> list, Comparator<? super T> comp)

Wyszukiwanie w kontekście list i tablic oznacza szukanie odpowiedzi na pytanie, czy dana lista lub tablica zawiera zadany element i jeśli tak to na której pozycji. Wyszukiwanie binarne implementowane przez metody binarySearch(…) z klasy Collections oraz Arrays to algorytm, który działa tylko dla kolekcji (list i tablic) posortowanych. Wyszukiwanie z zastosowaniem tego algorytmu działa o rząd wielkości szybciej niż wyszukiwanie liniowe (tj. przeglądanie listy element po elemencie) ale musimy pamiętać, że można go użyć tylko i wyłącznie do wyszukiwania w listach lub tablicach posortowanych. Co więcej, użycie algorytmu wyszukiwania binarnego dla listy czy tablicy nie posortowanej nie spowoduje błędu kompilacji czy wyjątku. Metoda binarySearch(…) wykona się, tyle że otrzymamy wynik nie zgodny ze stanem faktycznym.

W klasie Collections zaimplementowano dwie wersje metody binarySearch(…). Analogicznie jak w przypadku sortowania, pierwsza z nich służy do wyszukiwania z listy elementów, które są porównywalne a więc implementują interfejs Comparable<T>. Druga wersja służy do wyszukiwania z listy elementów, które same z siebie nie są porównywalne a więc musimy przekazać także komparator – obiekt, który będzie używany do porównywania – a więc obiekt implementujący interfejs Comparator<T>. Sygnatury tych metod są następujące:

public static <T> int binarySearch(List<? extends Comparable<? super T>> list,
T key)

public static <T> int binarySearch(List<? extends T> list,
T key,
Comparator<? super T> comp)

Drugiej wersji metody – tej w której przekazujemy także komparator – należy użyć także w przypadku, gdy lista posortowana jest zgodnie z porządkiem innym niż naturalny (określony przez metodę compareTo(…) interfejsu Comparable<T>). W szczególności, jeśli listę sortowaliśmy z użyciem konkretnego komparatora to do wyszukiwania binarnego powinniśmy użyć tego samego komparatora.

W klasie Arrays zaimplementowano po jednej metodzie dwuargumentowej dla tablicy każdego typu prostego i dla tablicy obiektów. Pierwszym argumentem jest tablica a drugim szukany element. Zaimplementowano także metodę, która akceptuje komparator. Sygnatury tych metod są następujące:

  // dla tablic byte[], short[], int[], long[], float[], double[], char[] i Object[]
public static int binarySearch(int[] a, int key)

public static <T> int binarySearch(T[] a,
T key,
Comparator<? super T> comp)

31 października 2008

Kolorowanie kodu poprawia czytelność artykułów

Jak pokazują wyniki mojej ostatniej ankiety, zatytułowanej „Czy kolorowanie kodu poprawia czytelność artykułów?”, warto poświęcić chwilkę i zadbać o oprawę graficzną prezentowanych na swoim blogu fragmentów programów. W ankiecie trwającej przez 30 dni oddano 123 głosy. Wyniki ankiety poniżej.


No dobrze. Wiemy więc że warto, pytanie tylko jak to zrobić, żeby się nie narobić? Ja używam Google Code Prettify o którym dowiedziałem się od Jacka Ciereszko. Więcej na ten temat na jego blogu, w artykule „Formatowanie kodu w blogger”.

28 października 2008

SCJP - Statyczne klasy zagnieżdżone

Statyczne klasy zagnieżdżone (ang. static nested classes) to jeden z niewielu tematów, które w książce „SCJP Sun Certified Programmer for Java 5 Study Guide (Exam 310-055)” są mocno niedopowiedziane - w jaskrawym przeciwieństwie do wielu innych, znacznie banalniejszych (choć pewnie ważniejszych) tematów, które są zdecydowanie przegadane, albo wręcz zagadane. Postaram się dziś wypełnić tę lukę, choć mając na uwadze ostateczny cel, tj. przygotowanie do SCJP powstrzymam się od wchodzenia w temat zbyt głęboko. Lubiącym poczytać po angielsku (oraz lubiącym wiedzieć więcej) polecam artykuł „Nested Classes, Part 3”. Przy okazji, przestrzegam czytelników wspomnianej książki o SCJP, że to co napisano we wstępie do rozdziału o klasach zagnieżdżonych jest nieprawdą. Proszę przeczytać więc poniżej zacytowany fragment tejże i zapamiętać, że tak nie jest. Inaczej niż piszą autorzy - klasy zagnieżdżone znajdują się w specjalnym związku z klasami zewnętrznymi (ang. special relationship), o czym za chwilę.

“While an inner class (…) enjoys that special relationship with the outer class (…), a static nested class does not. It is simply a non-inner (also called "top-level") class scoped within another. So with static classes it's really more about name-space resolution than about an implicit relationship between the two classes.”

Czy klasy wewnętrzne (ang. inner classes) i klasy zagnieżdżone (ang. nested classes) to to samo, tylko inaczej nazwane? Jak można się domyślać - nie! Podobnie jak klasę wewnętrzną, klasę zagnieżdżoną definiujemy wewnątrz innej klasy – klasy zewnętrznej – jednak klasy zagnieżdżone, w odróżnieniu od wewnętrznych są to klasy statyczne. Zobaczmy na przykładzie, jak to wygląda.
class Notepad {
private static Date lastCreatedDate;

static class Note {
private Date creationDate;

public Date getCreationDate() {
return creationDate;
}

public Date getLastCreatedDate() {
return lastCreatedDate; // statyczna zmienna prywatna klasy zewnętrznej
}
}

public static Note newNote() {
Note art = new Note();

art.creationDate = new Date(); // zmienna prywatna z klasy wewnętrznej
lastCreatedDate = art.creationDate;

return art;
}
}

Klasa ‘Notepad’ jest w naszym przypadku klasą zewnętrzną a ‘Note’ klasą zagnieżdżoną. Zauważmy, że deklarację klasy ‘Note’ poprzedzono modyfikatorem ‘static’. To właśnie dodając to słówko kluczowe mówimy, że chodzi nam o klasę zagnieżdżoną a nie wewnętrzną. Podobnie jak klasę wewnętrzną, klasę zagnieżdżoną łączy z klasą zewnętrzną pewien szczególny związek, związek oparty na pełnym zaufaniu, a więc taki, który pozwala na dostęp do zmiennych prywatnych swojego partnera. Zaufanie działa w obie strony. Zarówno klasa ‘Note’ ma dostęp do zmiennych prywatnych klasy ‘Notepad’ (zmienna ‘lastCreatedDate’) jak i klasa ‘Notepad’ do zmiennych prywatnych klasy ‘Note’ (zmienna ‘creationDate’). Zwróćmy jednak szczególną uwagę na fakt, że klasy zagnieżdżone to klasy statyczne – a więc nie związane z żadnym obiektem klasy zewnętrznej – stąd mogą one odwoływać się tylko i wyłącznie do zmiennych (i metod) statycznych z tejże klasy zewnętrznej. Jest to sytuacja zupełnie analogiczna do tej w której jesteśmy implementując metodę statyczną, chociażby metodę ‘main(…)’. Możemy wówczas używać jedynie innych statycznych metod bądź statycznych zmiennych. Zmiennych instancyjnych użyć nie możemy, bo przecież nie mamy żadnej instancji!

Zanim przejdziemy dalej zastanówmy się w jakich okolicznościach ta nieco dziwna konstrukcja językowa może się przydać. Szczęśliwie, bardzo dobry przykład jest na wyciągnięcie ręki; zerknijmy na interfejs ‘java.util.Map<K,V>’, a szczególnie na typ wyniku metody ‘entrySet()’. Jest to ‘Set<Map.Entry<K,V>>’. A cóż to takiego ‘Map.Entry<K,V>’? Uwaga, uwaga… statyczny interfejs zagnieżdżony! Tak, zagnieżdżone mogą być także interfejsy i to zarówno w interfejsach jak i w klasach. Generalnie rzecz biorąc, zagnieżdżać można wszystko i we wszystkim, nawet klasy w interfejsach. Interfejs ‘java.util.Map<K,V>’ implementuje klasa ‘java.util.AbstractMap<K,V>’ (która z kolei jest nadklasą klasy ‘java.util.HashMap<K,V>’) a interfejs zagnieżdżony ‘Map.Entry<K,V>’ klasa zagnieżdżona ‘AbstractMap.SimpleEntry<K,V>. Na drzewku dziedziczenia wygląda to tak:

Jak widać z powyższych przykładów, jeśli klasy zagnieżdżonej używamy poza klasą w której została ona zdefiniowana to musimy nazwę tejże poprzedzić nazwą klasy zewnętrznej, zupełnie jakby klasa zewnętrzna była pakietem, w którym zlokalizowano klasę zagnieżdżoną. Pełną nazwą klasy ‘Note’ jest więc ‘Notepad.Note’. Poniżej przykład, pokazujący jak należy identyfikować klasy zagnieżdżone, w zależności od stopnia zagnieżdżenia klasy i umiejscowienia kodu, który tej klasy używa.
class ExternalClass {
void externalTest() {
Notepad.Note.Line line = new Notepad.Note.Line();
}
}

class Notepad {
static class Note {
static class Line {

}

void noteTest() {
Line line = new Line();

// można też tak - za dużo nie ma nigdy
Notepad.Note.Line nextLine = new Notepad.Note.Line();
}
}

void notepadTest() {
Note.Line line = new Note.Line();
}
}

Istnieje też opcja alternatywna do każdorazowego podawania pełnej – poprzedzonej nazwą klasy zewnętrznej – nazwy klasy zagnieżdżonej. Wystarczy klasę zagnieżdżoną zaimportować; tak jak pokazano na poniższym przykładzie.
import my.pckg.Notepad.Note.Line;

class ExternalClass {
void externalTest() {
Line line = new Line();
}
}

class Notepad {
static class Note {
static class Line {

}
}
}

Poza tym co napisano powyżej zagnieżdżone klasy i interfejsy żądzą się tymi samymi regułami co klasy „normalne”, w szczególności, klasy i interfejsy zagnieżdżone mogą być deklarowane jako ‘public’, ‘protected’ czy ‘private’.

17 października 2008

Pierwsze spojrzenie na Blogger Data API

Blogger Data API to część Google Data API (w skrócie GData). Do czego ono służy? Mówiąc najprościej do interakcji (np. odczytywanie i publikowanie artykułów) z blogami hostowanymi przez Bloggera, czyli np. takimi jak mój. API dostępne jest dla wielu (no… dla kilku) języków programowania i zbudowane jest ponad wspólnym protokołem zdefiniowanym przez Google na bazie Atoma, RSSa i czegoś tam jeszcze. No ale nie wnikajmy za nadto w szczegóły – nas interesuje Java a więc fragment dokumentacji zatytułowany „Developer's Guide: Java”.

Naszą przygodę z Blogger Data API zaczynamy od pobrania biblioteki, tj. GData Java Client (plik gdata.java-1.13.0.zip). Pobrany plik rozpakowujemy w dogodnej lokalizacji.

Google przygotowało także wtyczkę (ang. plugin) do Eclipse’a wspierającą programowanie z GData i choć szczerze powiem nie robi ona zupełnie nic ciekawego, to jednak użyję jej, aby pokazać że jest. Więcej informacji o wtyczce na stronie GData Java Client Eclipse Plugin. Niecierpliwi mogą od razu przystąpić do instalacji używając update-site’u http://gdata-java-client-eclipse-plugin.googlecode.com/svn/update-site. Instalujemy więc wtyczkę i tworzymy nowy projekt, jak pokazano poniżej.


W oknie szczegółów wpisujemy nazwę projektu, wybieramy jego typ, tj. ‘Blogger’ oraz wskazujemy lokalizację pobranych uprzednio bibliotek, tj. podkatalogu \java\lib katalogu który powstał po rozpakowaniu pobranego archiwum GData. Zauważmy, że w opisie typu projektu ‘Blogger’ podana jest informacja, że nie ma zewnętrznych zależności („External dependencies required: None”) ale nie jest to prawdą. Na dole okna widzimy opcje sugerujące, że wtyczka jest w stanie samodzielnie pobrać te zależności (JavaMail i JAF) jeśli tylko ją o to poprosimy, ale moje eksperymenty wskazują, że to nie działa (pewnie dlatego, że wtyczka myśli że tych zależności nie ma). Zależności pobierzemy więc później samodzielnie, a póki co klikamy ‘Finish’ i tworzymy projekt.


To co robi zaprezentowana wtyczka to utworzenie zwykłego projektu typu Java, dodanie odpowiednich bibliotek GData do ścieżki budowania (dla projektu typu ‘Blogger’ jest to plik gdata-client-1.0.jar) i wygenerowanie klasy, która łączy się z Blogger’em i pobiera listę blogów zarejestrowanych przez danego użytkownika (identyfikator i hasło podajemy z linii poleceń). Wtyczka dodaje też do ścieżki budowania aplikacji (zbędną) bibliotekę wtyczki (sic)! Struktura naszego projektu jest jak pokazałem poniżej. Na dobry początek polecam eksperymenty z przeklejaniem kodu przykładów ze wspomnianego już rozdziału dokumentacji „Developer's Guide: Java”. Dodam jeszcze, że do odczytu artykułów nie jest wymagane uwierzytelnienie (usuwamy wywołanie funkcji setUserCredentials(…)).

15 października 2008

SCJP - Klasy StringBuffer i StringBuilder

Kilka dni temu – w artykule „SCJP - Klasa String” – pisałem o klasie String. Dziś będzie o dwu kolejnych klasach używanych do operacji na napisach; klasie StringBuffer i StringBuilder.

Klasa String reprezentuje sekwencję znaków, typ napisowy. Jak napisałem w poprzednim artykule, obiekty klasy String są niemodyfikowalne (ang. immutable); każda operacja, w wyniku której chcemy otrzymać inny napis na bazie istniejącego wymaga utworzenia nowego obiektu, do reprezentowania tego nowego napisu. Jest to coś, czego chcielibyśmy uniknąć. Potrzebujemy więc typu, który reprezentował by modyfikowalną sekwencję znaków i właśnie tym jest klasa StringBuffer. Aby zapewnić bezpieczne przetwarzanie wielowątkowe (ang. thread-safe) operacje klasy StringBuffer są synchronizowane. Ułatwia to znakomicie programowanie ale w sytuacji gdy wiemy, że nasza aplikacja nie będzie wykonywana przez więcej niż jeden wątek synchronizacja jest zbędna i wprowadza bezsensowne straty wydajnościowe. Z tego powodu do specyfikacji Java SE w wersji 5 dodano klasę StringBuilder, która różni się od klasy StringBuffer tylko tym, że jej metody synchronizowane już nie są. Poniżej przedstawiam metody tych klas – te ważne z perspektywy egzaminu SCJP – na przykładzie klasy StringBuilder, ale jak wiemy klasa StringBuffer ma metody analogiczne, tyle, że synchronizowane.

public StringBuilder append(String str) - Metoda ta zmodyfikuje łańcuch znaków reprezentowany przez obiekt dla którego ją wywołano w ten sposób, że do jego końca doklei napis przekazany jako argument. Metoda ta ma wiele wariantów różniących się typem parametru; istnieją między innymi wersje dla parametrów typu: int, char, boolean czy Object. Metoda ta zwraca obiekt dla którego została wywołana (nie nowy obiekt jak w klasie String) aby umożliwić wywołania łańcuchowe (ang. chained invocations). Uruchomienie metody
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder();

System.out.println(stringBuilder.append("mam ").append(5).append(" lat"));
}
spowoduje wyświetlenie napisu ‘mam 5 lat’.

public StringBuilder delete(int start, int end) - Metoda ta zmodyfikuje obiekt dla którego ją wywołano w ten sposób, że usunie ciąg znaków określony argumentami wywołania. Pierwszy argument to pozycja pierwszego znaku który ma być usunięty (jak zawsze liczymy od 0-ra) a drugi to pozycja pierwszego znaku który usunięty nie będzie. Metoda ta także zwraca obiekt dla którego została uruchomiona. Uruchomienie metody
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder("mam 55 lat");

System.out.println(stringBuilder.delete(4, 5));
}
spowoduje wyświetlenie napisu ‘mam 5 lat’.

public StringBuilder insert(int offset, String str) - Metoda ta zmodyfikuje obiekt dla którego ją wywołano w ten sposób, że na pozycji określonej pierwszym argumentem wywołania zostanie wklejony String przekazany jako drugi argument. Analogicznie jak metoda append(..) metoda insert(…) występuje w wielu odmianach, akceptujących jako drugi argument typy takie jak (między innymi): int, float, char, Object. Zwrócony obiekt to obiekt dla którego wywołano metodę. Uruchomienie programu
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder("mam 5 lat");

System.out.println(stringBuilder.delete(4, 5).insert(4, "niewiele"));
}
spowoduje wyświetlenie napisu ‘mam niewiele lat’.

public StringBuilder reverse() – Metoda ta odwraca kolejność znaków w obiekcie dla którego ją wywołano. Tak jak dla innych metod, zwrócony obiekt to obiekt dla którego wywołano metodę. Uruchomienie programu
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder("abcdef");

System.out.println(stringBuilder.reverse());
}
spowoduje wyświetlenie napisu ‘fedcba’.

13 października 2008

SCJP - Klasa String

Kolejny zastrzyk wiedzy przed egzaminem SCJP – kontynuacja dzieła zapowiedzianego w artykule „Przygotowania do SCJP czas zacząć”. Dziś będzie o typie napisowym reprezentowanym przez klasę String.

Zacznijmy od rozwiania ewentualnych wątpliwości natury fundamentalnej. Otóż typ napisowy w języku Java nie jest typem prostym. Co prawda przywykliśmy do tego, że obiekty tworzymy z użyciem operatora ‘new’, a więc inaczej niż zazwyczaj tworzone są String’i, ale nie zmienia to faktu, że każdy String jest obiektem. Zerknijmy na poniższy kod.
public class MyClass {
public static void main(String[] args) {
String a = new String("Ola ma kota"); // oczywiście można tak

String b = "A ula psa"; // ale możemy użyć także skróconej formy

System.out.print(b.getClass());
}
}

Uruchomienie tego programu spowoduje wyświetlenie napisu „class java.lang.String”. Czy jednak obydwa pokazane powyżej sposoby utworzenia obiektu napisu są równoważne? Otóż nie. Konstrukcja ‘new String(…)’ zawsze skutkuje utworzeniem nowego obiektu, natomiast forma skrócona, z bezpośrednim użyciem literału spowoduje utworzenie nowego obiektu tylko wówczas, gdy obiekt reprezentujący dany napis jeszcze nie istnieje. A gdzie ma istnieć? Otóż Wirtualna Maszyna Javy utrzymuje pulę String’ów – w specjalnym, przeznaczonym do tego celu obszarze pamięci – z nadzieją, że przyczyni się to do optymalizacji czasu wykonania i ilości potrzebnej pamięci. Pierwsze użycie literału napisowego spowoduje utworzenie nowego obiektu oraz umieszczenie go w owej puli, zaś każde następne użycie odnosić się będzie do tegoż właśnie obiektu z puli (kolejny obiekt nie jest tworzony). Przekonajmy się o tym analizując poniższy program,
public class MyClass {
public static void main(String[] args) {
String a = "Ola";
String b = "Ola";

System.out.println("a == b : " + (a == b));

String c = new String("Ola");
String d = new String("Ola");

System.out.println("c == d : " + (c == d));
}
}

który uruchomiony wyświetli napis
a == b : true
c == d : false

Zatem zgadza się. Referencja ‘a’ i ‘b’ wskazują na ten sam obiekt (wyrażenie ‘a == b’ ma wartość ‘true’) zaś ‘c’ i ‘d’ już nie (wyrażenie ‘c == d’ ma wartość ‘false’).

Kolejnym ważnym faktem odnośnie obiektów klasy String jest, że są one niemodyfikowalne (ang. immutable). Obiekt klasy String reprezentuje pewien stały, dokładnie jeden napis. Jeśli potrzebujemy obiektu, który reprezentuje inny napis, to najzwyczajniej w świecie potrzebujemy nowego obiektu klasy String. Zerknijmy na poniższy przykład,
public class MyClass {
public static void main(String[] args) {
String a = "Ola";
String b = a;

System.out.println("a == b : " + (a == b));

a += " ma kota";

System.out.println("a == b : " + (a == b));
}
}

który uruchomiony wyświetli napis
a == b : true
a == b : false

Prześledźmy teraz, jak to się dzieje. W pierwszej linii deklarujemy referencję ‘a’ i przypisujemy jej nowo utworzony obiekt reprezentujący napis „Ola”. Obiekt ten ląduje w puli obiektów o której mówiliśmy przed chwilą. Następnie deklarujemy referencję ‘b’ i przypisujemy jej ten sam obiekt, który przypisaliśmy do zmiennej ‘a’. Nic więc dziwnego, że pierwszą linią wyświetloną przez program jest ‘a == b : true’. Następnie, do zmiennej ‘a’ chcemy dodać sufiks „ ma kota”. Wiemy, że obiekty klasy String są niemodyfikowalne a więc operator ‘+=’ nie może zmienić wartości obiektu wskazywanego przez zmienną ‘a’. Co zatem się dzieje? Otóż tworzony jest nowy obiekt inicjowany wartością „Ola ma kota” i to ten właśnie obiekt jest przypisywany do zmiennej ‘a’. Zmienna ‘a’ wskazuje zatem teraz na nowy, zupełnie inny obiekt niż zmienna ‘b’, stąd porównanie ‘a == b’ ma wartość ‘false’.

String to klasa zupełnie podstawowa, stąd niezbędna jest – także z perspektywy egzaminu SCJP – znajomość jej najpopularniejszych metod. Poniżej zaprezentowano listę tychże, wraz z opisem i przykładem wywołania.

public char charAt(int index) - zwraca znak znajdujący się na zadanej argumentem wywołania pozycji w String’u, przy czym należy pamiętać, że tak jak tablice, indeks zaczyna się od liczby 0, tj. wyrażenie
"abcdef".charAt(1) 
ma wartość ‘b’

public String concat(String sufix) - zwraca nowy obiekt typu String, który ma wartość taką jak obiekt dla którego wywołano metodę z doklejonym jako sufiks argumentem wywołania, np. wyrażenie
"abcdef".concat("ghij")
ma wartość ‘abcdefghij’

public boolean equalsIgnoreCase(String str) – metoda pozwala stwierdzić, czy porównywane napisy (ten, dla którego wywołano metodę i ten przekazany jako argument) są takie same (leksykograficznie), przy czym przy porównywaniu ignoruje się „wielkość liter” (litery małe i duże są utożsamiane), np. wyrażenie
"abc".equalsIgnoreCase("AbC")
ma wartość ‘true’

public int length() - metoda ta zwraca długość napisu reprezentowanego przez obiekt, tj. ilość jego znaków, np. wyrażenie
"abc".length()
ma wartość ‘3’

public String replace(char old, char new) - metoda zwraca nowy obiekt typu String, powstały poprzez zastąpienie (w String’u dla którego wywołano metodę) każdego wystąpienia znaku przekazanego jako pierwszy argument, przez znak przekazany jako drugi argument, np. wyrażenie
"ababab".replace('b', 'c') 
ma wartość ‘acacac’

public String substring(int begin) - zwraca String’a powstałego po odrzuceniu początkowych znaków ze String’a dla którego wywołano metodę; pierwszym znakiem zwracanego String’a będzie ten, który znajduje się na pozycji przekazanej jako argument wywołania, np. wyrażenie
"abcdef".substring(1) 
ma wartość ‘bcdef’

public String substring(int begin, int end) - zachowuje się jak metoda opisana powyżej, z tym, że obcina także końcówkę napisu; wartość przekazana jako drugi argument wywołania to indeks pierwszego znaku który nie znajdzie się w zwracanym String’u, np. wyrażenie
"abcdef".substring(1, 3) 
ma wartość ‘bc’

public String toLowerCase() - zwraca String’a, który powstał po zamienieniu wszystkich dużych liter String’a dla którego wywołano metodę na odpowiadające litery małe, np. wyrażenie
"aBcDe".toLowerCase()
ma wartość ‘abcde’

public String toUpperCase() - metoda analogiczna do metody opisanej powyżej, z tym, że litery małe zamienia na duże a nie na odwrót, np. wyrażenie
"aBcDe".toUpperCase()
ma wartość ‘ABCDE’

public String trim() - zwraca napis powstały ze Stinga użytego do wywołania metody po obcięciu białych znaków znajdujących się na początku i końcu tegoż Stringa, np. wyrażenie
"    przed tabulacja, po spacja ".trim()
ma wartość ‘przed tabulacja, po spacja’

8 października 2008

SCJP - Metody equals() i hashCode()

W ostatnim artykule z serii o SCJP – „SCJP - Serializacja” – zapowiedziałem powrót do nieco intensywniejszego trybu pracy… no i jestem. Ostatnio publikowałem 28 września, także zgodnie z obietnicą jest częściej niż co miesiąc. Dziś będzie o metodach equals() i hashCode().

Zacznijmy od podstaw, a mianowicie – do czego właściwie służy metoda equals()? Przecież mamy operator równości ‘==’? Mówiąc najprościej, operator równości służy do sprawdzenia, czy dwa obiekty są dokładnie tym samym (tym samym obiektem), zaś metoda equals() odpowiada na pytanie, czy dwa obiekty są „takie same”. Sformułowanie „takie same” ująłem w cudzysłów, bowiem co ono oznacza zależy tylko od naszego uznania, a mówiąc ściślej, właśnie od implementacji metody equals(). Metoda equals() jest zdefiniowana w klasie java.lang.Object w ten sposób, że jest tożsama z operatorem równości ‘==’, ale w wielu przypadkach nie jest to implementacja właściwa. Dla przykładu, w klasie java.lang.Integer metodę tę nadpisano w ten sposób, że dwa obiekty ‘x’ i ‘y’ są uznawane za „takie same” (‘x.equals(y) == true’), jeśli reprezentują tę samą wartość liczbową, tj. ‘x.intValue() == y.intValue()’.

Powiedzieliśmy sobie przed chwilą, że znaczenie sformułowania „takie same”, a więc implementacja metody equals(), mogą być dowolne i zależą wyłącznie od naszego uznania, ale nie do końca jest to prawdą. Metodę equals() obowiązuje bowiem kontrakt, którego musimy przestrzegać. Oto on:

- Relacja wyznaczona metodą equals() musi być zwrotna, tj. dla każdej zmiennej referencyjnej ‘x’ (różnej od ‘null’) wyrażenie ‘x.equals(x)’ ma wartość ‘true’.

- Relacja wyznaczona metodą equals() musi być symetryczna, tj. dla każdej pary zmiennych referencyjnych ‘x’ i ‘y’, wyrażenie ‘x.equals(y)’ ma wartość ‘true’ wtedy i tylko wtedy gdy ‘y.equals(x)’ ma wartość ‘true’. Chciałoby się powiedzieć, że ‘x.equals(y)’ musi dawać dokładnie ten sam wynik co ‘y.equals(x)’, ale tak nie jest. Jeśli bowiem ‘x’ wskazuje na pewien obiekt a ‘y’ ma wartość ‘null’, to ‘x.equals(y)’ ma wartość ‘false’ (musi być ‘false’, co jest kolejnym punktem kontraktu) a ‘y.equals(x)’ wywołuje wyjątek NullPointerException.

- Relacja wyznaczona metodą equals() musi być przechodnia, tj. dla dowolnych zmiennych referencyjnych ‘x’, ‘y’ i ‘z’, jeśli ‘x.equals(y)’ ma wartość ‘true’ oraz ‘y.equals(z)’ ma wartość ‘true’ to także ‘x.equals(z)’ musi mieć wartość ‘true’.

- Przy założeniu, że porównywane obiekty się w międzyczasie nie zmieniają, każdorazowe wywołanie funkcji ‘x.equals(y)’ musi dawać taki sam wynik, tj. albo zawsze ‘true’ albo zawsze ‘false’.

- Każdy obiekt musi być różny od wartości ‘null’, tj wywołanie ‘x.equals(null)’ musi zawsze zwracać wartość ‘false’.

Ściśle związaną z operacją equals() jest operacja hashCode(). Operację hashCode() również obowiązuje pewien kontrakt i to taki, który definiuje jej zachowanie w zależności od zachowania metody equals(). Stąd w typowej sytuacji, ilekroć nadpisujemy jedną z tych metod musimy nadpisać też i drugą. Oto kontrakt dla metody hashCode():

- Przy założeniu, że obiekt się w międzyczasie nie zmienia, każdorazowe wywołanie funkcji hashCode() musi zwracać, w ramach pojedynczego uruchomienia aplikacji, taką samą wartość. Co prawda wtrącenie, że „w ramach pojedynczego uruchomienia aplikacji” nie ma specjalnie sensu (inne uruchomienie, a więc inny obiekt), ale jest to wtrącone także w „oficjalnej” dokumentacji więc niech zostanie.

- Jeśli dwa obiekty ‘x’ i ‘y’ są „takie same”, tj. ‘x.equals(y)’ ma wartość ‘true’, to muszą one mieć takie same wartości skrótu (ang. hash code), tj. musi zachodzić ‘x.hashCode() == y.hashCode()’. Zauważmy, że implikacja działa tylko w jedną stronę, tj. jeśli obiekty „są różne” (tj. nie są „takie same”), to ich wartości skrótu wcale nie muszą być różne.

I na koniec jeszcze przykład. Zwróćmy szczególną uwagę na sygnatury metod. Upewnijmy się, że to co nam pokażą na egzaminie SCJP, to będą rzeczywiście nadpisania a nie przeciążenia.
public class MyClass {
private int someValue;

public int hashCode() {
return someValue;
}

public boolean equals(Object obj) {
if (obj == null || getClass() != obj.getClass())
return false;

if (someValue != ((MyClass) obj).someValue)
return false;

return true;
}
}

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.

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.

29 czerwca 2008

SCJP – Operatory

Ledwie zdążyłem wyrazić swą radość – w artykule "SCJP - Odśmiecacz, czyli mechanizm przywracania pamięci" – z powodu zakończenia przerabiania trzeciego rozdziału Książki, a tymczasem już kończymy rozdział czwarty. Zgadza się! Cały rozdział czwarty streszczony w jednym, dzisiejszym artykule. A będzie dziś o operatorach wszelkiej maści.

Operatory przypisania opisałem zasadniczo w artykule "SCJP - Operatory przypisania dla typów prymitywnych". Na egzaminie, oprócz zwykłego operatora przypisania ‘=’ występują podobno jeszcze tylko operatory ‘+=’, ‘-=’, ‘*=’ oraz ‘/=’. Jedyną rzeczą, jaką można w tym kontekście dodać jest, że prawa strona wyrażeń z operatorami przypisania jest zawsze wyliczana jako pierwsza, tak więc wyrażenie ‘x *= 1 + 2’ jest równoważne wyrażeniu ‘x = x * (1 + 2)’ a nie ‘x = x * 1 + 2’.

Operatory arytmetyczne, mogące pojawić się na egzaminie SCJP to ‘+’, ‘-‘, ‘*’, ‘/’ i ‘%’ czyli reszta z dzielenia oraz operatory inkrementacji i dekrementacji ‘++’ oraz ‘--‘. Na komentarz zasługuje operator ‘+’. Oprócz tego, że jest on operatorem dodawania, może także być stosowany jako operator konkatenacji String’ów. Pamiętajmy, że jeśli choć jeden z operandów jest String’iem, to ‘+’ nie jest operatorem dodawania, tylko operatorem konkatenacji. Pamiętajmy także o kolejności wykonywania działań. Dla przykładu, instrukcja

System.out.println("2 plus 2 to " + 2 + 2);

powoduje wyświetlenie napisu "2 plus 2 to 22". Ale już instrukcja

System.out.println("2 razy 2 to " + 2 * 2);

daje wynik oczekiwany, tj. "2 razy 2 to 4".

Pierwszeństwo mają zawsze operatory unarne, a więc ‘++’ i ‘--‘. Następnie wykonywane są operacje ‘*’, ‘/’ oraz ‘%’ a operatory ‘+’ i ‘-‘ wiążą najsłabiej. Poza tym, jeśli kolejność wykonywania działań nie jest w pełni wyznaczona przez pierwszeństwo operatorów to są one wykonywane od lewej do prawej. W wyrażeniu >>"2 plus 2 to " + 2 + 2<< jako pierwsza wykona się więc operacja konkatenacji >>"2 plus 2 to " + 2<< której wynikiem jest "2 plus 2 to 2". Następnie, wykonana będzie konkatenacja >>"2 plus 2 to 2" + 2<< a więc w rezultacie otrzymamy napis "2 plus 2 to 22". Ponieważ jednak operator ‘*’ wiąże silniej niż ‘+’, w wyrażeniu >>"2 razy 2 to " + 2 * 2<< w pierwszej kolejności zostanie wyliczona wartość dla >>2 * 2<<, a potem wykonana będzie konkatenacja >>"2 razy 2 to " + 4<<.

Na SCJP pojawiają się także operatory logiczne. Są to ‘&’, ‘|’ i ‘^’ – które są operacjami logicznymi dla operandów typu logicznego a operacjami bitowymi dla operandów typu całkowitoliczbowego – oraz ‘&&’, ‘||’ i ‘!’. Różnica między operatorami ‘&’ i ‘|’ oraz ‘&&’ i ‘||’ zastosowanymi do operandów typu logicznego jest taka, że te pierwsze wyliczają wartość wyrażeń logicznych zachłannie a te drugie leniwie.

Operatory relacyjne ‘<’, ‘<=’, ‘>’, ‘>=’ mogą być stosowane tylko do porównywania wartości typów numerycznych i typu ‘char’, przy czym za wartość dla typu ‘char’ przyjmuje się kod Unicode znaku. Operatory ‘==’ oraz ‘!=’ można stosować dodatkowo do porównywania wartości typu logicznego i typów referencyjnych.

Operator ‘instanceof’ służy do testowania, czy referencja odnosi się do obiektu danego typu. Referencja jest operandem lewostronnym a nazwa klasy bądź interfejsu prawostronnym. Mówiąc precyzyjniej, wynikiem wyrażenia ‘x instanceof SomeClass’ jest ‘true’ gdy instancja ‘x’ IS-A ‘SomeClass’. Co to oznacza IS-A opisywałem w artykule "SCJP - Związki typu IS-A oraz HAS-A". A co, jeśli ‘x’ ma wartość ‘null’? Czy w rezultacie otrzymamy ‘NullPointerException’? Nie, wyrażenie ‘null instanceof SomeClass’ ma wartość ‘false’ i nic złego się nie dzieje. Trzeba jeszcze zaznaczyć, że operatora ‘instanceof’ nie możemy użyć, jeśli nie ma szans na powodzenie, tj. jeśli typ referencji ‘x’ nie należy do tej samej hierarchii dziedziczenia co klasa ‘SomeClass’. Nie spełnienie tego warunku jest błędem czasu kompilacji. Przykład poniżej.

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

if(x instanceof Long) // błąd kompilacji!
System.out.println("cuda się zdarzają");
}
}

25 czerwca 2008

SCJP - Odśmiecacz, czyli mechanizm przywracania pamięci

Dzisiejszym artykułem zamkniemy wreszcie trzeci rozdział Książki – patrz artykuł "Przygotowania do SCJP czas zacząć". Jak sami autorzy piszą, był to rozdział monstrum; cieszę się więc, że mamy to już za sobą. Kolejny rozdział dla odmiany jest króciutki. Będzie okazja poprawić sobie humor, że nie idzie to aż tak wolno. Będzie dzisiaj o odśmiecaniu pamięci (ang. garbage collection).

Obiekty w Javie, tak jak wszystko w przyrodzie mają swój początek i koniec. Cykl jest prosty – tworzymy, używamy i… porzucamy. Zwróćmy uwagę na ostatnie słowo; porzucamy, ale nie niszczymy. Niszczeniem zajmuje się odśmiecacz (ang. garbage collector). Naturalnie, odśmiecacz niszczy tylko obiekty porzucone, a więc takie, które już nigdy nie będą mogły zostać użyte. Ktoś mógłby powiedzieć, że niepotrzebne obiekty to takie, na które nie wskazuje żadna referencja. Jest to prawda, ale tylko częściowa. Rzeczywiście, obiekty na które nie wskazuje żadna referencja są już bezużyteczne i podlegają odśmiecaniu, ale nie jest to warunek konieczny. Zerknijmy na poniższy przykład.

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

oneInstance.otherInstance = new SomeClass();

oneInstance = null;
}
}

class SomeClass {
public SomeClass otherInstance;
}

W pierwszej linii metody ‘main(…)’ tworzymy nową instancję klasy ‘SomeClass’ i przypisujemy ją do zadeklarowanej referencji. W drugiej linii tworzymy kolejny obiekt i przypisujemy go do zmiennej instancyjnej obiektu utworzonego uprzednio. Zauważmy, że póki co oba obiekty są osiągalne z aktywnego wątku aplikacji – pierwszy obiekt jest osiągalny bezpośrednio, poprzez referencję ‘oneInstance’ a drugi pośrednio. W trzeciej linii metody ‘main(…)’ przypisujemy do zmiennej ‘oneInstance’ wartość ‘null’. Tym samym powodujemy, że żadna z dwu utworzonych uprzednio instancji nie jest już osiągalna z aktywnego wątku aplikacji. Co prawda żadna referencja nie wskazuje już na obiekt utworzony jako pierwszy, ale na drugi z obiektów cały czas wskazuje referencja ‘otherInstance’. Mimo to, oba obiekty są już bezużyteczne i podlegają procesowi odśmiecania. Definicja obiektu porzuconego, a więc podlegającego procesowi odśmiecania jest więc taka, że jest to obiekt który nie jest bezpośrednio ani pośrednio osiągalny z żadnego aktywnego wątku wykonania.

Pozostało jeszcze odpowiedzieć na pytanie – kiedy uruchamiany jest proces odśmiecacza? Generalnie rzecz biorąc… nie wiadomo – zależy to od konkretnej implementacji JVM. Co prawda mamy do dyspozycji metodę ‘java.lang.Runtime.getRuntime().gc()’, która służy do zgłoszenia prośby o uruchamianie odśmiecania, ale to tylko tyle – zgłoszenie prośby. Wywołanie tej metody wcale nie musi zakończyć się podjęciem jakiejkolwiek akcji, jest to tylko drobna sugestia, którą kierujemy do JVM, jednak, gdy już JVM zdecyduje się na podjęcie akcji oczyszczania pamięci to robi to synchronicznie – proces kończy się, zanim nastąpi powrót z wywołania. Ekwiwalentem dla wywołania ‘java.lang.Runtime.getRuntime().gc()’ jest ‘java.lang.System.gc()’.

Ze względu na brak możliwości założenia czegokolwiek na temat sposobu działania i momentu uruchomienia odśmiecacza – oraz braku gwarancji, że dany obiekt w ogóle zostanie kiedykolwiek usunięty z pamięci – język Java nie oferuje znanych z skądinąd destruktorów. Oferuje jednak pewną namiastkę – metodę ‘finalize()’. Jest to metoda zdefiniowana w klasie ‘Object’. Nie jest to metoda finalna, więc może być nadpisana (ang. overridden) w dowolnej podklasie. Znaczenie tej metody jest zbliżone do destruktorów w tym sensie, że jest to metoda wywoływana automatycznie przez proces odśmiecacza tuż przez usunięciem obiektu z pamięci, tyle, że jest jedno ale. Spójrzmy na poniższy przykład.

public class ResurrectionClass {
public static Set<Object> collection;
}

class SomeClass {

@Override
public void finalize() {
ResurrectionClass.collection.add(this);
}
}

Kod ten jest jak najbardziej poprawny. Jako że metoda ‘finalize()’ to metoda jak każda inna, to możliwe jest umieszczenie w niej kodu, który powoduje, że dana instancja zaczyna być osiągalna z aktywnego wątku a więc nie może być usunięta. I nie jest. Nie czyni to jednak instancji tej klasy nieśmiertelnymi, bowiem metoda ‘finalize()’ wywoływana jest dla każdego usuwanego obiektu tylko i wyłącznie raz! Proces odśmiecania wywoła metodę ‘finalize()’ za pierwszym razem, gdy próbuje usunąć obiekt z pamięci i wtedy możliwe jest wykonanie operacji pokazanej powyżej, jednak już za drugim razem metoda ta nie jest wywoływana i obiekt jest nieuchronnie niszczony.