JAVA exPress > Archiwum > Numer 5 (2009-10-01) > SCJP w pigułce

SCJP w pigułce

Chyba każdy programista Java słyszał o certyfikacie SCJP. Wielu mniej go zdawało, a i oni pewnie mają do powiedzenia tylko tyle, że dobrze, iż mają to za sobą. W poniższym artykule zamierzam zaprezentować podsumowanie mojej lektury podręcznika przygotowującego do zdania tego egzaminu.

Słowem wstępu

Egzamin SCJP (Sun Certified Java Programmer) jest jednym z najlepiej rozpoznawalnych egzaminów certyfikacyjnych oferowanych przez firmę SUN. Nie tylko jako pierwszy stopień do zdawania kolejnych, ale również jako potwierdzenie znajomości Javy na dość wysokim poziomie. Dla osób, które pragną zawodowo zajmować się tworzeniem aplikacji w języku Java jest on zatem bardzo cennym atrybutem, cenniejszym tym bardziej, iż uznawanym na całym świecie.

Swoją przygodę z nauką pod kątem zdawania tegoż egzaminu zacząłem jakiś czas temu. Jako człowiek, który nie ukończył kierunku w minimalnym stopniu związanego z informatyką – egzamin ten miał być potwierdzeniem własnej wiedzy oraz umiejętności programistycznych. Moje przemyślenia z lektury kolejnych rozdziałów podręcznika autorstwa duetu Bates & Sierra publikowałem w formie cyklicznych wpisów na swoim blogu (http://www.chlebik.wordpress.com). Jednakże pomimo kategoryzacji oraz przejrzystości wpisów – brakowało jednolitej syntezy wszystkich zapisanych wskazówek. Co więcej, szereg testowych egzaminów dostępnych w internecie również miał wpływ na moją wiedzę, a tym samym zaowocowało to rozbudowaniem niektórych porad. Dzięki uprzejmości redaktorów Java exPress mam możliwość przedstawienia całości w formie ujednoliconej oraz bardziej rozbudowanej, niż miało to miejsce w formie periodycznych wpisów na blogu.

Poniższy artykuł to spisane zagadnienia, które sprawiły mi problemy (lub wzbudziły wątpliwości) podczas rozwiązywania testów zamieszczonych na końcu każdego z rozdziałów podręcznika, a także testów próbnych (tzw. mock exams). Wskazówki są momentami dość szczegółowe, ale to przecież nie wada – zawsze bowiem lepiej jest wiedzieć więcej, niż mniej. Całość podzieliłem na 10 części (tyle, ile jest rozdziałów w książce), a także na kilka rad ogólnych. Autorzy podręcznika mają bardzo miłą tendencję do wplatania w treść rozdziałów zabawnych wypowiedzi czy aforyzmów. Nie służy to tylko zabawie – jest to doskonała mnemotechnika, którą warto sobie przyswoić przy nauce czegokolwiek. Każdą część okrasiłem zatem tego typu cytatem (w oryginale), co mam nadzieję uprzyjemni lekturę. Do dzieła!

Porady ogólne:

Należy zawsze zwracać uwagę na poprawność każdego kawałka kodu! Wiem, że może to brzmieć śmiesznie, ale prawda jest taka, że paru błędów można by uniknąć przy bardziej skrupulatnej analizie dostarczonego listingu. Jak pokazuje doświadczenie – praca z IDE potrafi drastycznie obniżyć czujność programisty. Podstawowe czynności przy analizie otrzymanego kodu to:

  • sprawdzanie poprawności indeksów w tablicy, zwłaszcza dotyczy to parametrów przekazywanych do wywołania metody main() (z linii poleceń);
  • zwracanie uwagi na import stosownych klas. Mnie sytuacja ta niemile zaskoczyła w przypadku wyrzucanego wyjątku (bodajże IOException). Należy pamiętać (przynajmniej taka konwencja jest stosowana w podręczniku), iż kiedy listingi programów rozpoczynają się od linii z numerem 1, wówczas widzimy cały przedstawiony kod i możemy z dużą dozą prawdopodobieństwa założyć, że jest on poprawny. Jednakże kiedy widzimy tylko kod metody, a numeracja linii rozpoczyna się mniej więcej od 5, wówczas należy zwrócić uwagę czy czasem nie brakuje stosownej instrukcji importu;
  • czy wszystkie zmienne instancji zostały zainicjalizowane, a jeśli tak to czy na pewno są to określone wartości, czy też mają one przypisane wartości domyślne?
  • czy użyto poprawnych identyfikatorów. Należy pamiętać, że taki potworek ( $___$___$ ) rodem z najgorszych zaułków nasza-klasa.pl jest jak najbardziej poprawny – świetnie sprawdzi się jako identyfikator zmiennej;
  • rzutowanie i polimorfizm. Czy aby na pewno dany obiekt może zachowywać się w dany sposób (głównie dotyczy kolekcji, ale gdzie indziej też potrafi zaskoczyć)?
  • troszeczkę uwagi przy czytaniu treści zadania, a konkretniej zrozumienie o co jesteśmy tak naprawdę pytani. Otóż "compile and run" to nie to samo co "run", ani też to nie to samo, co "compile". Takie kruczki często są wykorzystywane do zaznaczenia różnic pomiędzy wersjami Javy (głównie w pytaniach poświęconych kompilatorowi).

Rozdział 1 – Deklaracje i kontrola dostępu

"As a Java programmer, you want to be able to use components from the Java API, but it would be great if you could also buy the Java component you want from "Beans 'R Us," that software company down the street."

Pętle for potrafią być momentami zwodnicze. Potencjalna kombinacja pułapek w przypadku deklaracji, bądź też wykonania jest całkiem spora. Zważmy na taki przypadek:

    for (int __x = 0; __x < 3; __x++);

Jest to konstrukcja jak najbardziej poprawna składniowo! Pętla jest pusta, stąd średnik na końcu linii. Użyte identyfikatory są jak najbardziej legalne. Za odpowiednią możemy również uznać poniższą instrukcję:

    for( int i = 0, y = 2; i < 10 && y < 11; ++i, y++ ) {
        System.out.println( " Zmienna i to: " + i + " zaś zmienna y to: " + y );
    }

Poprawnie zainicjowano zmienne i oraz y, drugie wyrażenie w pętli daje w wyniku wartość BOOL, zaś ostatecznie zwiększamy wartości obu zmiennych. Warto podkreślić, iż niezależnie od użytego operatora (++i czy i++) wartość zmiennej i będzie zwiększała się dopiero po każdorazowym wykonaniu ciała pętli.

W Javie 5 pojawiła się konstrukcja, która umożliwia przekazywanie do metody nieznanej zawczasu ilości parametrów konkretnego typu. Rozwiązanie to nazwano var-args i jest cudownym wręcz sposobem na zniszczenie nieznającego tej konstrukcji programisty. Użyty w pierwszym rozdziale przykład został wyjaśniony dogłębniej w rozdziale drugim, co jednakże nie przeszkodziło w zamieszczeniu pytania w rozdziale pierwszym.

    static void sifter(A[]... a2) { s += "1"; }
    static void sifter(B[]... b1) { s += "2"; }
    static void sifter(B[] b1)    { s += "3"; }
    static void sifter(Object o)  { s += "4"; }

Wywołania metod, w których deklaracjach wymieniono var-argsy są z definicji brane przy przeciążaniu na samiuteńkim końcu (chyba, że to jedyny parametr). I warto sobie tę prawdę wbić do głowy. Drugą dość istotną kwestią jest traktowanie tablic – jak wiadomo są one kontenerem wartości określonego typu, jednakże tablica sama z siebie jest również obiektem! Zatem gdybyśmy korzystając z powyższego przykładu wywołali metodę z tablicą jako parametrem, wówczas do zmiennej s dopisano by wartość "4".

Tablice. Nie potrzeba o nich zbyt rozwlekle pisać, jedyną dziwną rzeczą (zwracają na to uwagę autorzy podręcznika), jest możliwość ich udziwnionego tworzenia. Wszystkie poniższe instrukcje są jak najbardziej poprawne.

    Boolean [] tablica [];  // Utworzenie tablicy dwuwymiarowej
    Boolean[] tablica;      // Tablica jednowymiarowa
    Boolean tablica [];     // Jak wyżej

Rozdział 2 – Programowanie obiektowe

"[…] when the JVM invokes the true object's (Horse) version of the method rather than the reference type's (Animal) version—the program would die a horrible death. (Not to mention the emotional distress for the one who was betrayed by the rogue subclass.)"

Polimorfizm nie dotyczy metod statycznych. Weźmy dla przykładu ten fragment kodu:

    public class Tester {

        public static void main( String[] sd ) {
            BetaTester t = new BetaTester();
            Tester t2 = new BetaTester();

            t.zrobCos();
            t2.zrobCos();
        }

        public void zrobCos() {
            System.out.println(1);
        }
    }


    class BetaTester extends Tester {
        public void zrobCos() {
            System.out.println(2);
        }
    }

Na wyjściu programu otrzymamy wynik: " 2 2". Dlaczego? Gdyż za każdym razem zostanie wywołana nadpisana metoda zrobCos z klasy BetaTester. Jednakże teraz wystarczy zmienić jej deklarację (w obu klasach) na static i w tym momencie zwracane wartości będą się różniły. W przypadku metod statycznych – polimorfizm nie jest w tym momencie możliwy.

Polimorfizmu i nadpisywania metod ciąg dalszy. Należy pamiętać o sytuacji, w której jedna klasa rozszerzając drugą, dziedziczy po niej własności i metody (choć to raczej typowe dla klasy, prawda?). Jedna z metod zostaje przeciążona. Własność zaś domyślnie ma nadawaną inną wartość niż miała w klasie rodzicielskiej!. Co z tego wynika? (przykład z podręcznika)

    public class Tester {

        public static void main(String[] args) {
            new Tester().go();
        }

        void go() {
            Mammal m = new Zebra();
            System.out.println(m.name + m.makeNoise());
        }
    }

    class Mammal {
        String name = "siersc ";
        String makeNoise() {
            return "jakis odglos";
        }
    }

    class Zebra extends Mammal {
        String name = "pasy ";
        String makeNoise() {
            return "meczy";
        }
    }

Na wyjściu programu zobaczymy "sierść meczy". Jak zatem widać polimorfizm nie dotyczy nadpisywanych własności, albo tez innymi słowy – w przypadku nadpisywania pierwszeństwo ma typ obiektu, a nie referencji do niego.

Podczas egzaminu przyda się także znajomość tych oto pojęć:

  • kohezja (cohesion) – to inaczej "spoistość", termin głównie odnoszony do klasy. Dla celów zdania egzaminu wystarczy wiedzieć, że za tym pojęciem stoi dążenie do tworzenia klas skupionych ściśle na jednym określonym zadaniu;
  • zależność (coupling) – tłumaczenie może nie jest odpowiednie, jednakże jakoś nie przychodzi mi do głowy inne. Otóż coupling to wyznacznik zależności pomiędzy klasami lub modułami. Im niższy coupling, tym większa kohezja, i na odwrót! Im mniejsze zależności tym klasy bardziej autonomiczne i "niemieszające" we własnych implementacjach.

Rozdział 3 – Przypisania

"Again, repeat after me, 'Java is not C++.'"

Bloki inicjalizacyjne są tym zagadnieniem, które potrafi nieźle namieszać i ostatecznie doprowadzić do utraty punktów na zasadniczo banalnych pytaniach. Jak powszechnie wiadomo bloki inicjacyjne dzielimy na statyczne i instancyjne. Bloki statyczne, jak większość artefaktów języka oznaczonych statycznie – są wykonywane tylko raz – podczas ładowania klasy przez maszynę wirtualną. Bloki instancyjne wykonywane są przy tworzeniu każdej nowej instancji obiektu. W czym problem? W zwróceniu uwagi na dwie rzeczy:

  • kolejność wykonywania – bloki są wywoływane po wywołaniu wszystkich konstruktorów (także rodziców). W przypadku wystąpienia kilku z nich w jednej klasie pod uwagę brana jest kolejność (z góry do dołu);
  • podchwytliwe pytania – mieszanie trzech klas, w których jedna dziedziczy po drugiej zaś najstarsza z nich posiada bloki statyczne... bla, bla, bla. Takie pytania to esencja pytań o bloki inicjalizacyjne. Spójrzmy na taki kod:
    class A {

        {
            // inicjalizujemy zmienne
        }

    }

    class B extends A {

        static
        {
            // też coś robimy
        }
    }

    class C extends B {
        // tutaj metoda main, konstruktor, również statyczne bloki inicjalizujące
    }

Na powyższym przykładzie widać rzecz wyraźnie – klasa B dziedziczy po A, zatem w momencie kiedy kompilator napotka słowo kluczowe extends, ładuje klasę A do pamięci oraz wykonuje jej statyczne bloki inicjalizujące! Problem polega na tym, aby nie rozpędzić się i nie zauważyć problemu z tym, iż klasa A nie ma statycznych bloków inicjalizujących! Na takie "oczywiste oczywistości" bardzo łatwo się nabrać.

"Przyciemnianie zmiennych" (shadowing) to kolejna wariacja problemów z polimorfizmem. Rzecz jest dość prosta, wystarczy pamiętać o kilku istotnych zagadnieniach:

  • przy przekazywaniu prymitywów lub referencji do obiektu przekazujemy kopię – skutkuje to faktem, iż zmiany dokonywane na wartości prymitywnej dotyczą tylko ciała konkretnej metody – dokładniej kopii przekazanej wartości. Jeśli mowa o referencjach do obiektów to należy pamiętać o tym, iż możemy zmienić wskazywany obiekt, nie jesteśmy zaś w stanie zmienić samej referencji! Zatem w poniższym kodzie:
public function zrobCos( Klasa a ) {
    a.setWartosc( 2 );   // To jest dozwolone
    a = null;            // No ale to już nie
}
  • modyfikator final dotyczy referencji, nie wskazywanego obiektu!

Var-argsy po raz mam nadzieję ostatni. Metoda z parametrami w formie var-args zostanie wybrana tylko i wyłącznie wówczas, kiedy nie będzie żadnej innej metody dla pojedynczych parametrów! Oto mały przykład (z podręcznika):

    public class Bertha {

        static String s = "";
        public static void main(String[] args) {
                int x = 4; Boolean y = true; short[] sa = {1,2,3};
                doStuff(x, y);
                doStuff(x);
                doStuff(sa, sa);
                System.out.println(s);

        }

        static void doStuff(Object o) { s += "1"; }
        static void doStuff(Object... o) { s += "2"; }
        static void doStuff(Integer... i) { s += "3"; }
        static void doStuff(Long L) { s += "4"; }

    }

Co zobaczymy na wyjściu? "212" – istotną rzeczą do zapamiętania jest fakt, iż w przypadku poszerzania prymitywów (widening), mechanizm ten nie działa w połączeniu z autoboxingiem. Zatem wywołanie metody doStuff() z parametrem int nie może skorzystać z wywołania ostatniej deklaracji metody (dla parametru typu Long). Stąd wywołanie pierwszej wersji (z parametrem typu Object).

Garbage Collector (GC). Podchwytliwie można naciąć się na pytaniach o możliwość usunięcia obiektu z pamięci przez odśmiecacz. Należy bowiem pamiętać o tym, iż składowe klasy będące obiektami również liczą się jako obiekty, które posiadają prawo do własnego bytowania. Dlatego instancja poniższej klasy:

    public class Chlebik {

        String nick = "Chlebik";
        int    wiek = 25;

    }

Nie zostanie usunięta nawet po "zgubieniu" do niej referencji, jeśli składowa nick będzie wciąż dostępna. Dotyczy to zwłaszcza statycznych składowych klas.

Rozdział 4 – Operatory

"Having said all this about bitwise operators, the key thing to remember is this:
BITWISE OPERATORS ARE NOT ON THE EXAM!"

Operatory są zwodnicze! Przeczytać trzy razy, splunąć przez lewe ramię, obrócić się przez ramię prawe, przeczytać ponownie, zasada 4xZ (zapamiętać, zapamiętać, zapamiętać, zapamiętać).

Operatory inkrementacji (++) oraz dekrementacji (--) są używane "na bieżąco" nawet jeśli operują na tej samej zmiennej w jednym wierszu:

static int zmienna = 7;

public static void main(String[] args) {
    System.out.print( zmienna++ + " " + ++zmienna );
}

Powyższy kod w wyniku da "7 9".

Wartości ENUM można porównywać zarówno z użyciem operatora == jak i metody equals. Zawsze zwrócą to samo.

Operator && przy pomyślnym wyniku pierwszego argumentu przechodzi do drugiego. Jeśli jednakże pierwszy argument zwróci wartość FALSE, wówczas kończy działanie.

Rozdział 5 – Przepływ, wyjątki i asercje

"Using assertions that cause side effects can cause some of the most maddening and hard-to-find bugs known to man! When a hot tempered Q.A. analyst is screaming at you that your code doesn't work, trotting out the old 'well it works on MY machine' excuse won't get you very far."

Metoda main() może zadeklarować wyrzucanie wyjątków. Dlaczego by nie?

O ile w przypadku pętli for nie ma problemów z użyciem w roli licznika zmiennej zadeklarowanej wcześniej (np. zmiennej lokalnej), o tyle w przypadku rozszerzonej pętli for taka operacja spowoduje wygenerowanie błędu. Poniższy kod nie zadziała:

    Integer i = 1;
    for( i : jakasTablicaWartosciInteger ) {}

Jak widać zmienna wskazująca na kolejne elementy tablicy musi zostać zadeklarowana w ciele pętli.

Ciekawe są pytania o potencjalną możliwość przepełnienia stosu (StackOverflow). Należy pamiętać, że samo wywołanie nieskończonej pętli, która nie alokuje dodatkowej pamięci, nie spowoduje przepełnienia stosu. Zatem kod:

    for( int i = 0; i < 10; i++ ) {
        if( i == 9 ) i = 1;
    }

Będzie się wykonywał dopóki Google będzie miało swoje serwery i jeden dzień dłużej. Jeżeli jednakże zrobimy coś takiego:

    public class Tester {

        public static void main(String[] args) {
            new Tester().zrobCos();
        }

        void zrobCos() {
            zrobCos();
        }
    }

Zaowocuje pięknym StackOverflowError, gdyż kolejne wywołania metody wymuszają zarezerwowanie pewnej części pamięci dla swego działania.

Klasy nadpisujące metody ze swej klasy rodzicielskiej nie mogą deklarować szerszego zakresu generowanych wyjątków niż klasy rodzicielskie! Czyli jeśli metoda zrobCos() deklaruje, że wyrzuca IOException, wówczas jeśli klasa potomna chce nadpisać tę metodę nie może zadeklarować wyrzucenia wyjątku o typie Exception!

Jeśli zamiast standardowego formatu zapisu asercji zdecydujemy się na drugi (z możliwością wygenerowania na wyjście pewnej informacji), wystarczy, że drugi parametr asercji będzie zwracał jakikolwiek obiekt (choć bardziej rzecz w metodzie toString() ).

Rozdział 6 – Łańcuchy, parsowanie, I/O, formatowanie

"There are over 500 ISO Language codes, including one for Klingon ('tlh'), although unfortunately Java doesn't yet support the Klingon locale. We thought about telling you that you'd have to memorize all these codes for the exam... but we didn't want to cause any heart attacks."

Co ostatecznie podlega, a co nie podlega serializacji? Pytanie jest dość istotne, gdyż w grę wchodzi szereg czynników, na które dobrze jest zwracać uwagę. Rozpatrzmy zachowanie takiego kodu:

    class A {}

    class B extends A implements Serializable {
        A zmienna = new A();
        static int zmienna2 = 9;
        int transient zmienna3 = 1;
    }

W przypadku serializacji i deserializacji sprawa wygląda następująco. Na etapie kompilacji kodu nie zostaną wyłapane oczywiste błędy! Klasa A nie może być serializowana, co mimo to nie powoduje błędu kompilacji klasy B. W toku działania programu zostanie wygenerowany wyjątek, jednakże kompilacja się powiedzie.

Zmienna o identyfikatorze zmienna3 po procesie deserializacji nie otrzyma wartości 1, ale za to domyślną wartość dla prymitywu o typie int (czyli 0 ). Pozostając już przy deserializacji wypada również wspomnieć o tym, iż o ile klasa B podlega serializacji i jej odtworzenie nie powoduje uruchomienia konstruktora, o tyle zostanie wywołany konstruktor klasy rodzicielskiej (czyli A).

Na sam koniec zostawiłem wartości statyczne – należy pamiętać, że istnieją one niezależnie od instancji, zatem ich wartości nie są w ogóle "ruszane" przez proces serializacji/deserializacji.

Rozdział ten skupia się w znacznej mierze na API – ktoś kto miał okazję pracować z Javą przez dłuższy czas nie powinien mieć większych problemów. Początkujący mogą jednakże momentami napotkać ciekawe kwiatki. Ja natrafiłem na dwa:

  • niekonsekwencję w nazewnictwie metod ( mkdir(), ale createNewFile() )
  • metoda setMaximumFractionDigits() klasy NumberFormat dotyczy tylko jej metody format(), w przypadku metody parse() nie ma ona żadnego wpływu na wynik.

Rozdział 7 – Typy generyczne I kolekcje

"You're an object. Get used to it. You have state, you have behavior, you have a job. (Or at least your chances of getting one will go up after passing the exam.)"

Zanim w ogóle zacznę, pragnę poinformować, iż ten rozdział doczekał się u mnie na blogu dłuższego wpisu. Zatem poniższa lista jest tylko szybkim podsumowaniem – zachęcam do przeczytania całego artykułu.

Należy zapamiętać proste zależności między równością obiektów oraz ich hashcode. Jeżeli dwa obiekty są znaczeniowo te same (przy nadpisaniu metody equals), wówczas ich hashcodes muszą być takie same. Nie działa to jednakże w drugą stronę – jeśli dwa obiekty posiadają takie same hashcodes to wcale nie oznacza, iż są one znaczeniowo tożsame.

Pozostając w tematyce hashcodes – jak podają sami autorzy na potrzeby egzaminu można założyć, iż kiedy nie nadpisujemy metody hashcode w klasie, wówczas każdy obiekt jest inny – będzie posiadał inny hashcode.

Często w deklaracjach można napotkać takie oto krzaczki:

    List<List<Integer>> table = new List<List<Integer>>();

Co raczej nie zadziała, gdyż List jest interfejsem i nie za bardzo można tworzyć jego instancje. Nie zadziała również taki kod:

    List<List<Integer>> table = new ArrayList<ArrayList<Integer>>();

Nie zadziała z tej prostej przyczyny, iż w przypadku generyków deklaracja musi pokrywać się z tworzonym obiektem, pomimo faktu, iż ArrayList rozszerza List. Co więcej – ta sama sytuacja dotyczy metod. Jeśli ma zwracać np.: List<Number> to jeśli ostatecznie zwracamy ArrayList<Integer> - również nie da się takiego kodu skompilować.

Ciekawą klasą jest TreeSet. Kwestia konkretnie dotyczy dwóch rzeczy, choć wynikających z tego samego założenia. Klasa TreeSet posiada kilka różnych konstruktorów, których celem jest stworzenie zbioru elementów, które są uporządkowane w formie drzewa. Co z tego wynika? Ano tyle, iż elementy będące składnikami klasy muszą implementować interfejs Comparable, albo też "same z siebie" posiadać możliwości porównywania jednych z drugimi (np. klasa String). Jeśli do obiektu klasy TreeSet (bez wykorzystania typów generycznych) dodamy nawet kilka obiektów różnych klas kod skompiluje się bez problemu. Natomiast z całą pewnością dostaniemy błędy podczas uruchomienia programu – kompilator po prostu nie będzie wiedział z jakimi obiektami ma do czynienia i wyrzuci ClassCastException.

Rozdział 8 – Klasy wewnętrzne

"More important, the code used to represent questions on virtually any topic on the exam can involve inner classes. Unless you deeply understand the rules and syntax for inner classes, you're likely to miss questions you'd otherwise be able to answer. As if the exam weren't already tough enough."

Zasadniczo zagadnienia dostępności/widoczności poszczególnych klas czy metod dotyczy większość pytań w tym rozdziale. Tutaj napiszę tylko, że w przypadku method-local inner class ma ona nielimitowany dostęp do klasy otaczającej. Taki kod:

public class Tester {
    final String lancuch = "Chlebik";

    public void pokazKlaseWewnetrzna()
    {
        class takaSobieKlasa {
            void hej() {
                System.out.println( lancuch );
            }
        }
    }
}

jest jak najbardziej w porządku i zadziała bez problemu.

Pamiętaj o statykach – zarówno o tym, że metody niestatyczne nie mogą być wywoływane ze statycznych metod, a także, że statyczne zmienne trzymają się twardo i mają za nic tworzenie instancji klasy.

Widoczność klas wewnętrznych – kod z pytania testowego:

    class A {
        void m() {
            System.out.println("outer");
        }
    }

    public class TestInners {

        public static void main(String[] args) {
            new TestInners().go();
        }

        void go() {
            new A().m();
            class A { void m() { System.out.println("inner"); } }
        }

        class A { void m() { System.out.println("middle"); } }
    }

Pytanie brzmi – co zostanie wyświetlone na wyjściu programu? Odpowiedź: middle. Dlaczego? Ano bo klasa wewnątrz metody jest zadeklarowana dopiero po wywołaniu (czyli po fragmencie new A().m();, w związku z czym jest niewidoczna. Klasa A, która jest w pierwszej linijce kodu jest "poziom wyżej" niż klasa dająca na wyjściu "middle" i dlatego też nie zostanie wykorzystana.

Rozdział 9 – Wątki

"In fact, the most important concept to understand from this entire chapter is this:
When it comes to threads, very little is guaranteed."

Usypianie wątku to bardzo interesujące zagadnienie. O metodzie sleep() trzeba wiedzieć dwie rzeczy. Raz – jest metodą statyczną klasy Thread. Dlatego też zawsze usypia działanie bieżącego wątku. Do tego wyrzuca ona InterruptedException, zatem musimy wywołanie metody zawrzeć w klauzuli try..catch, albo też przekazać obsługę wyjątku wyżej.

Metoda join() – podobnie jak powyższa wyrzuca wyjątek InterruptedException. Nie jest jednakże metodą statyczną. Spójrzmy na taki kod:

    public static void main( String[] args ) {

        Thread t = new Thread( new Runnable() {
                public void run() {
                    System.out.println( "Początek pętli" );
                    for( double i = 0; i < 1000000000; i++ ) { }
                    System.out.println( "Koniec pętli" );
                }
            }
        );

        System.out.println( "Chlebik 1:" );
        t.start();
        System.out.println( "Chlebik 2:" );

        try {
            t.join();
        } catch( Exception e ) { }

        System.out.println( "Chlebik 2:" );
    }

Obecnie wykonywany wątek (ten z metody main) nie wyświetli napisu "Chlebik 2:" dopóki nie zostanie zakończone działanie wątku reprezentowanego przez obiekt t!

Metoda wait() – odstaje troszeczkę od powyższego towarzystwa, gdyż jest ona właściwa dla wszystkich obiektów w Javie (pochodzi z klasy Object). Nie jest statyczna, zaś jej wywołanie nie powoduje wyrzucenia wyjątku. W parze z nią idą dwie inne metody z klasy Objectnotify() i notifyAll(). Wszystkie są oznaczone jako final, zatem nie potrzeba w ich przypadku karkołomnych zabaw jak z np. metodą equals().

Idąc dalej – metody te mogą być wywołane tylko i wyłącznie w kontekście synchronized! Próby użycia poza tymże kontekstem skutkują wyrzuceniem IllegalMonitorStateException (i to nie jest sprawdzalny wyjątek więc nie trzeba definiować jego łapania). Metody te służą do zarządzania blokadami obiektu (dlatego są elementami klasy Object). Metoda wait() pozwala na wstrzymanie działania wątku, który posiada blokadę obiektu, aż do wywołania przez ten obiekt metody notify(), albo notifyAll(). Nie za bardzo podejmuję się więcej tłumaczyć to pisząc – myślę, że kod powie więcej (wzięty z podręcznika):

    class ThreadA {
        public static void main(String [] args) {
            ThreadB b = new ThreadB();
            b.start();

            synchronized(b) {
                try {
                    System.out.println("Waiting for b to complete...");
                    b.wait();
                } catch (InterruptedException e) {}
                System.out.println("Total is: " + b.total);
            }
        }
    }

    class ThreadB extends Thread {
        int total;

        public void run() {
            synchronized(this) {
                for(int i=0;i<100;i++) {
                    total += i;
                }
                notify();
            }
        }
    }

Kiedy blokada istnieje, a nie kiedy nie – to temat bardzo istotny. Oto cytat z Thinking in Java w wersji 4.

"Ważne jest, aby zrozumieć, że wywołanie metody sleep() nie zwalnia blokady obiektu, tak samo jak nie czyni tego wywołanie yield(). Z drugiej strony, wywołanie wait() zainicjowane w obrębie synchronizowanej metody wymusza zawieszenie wątku i zwolnienie blokady danego obiektu."

Metoda getId() – na to się trochę wkurzyłem. Ni stąd, ni zowąd wyskoczyły mi pytania o tę metodę. Może to i sposób, aby w pytaniach testowych poruszać zagadnienia, których nie było w konkretnym rozdziale. Ale to nie lepiej by było po prostu dać listę metod, o które potencjalnie jeszcze mogą paść pytania? Uczenie się całego API na pamięć to chyba nie jest cel egzaminacyjny? No nic, koniec narzekania – rzecz w metodzie getId().

Dokumentacja mówi, że zwraca ona (metoda rzecz jasna) unikatowy identyfikator wątku (prymityw typu long). Spójrzmy na ten kod:

    // Obiekty a-a3 są tego samego typu jak ten poniżej

    read a4 = new Thread( new Runnable() {
        public void run() {
            for( int i = 0; i < 100000; i++ ) {
               if( i == 99999 ) System.out.println('.' + Thread.currentThread().getId());
            }
        }
    });

    System.out.println( Thread.currentThread().getId() );
    a.start(); System.out.println( a.getId() );
    a2.start(); System.out.println( a2.getId() );
    a3.start(); System.out.println( a3.getId() );
    a4.start(); System.out.println( a4.getId() );

Oto efekt działania:

    1
    8
    9
    10
    11
    54
    55
    56
    57

I tak raz za razem – kolejność cyfr oczywiście bywa różna (poza pierwszymi trzema-czterema) – wiadomo, nieokreśloność wywoływania wątków. Tak to wygląda. Dwie kwestie – jak widać wątek metody main() zawsze (przynajmniej u mnie) ma numer 1. Pozostałe w miarę równo i oczywiście idąc w górę. Pytania na testowym egzaminie dotyczyły ewentualnego wyniku na wyjściu programu podobnego do powyższego. Ciekawe, ale jednak to w wielu momentach jest po prostu loteria.

Rozdział 10 – Development

"When you start to put classes into packages, and then start to use classpaths to find these classes, things can get tricky. The exam creators knew this, and they tried to create an especially devilish set of package/classpath questions with which to confound you."

Statyczne importowanie – oczywiście koniecznym jest ich użycie za pomocą słów kluczowych static import (w takiej kolejności). Jednakże mniej oczywistym zapisem jest to, iż możemy importować w ten sposób nawet pojedyncze stałe i metody.

Asercje – było o nich w rozdziale piątym, ale nie zaznaczyłem tam rzeczy najistotniejszej. Otóż należy pamiętać, że asercje zostały wprowadzone już w wersji 1.4! I dlatego też wywoływanie kompilatora i wirtualnej maszyny w ten sposób:

    javac -source 1.4 plik.java
    java -ea plik

Spowoduje, że kod, w którym występują niespełnione asercje (zwracające wartość false) spowoduje wygenerowanie błędu podczas wykonania programu (czyli po prostu asercje będą działały). Wykonania, nie kompilacji! Powtórzmy – błędy wykonania (run), to co innego niż błędy kompilacji (compile).

Do czego tak naprawdę służy nam dyrektywa classpath? Otóż jej zadaniem jest głównie znalezienie wszystkich klas, których kompilowana/uruchamiana klasa będzie potrzebowała. To jest główne zadanie dla classpath. Pamiętać należy również o tym, iż w przypadku kompilacji (polecenie javac) podanej nazwy pliku do kompilacji poszukuje się domyślnie w bieżącym katalogu. W przypadku uruchamiania pliku tak nie jest! No i rzecz ostatnia – podanie wartości dla classpath powoduje nadpisanie zmiennej systemowej (o ile rzecz jasna istnieje).

Pliki JAR – archiwa są dość proste do zrozumienia, co więcej, na egzaminie nie ma pytań dotyczących ich tworzenia i zarządzania. Natomiast na pewno trzeba wiedzieć, że po utworzeniu pliku JAR z konkretnego katalogu, nawet po dodaniu go do classpath do klas zawartych w archiwum należy odwoływać się w kodzie poprzez podanie pełnej nazwy klasy (łącznie z nazwą pliku JAR). Oto przykład:

test	|
        plik.uzywajacy.klasy.z.jara
        tutaj.utworzymy.plik.jar
        katalog.do.zjarowania	|
                                podkatalog1
                                podkatalog2	|
                                        plik.java

Odwołując się do pliku w archiwum JAR, które utworzyliśmy w katalogu test należy podawać pełną ścieżkę. A zatem nasz plik w katalogu test, w którym chcielibyśmy wykorzystać klasę z archiwum musi odwoływać się do niej poprzez zapis PLIK_JAR/katalog.do.zjarowania/podkatalog2/plik – pomimo dodania pliku JAR do classpath.

Nie ma jeszcze komentarzy.

Tylko zalogowani użytkowincy mogą pisać komentarze

Developers World