JAVA exPress logo Published on JAVA exPress (http://www.javaexpress.pl)

Kubek Kawy - czyli alternatywny kurs Javy, cz. II

Bartek Kuczyński

Issue 2 (2008-12-06)

Artykuł jest modyfikacją kursu z portalu http://4programmers.net/java/Podstawy_Javy

Siłą komputerów nie jet ich inteligencja, bo jej nie mają. Generalnie komputer potrafi wykonać kilka podstawowych operacji logicznych takich jak porównanie, koniunkcja, alternatywa, alternatywa wykluczająca czy przypisanie. Nawet dodawanie, które jest realizowane w systemie dwójkowym, jest ciągiem kilku prostych operacji logicznych. Prawdziwą siłą komputerów jest to, że potrafią wykonywać miliardy takich operacji na sekundę.

Poprzednią część kursu zakończyliśmy na modyfikatorach metod, klas i pól. Dziś zajmiemy się kolejną partią teorii. Porozmawiamy o tym jakie są typy w Javie. Jak sterujemy programem i jak przechowywać dane.

Typy Proste i Obiekty

W pierwszej części pojawiła się definicja Javy. Było w niej magiczne pojecie „zorientowany obiektowo”. Jak pisałem oznacza to, że w Javie występują zarówno obiekty jak i typy proste. Typów prostych jest 8:

- byte

- short

- int

- long

- float

- double

- char

- boolean

Wszystkie inne dane w Javie traktowane są jako obiekty. Należy pamiętać, że typy proste nie występują w programach jako byty niezależne. Pojawiają się jako pola lub zmienne w obiektach i metodach. Warto zaznaczyć, że od wersji 1.5 Java oferuje mechanizm "pudełkowania" (autoboxing) dla typów prostych. Zasada działania tego mechanizmu jest teoretycznie dość prosta. Wszelkie typy proste są w procesie kompilacji zamieniane na odpowiadające im obiekty. Mechanizm zamieniający kompilatora dba jednak o to, żeby skompilowany kod zachował zgodność wstecz. Obiekty odpowiadające typom prostym mogą zostać "wypakowane" (unboxing) do typu prostego.

Każdy obiekt i typ prosty jest reprezentowany przez zmienną.

Przykłady deklaracji zmiennych typów prostych:

            int a;
            //deklaracja zmiennej a typu całkowitego
            char b;
            //deklaracja zmiennej b, która zawiera znaki Unicode
        

Równoczesna deklaracja i inicjacja zmiennych:

            int a = 3;
            char b = 'c';
        

Oprócz typów prostych w Javie istnieją typy obiektowe. Są to np:

Button, Panel, Label, Okno - nazwa klasy stworzonej przez użytkownika.

Deklaracja zmiennych typu obiektowego:

            Button b = new Button();
        

Typy proste i obiekty mogą zostać najpierw zdefiniowane, a zainicjowane dopiero później. Zmienne, które są polami obiektów zawsze są inicjowane z wartością domyślną. Zmienne lokalne (zwane też automatycznymi) muszą zostać jawnie zainicjowane. Przykład:

            class Foo {
              int zmienna;
              // zmienna będzie miała domyślna wartość 0
              // w momencie stworzenia obiektu klasy
              Object obj;
              // zmienna będzie miała domyślna wartość null
              // w momencie stworzenia obiektu klasy

              public void bar() {
                /*
                 * zmienna musi zostać jawnie zainicjowania,
                 * inaczej kompilator zwróci błąd
                 *  variable may not been initialized
                 *  jeżeli będziemy chcieli jej użyć
                 */
                int zmienna2;
                zmienna2 = 1;
                System.out.println(zmienna); // 0
                System.out.println(zmienna2); // 1
              }
            }
        

Od wersji 1.5 wprowadzono nowy typ enum. Jest to rozwiązanie problemów pojawiających się, gdy stosujemy konstrukcje podobne do poniższej:

            class Klasa {
              // definiujemy jakieś stałe - flagi
              public static final String ADD = "dodaj";
              public static final String LIST = "Wypisz";
              public static final String DELETE = "Usuń";
              // ....
            }

            // później w kodzie:

            class A {
              public void metoda(String akcja) {
                if (akcja.equals(Klasa.ADD)) {
                  // ...
                } else if (akcja.equals(Klasa.LIST)) {
                  // ...
                } else if (akcja.equals(Klasa.DELETE)) {
                  // ...
                } else {
                  // ...
                }
              }
            }
        

Takie rozwiązanie ma wiele wad. Do najważniejszych należą:

- możliwość użycia złej nazwy, słaba typowalność nazw,

- skrajna nierozszerzalność, użycie słowa kluczowego final wyklucza zmianę klucza. Przypadkowa próba nadpisania klucza powoduje błędy kompilacji.

Zamiast takiego podejścia należy używać typu enum :

            enum Akcje {
              ADD,
              LIST,
              DELETE;
            }
        

Problematyka typów wyliczeniowych (enum) jest złożona. Na chwilę obecną musimy zapamiętać tylko tyle, że są.

Operatory

W Javie istnieje kilka operatorów. Możemy je podzielić na trzy główne grupy. Pierwszą stanowią operatory matematyczne. Drugą operatory logiczne, w tym operatory porównania, a trzecią operatory bitowe.

Operatory matematyczne

W Javie zdefiniowane są następujące operatory matematyczne:

- Dodawanie (+)

- Odejmowanie (-)

- Mnożenie (*)

- Dzielenie(/)

- Reszta z dzielenia (%)

- Wynik dzielenia z resztą ()

Dodatkowo zdefiniowane są jeszcze dwa operatory inkrementacji i dekrementacji:

- Inkrementacja - zwiększenie o 1 (++)

- Dekrementacja - zmniejszenie o 1 (--)

Powyższe operatory mogą stać zarówno po nazwie zmiennej, jak i przed. W pierwszym przypadku mówimy o postinkrementacji lub postdekrementacji. Oznacza to, że zmienna jest zwiększana lub zmniejszana po użyciu. W drugim przypadku mówimy o preinkrementacji lub redekrementacji i odpowiednia operacja jest wykonywana przed użyciem zmiennej.

Wszystkie operatory z tabeli mają też odpowiedniki mające sens "wykonaj działanie i przypisz". Za przykład niech posłuży nam operator dodawania:

            int a = 1;
            a += 1; // a ma wartość 2
        

Operatory logiczne

Komputer jest maszyną "myślącą" w sposób całkowicie logiczny. Na podstawowym poziomie komputer wykonuje najprostsze operacje logiczne. Zasada te ma odwzorowanie w operatorach logicznych oraz w operatorach porównania.

- Operacja LUB (||)

- Operacja I (&&)

- Operacja negacja (!)

- większy niż(>)

- mniejszy niż (<)

- większy równy (>=)

- mniejszy równy (<=)

- różny od (!=)

Jak widać w powyższej tabeli zabrakło ważnego operatora. Operator równy w języku Java sprawia osobą początkującym wiele problemów. Wynika to ze specyfiki języka. Zapis:

            a == b
        

ma różne znaczenie w zależności od tego czy odnosi się do typów prostych, czy też obiektów. Jeżeli porównujemy w ten sposób dwie zmienne typu prostego, to operator działa "intuicyjnie". Przykład:

            int a = 1;
            int b = 1;
            System.out.println(a == b); // zwróci true
        

W odniesieniu do zmiennych obiektowych zasada ta jest inna. Operator == oznacza Identyczność, a nie równość:

            Integer a = new Integer(1);
            Integer b = new Integer(1);
            Integer c = a;
            // c jest referencją do tego samego obiektu
            // na stercie co a
            System.out.println(a == b);
            // zwróci flase, a i b to różne obiekty
            // tej samej klasy
            System.out.println(a == c);
            // zwróci true, a i c to ten sam obiekt
        

Jeżeli chcemy porównać dwa obiekty, należy użyć metody equals():

            Integer a = new Integer(1);
            Integer b = new Integer(1);
            System.out.println(a.equals(b)); // zwróci true
        

Ostatnim operatorem porównania dla obiektów jest słowo kluczowe instanceof. Przykład:

            Integer a = new Integer(1);
            // dziedziczy po Number, a ta po Object
            System.out.println(a instanceof Integer);
            // zwróci true a jest klasy Integer
            System.out.println(a instanceof Object);
            // zwróci true, klasa Integer dziedziczy po Object,
            // a jest klasy Object
            System.out.println(a instanceof Serializable);
            // zwróci true, klasa Number implementuje
            // Serializable, a jest Serializable
        

Prawie 90% błędów popełnianych przez początkujących programistów związanych z warunkami logicznymi związane jest z niezrozumieniem i pomyleniem operatora == i metody equals().

Operatory bitowe

Komputery pracują na bitach. Operacje na nich w wielu przypadkach pozwalają na znaczne przyspieszenie obliczeń.

- Operacja LUB (|)

- Operacja I (&)

- Operacja negacji (~)

- Operacja różnicy symetrycznej XOR (^)

- Operacja przesunięcia w prawo (>>)

- Operacja przesunięcia w lewo (<<)

- Operacja przesunięcia w prawo z wypełnieniem zerami (>>>)

Instrukcje sterujące

W momencie, gdy chcemy, aby program dokonał wyboru jednej z dróg na podstawie prawdziwości jakiegoś warunku logicznego możemy użyć jednej z dwóch instrukcji sterujących. Jeżeli chcemy, aby o drodze decydował jakiś warunek logiczny, to używamy instrukcji if/else. Jeżeli chcemy, aby wybór został dokonany na podstawie stanu obiektu możemy użyć przełącznika - switch.

Instrukcja if / if else

Najprostszą instrukcją warunkową jest instrukcja if:

            if (warunek_logiczny) {
              // instrukcje wykonane jeżeli warunek jest PRAWDZIWY
            }
        

odmianą tej instrukcji jest instrukcja if else:

            if (warunek_logiczny) {
              // instrukcje wykonane jeżeli warunek jest PRAWDZIWY
            } else {
              // instrukcje wykonane jeżeli warunek jest FAŁSZYWY
            }
        

instrukcje można zagłębiać:

            if (warunek_logiczny) {
              if (warunek_logiczny2) {
                // instrukcje wykonane jeżeli warunek jest PRAWDZIWY
              }
            }
        

oraz dokonywać wielokrotnego wyboru:

            if (warunek_logiczny) {
              // instrukcje wykonane jeżeli warunek1 jest PRAWDZIWY
            } else if (warunek_logiczny2) {
              // instrukcje wykonane jeżeli warunek2 jest PRAWDZIWY
            } else {
              // instrukcje wykonane jeżeli warunek1 i warunek 2 są FAŁSZYWE
            }
        

Operator trójargumentowy ? :

Jeżeli chcemy, aby zmienna przyjęła jakąś wartość w zależności od warunku logicznego możemy, zamiast bloku if else, użyć specjalnego operatora trójargumentowego:

            zmienna = warunek ? wartosc_jak_prawda  : wartosc_jak_falsz;
        

Jest to szybsza i czytelniejsza forma od:

            if (warunek) {
              zmienna = wartosc_jak_prawda;
            } else {
              zmienna = wartosc_jak_falsz;
            }
        

Blok switch

Jeżeli chcemy, aby jakiś kod został wykonany w momencie, gdy zmienna znajduje się w określonym stanie, to możemy użyć bloku switch:

            switch (key) {
            case value1:
              // instrukcje dla key równego value1
              break;
            case value2:
              // instrukcje dla key równego value2
              break;
            default:
              break;
            }
        

W języku Java klucz (key) może być tylko typu int lub char (ten jest odczytywany jako liczba z tablicy unicode), a od wersji 1.5 też enum. Warto zauważyć, iż słowo kluczowe break jest w pewnym sensie obowiązkowe. Jeżeli nie użyjemy go, to instrukcje będą dalej przetwarzane. Zatem rezultatem takiego kodu:

            int i = 0;
            switch (i) {
            case 0:
              System.out.println(0);
            case 1:
              System.out.println(1);
              break;
            default:
              System.out.println("default");
              break;
            }
        

będzie:

            0
            1
        

Pętle

Jeżeli chcemy wykonać jakiś fragment kodu wielokrotnie, to możemy wypisać go explicite:

            System.out.println(1);
            System.out.println(2);
            System.out.println(3);
        

Takie rozwiązanie jest jednak złe. Co jeżeli chcemy wypisać np. wszystkie posty z forum? Jest tego trochę. W dodatku liczba ta wciąż rośnie więc w momencie uruchomienia kodu na pewno nie będzie tam ostatnich postów. Rozwiązaniem tego problemu jest specjalna instrukcja języka - Pętla. Ogólna zasada działania pętli jest bardzo prosta i można ją ująć na trzy sposoby:

WYKONAJ POLECENIE N-KROTNIE

lub

WYKONAJ POLECENIE DOPÓKI SPEŁNIONY JEST WARUNEK

lub

WYKONAJ POLECENIE DLA KAŻDEGO ELEMENTU ZBIORU B

Tak oto zdefiniowaliśmy trzy podstawowe rodzaje pętli w Javie, a ogólniej w programowaniu.

Pętla for

Jest to pętla policzalna, czyli taka, o której możemy powiedzieć, iż wykona się określoną liczbę razy. Ogólna składnia pętli for wygląda w następujący sposób:

            for (int i = 0; warunek; krok) {
              // instrukcja
            }
        

Uwagi:

- Zmienną i nazywamy Indeksem Pętli

- Indeks może być dowolnym typem prostym poza boolean. Typ char ograniczony jest do 65535

- Warunek może być dowolnym zdaniem logicznym, należy jednak zwrócić uwagę by nie była to tautologia. Otrzymamy wtedy pętle nieskończoną.

- Krok pętli może być dowolny jednak tak samo jak w przypadku warunku, trzeba uważać na zapętlenie się programu.

Gdzie należy używać pętli for? Odpowiedź na to pytanie jest jedną z kwestii spornych i wywołuje gorące dyskusje. Pętla ta najlepiej sprawdza się, gdy chcemy wykonać jakąś operację na wszystkich elementach tablicy. Jest naturalną i najbardziej czytelną dla tego typu problemów:

            int[] a = new int[] { 1, 2, 3 };
            for (int i = 0; i < a.length; i++) {
              System.out.println(a[i]);
            }
        

Odmianą pętli for wprowadzoną w wersji 1.5 jest wersja przyjmująca dwa argumenty. Iterator i warunek. Operuje on na kolekcjach. Przykład:

            Collection col = new HashSet();
            col.add("a");
            col.add("b");
            for (Iterator it = col.iterator(); it.hasNext();) {
              System.out.println(it.next());
            }
        

        

Pętle while i do while

Pętle while i do while są pętlami niepoliczalnymi, czyli takimi, o których nie możemy powiedzieć ile razy się wykonają. Ich składnia jest następująca:

            while (warunekLogiczny) {
              // instrukcja
            }
            /*---------*/
            do {
              // instrukcja
            } while (warunekLogiczny);
        

Uwagi:

- warunek musi być zmienną boolean lub obiektem klasy java.lang.Boolean.

Obie te konstrukcje są bardzo podobne do siebie. Główna różnica polega na tym, iż w pętli while warunek jest sprawdzany przed wykonaniem instrukcji, a w pętli do while po wykonaniu instrukcji. Oznacza to, że pętla do while wykona się co najmniej jeden raz. Poniższy przykład ilustruje różnicę:

            int i = 0;
            while (i < 1) {
              System.out.println("while " + i);
              i++;
            }
            do {
              System.out.println("do while " + i);
              i++;
            } while (i < 1);
        

Druga pętla wykona się pomimo iż warunek, 1 < 1, nie jest prawdziwy.

Kiedy używać? Najczęściej pętla while jest wykorzystywana do nasłuchiwania. Polega to na stworzeniu nieskończonej pętli, najczęściej podając jako argument słowo true, której zadaniem jest wykonywanie danego kodu nasłuchującego. Prosty szablon animacji:

            while (animuj) {
              // kod animacji
            }
        

Dopóki flaga animuj jest prawdziwa, wykonywana jest animacja. Podobnie ma się sprawa z prostymi serwerami, które nasłuchują w ten sposób na portach.

Pętla for element : Iterable

W raz nadejściem Javy w wersji 1.5, pojawiła się możliwość użycia konstrukcji for E : I. Ta zdawać by się mogło dziwaczna konstrukcja jest w rzeczywistości odpowiednikiem pętli foreach. Jako argument przyjmuje tablicę lub obiekt klasy implementującej interfejs Iterable. Przykład:

            List l = new LinkedList();
            l.add("a");
            l.add("b");
            for (Object e : l) {
              System.out.println(e.toString());
            }
        

        

Gdzie stosować? Konstrukcja ta jest najodpowiedniejsza dla wszelkiego rodzaju list, kolekcji i wektorów.

Przerywanie i przechodzenie do następnego kroku w pętlach

Czasami zdarza się, że chcemy przerwać lub pominąć krok pętli, jeżeli spełniony jest jakiś warunek. Aby uzyskać taki efekt, musimy użyć jednego z dwóch słów kluczowych.

break

Jeżeli chcemy przerwać wykonanie pętli, gdy spełniony jest jakiś warunek, to musimy użyć słowa break:

            int i = 0;
            while (true) {
              if (i == 5)
                break;
              System.out.println(i);
              i++;
            }
        

continue

Jeżeli chcemy pominąć jakiś krok w pętli, to musimy użyć słowa continue:

            for (int i = 0; i < 10; i++) {
              if (i == 5)
                continue;
              System.out.println(i);
            }
        

Podsumowanie pętli

- Mamy trzy rodzaje pętli,

- Różnią się one zasadą działania,

- Należy uważać, by nie popełnić błędu i nie stworzyć pętli nieskończonej.

Tablice i Kontenery

Omówiliśmy już wszystkie najistotniejsze elementy języka Java. Jedną z ostatnich kwestii, jakie zostaną poruszone w tym artykule, jest temat tablic i kolekcji.

Wyobraźmy sobie, że mamy do stworzenia kilka Obiektów tej samej Klasy. Możemy zrobić to na kilka sposobów. Najprostszym jest zdefiniowanie ich jako kolejnych zmiennych:

            Object o1 = new Object();
            Object o2 = new Object();
            Object o3 = new Object();
        

Metoda ta posiada wiele wad:

- tworzymy dużo dodatkowego kodu,

- tworzymy wiele zmiennych,

- chcąc wykonać operację na każdym z obiektów musimy duplikować kod.

Chcąc uniknąć tych nieprzyjemności, możemy wykorzystać jeden z mechanizmów do obsługi zbiorów obiektów.

Tablice

Kontener na dane, w którym do każdej z komórek można odwołać się za pomocą klucza. Klucz jest co do zasady wartością numeryczną.

Najprostszym opisem tablicy jest porównanie jej do tabeli, w której każda komórka ma swój unikatowy numer. Za pomocą numeru możemy odwołać się do komórki i pobrać lub umieścić w niej pewną daną. Java pozwala na definiowanie tablic na kilka sposobów:

            // n - wielkość tablicy
            Object[] o1;
            o1 = new Object[n];
            Object o2[] = { new Object(), new Object() };
            int[] i = new int[10];
        

Komórki w tablicach są numerowane od 0. Oznacza to, że ostatni element znajduje się w komórce o numerze o jeden mniejszym od długości tablicy. Tablica może mieć maksymalnie 2^31 -1 komórek. Uwaga! Inicjacja tablicy o maksymalnej długości może spowodować błąd przepełnienia stosu.

Jeżeli chcemy otrzymać wartość elementu na pozycji m, to wystarczy odwołać się do niego w ten sposób:

            int elementM = i[m];
        

Tablica, nawet jeżeli jest tablicą typów prostych, jest też Obiektem. Warto o tym pamiętać, ponieważ częstym błędem jest porównywanie tablic w taki oto sposób:

            Object[] o1;
            o1 = new Object[2];
            o1[0] = new Object();
            o1[1] = new Object();
            Object[] o2 = { new Object(), new Object() };
            System.out.println("== " + (o1 == o2));
            System.out.println("equals " + o1.equals(o2));
            /***
             * == false equals false
             **/
        

Jedyną prawidłową metodą jest porównanie każdego elementu tablicy z elementem o takim samym indeksie w drugiej tablicy:

            public boolean porownaj(Object[] o1, Object[] o2) {
              if (o1 == null || o2 == null)
                return false;
              if (o1.length != o2.length)
                return false;
              for (int i = o1.length - 1; i >= 0; i--) {
                if (!o1[i].equals(o2[i]))
                  return false;
              }
              return true;
            }
        

Java umożliwia też tworzenie tablic wielowymiarowych:

            Object[][] o3 = {
                { new Object(), new Object() },
                { new Object(), new Object() } };
            Object[] o4[];
            o4 = new Object[10][10];
            Object[][] o5 = new Object[2][];
            o5[0] = new Object[10];
            o5[1] = new Object[1];
        

Jak widać każda tablica w tablicy jest niezależna. Zmienna o5 to tablica dwuwymiarowa, w której pierwszy wymiar ma krotność 2, a poszczególne komórki tego wymiaru odpowiednio 10 i 1.

Ostatnią istotną kwestią dotyczącą tablic jest zagadnienie inicjacji wartości komórek. Tablica jest inicjowana na tych samych zasadach co obiekt, to znaczy:

- jeżeli tablica jest polem Klasy, to jest inicjowana na null. Nie posiada rozmiaru.

- jeżeli tablica jest zmienną lokalną, to nie jest inicjowana i trzeba ją inicjować ręcznie.

Co jednak z poszczególnymi komórkami? Jeżeli tablica zostanie zainicjowana, to wszystkie komórki zostaną zainicjowane tak, jak by były polami obiektu i przyjmą wartości domyślne. Przykład:

            Object o6[] = new Object[2];
            System.out.println(o6[0]);
            int[] j = new int[2];
            System.out.println(j[0]);
        

Kontenery

Jednym z ograniczeń tablic jest ich ograniczona wielkość. Ograniczona zarówno w sensie ilości elementów, ale też zmiany rozmiarów tablicy. Chcąc zaradzić temu problemowi Java posiada dość pokaźny zbiór klas i interfejsów rożnych typów kontenerów.

Kontener (ang. collection) jest to struktura pozwalająca na przechowywanie danych w sposób uporządkowany. Posiada mechanizmy dodawania, usuwania i zamiany elementów.

Pakiet java.util zawiera w sobie wszystkie najważniejsze rodzaje kontenerów, a są to:

Listy

Lista to uporządkowany zbiór danych. Do elementu listy można dostać się za pomocą podania wartości indeksu. Interfejs java.util.List pozwala na przechowywanie danych w postaci list wiązanych, list opartych o tablice, stosu, wektora.

Mapy

Mapy pozwalają na dostęp do obiektów na podstawie wartości klucza. Każdy element mapy składa się z pary. Interfejs java.util.Map pozwala na stworzenie map opartych o hasz jak też o drzewa.

Set

Jest to specyficzny rodzaj kontenera, w którym obiekty przechowywane nie mogą się powtarzać. Interfejs java.util.Set pozwala na przechowywanie danych w postaci listy wiązanej bez powtórzeń, drzewa, kolekcji opartej o funkcję skrótu (hasz).

Kolejka

Kolejka jest to rodzaj kontenera pozwalający na dostęp do danych w oparciu o algorytmy FIFO i LIFO. Interfejs java.util.Queue pozwala na stworzenie kolejek blokujących, synchronizowanych i innych.

Podsumowanie

Jak można zauważyć, tablice i kontenery mają swoje wady i zalety. Wybierając kontener należy kierować się kilkoma prostymi pytaniami:

- czy wielkość zbioru jest stała?

- czy ilość dostępów do elementów w środku zbioru jest duża?

- czy ilość wstawień w środku zbioru jest duża?

- czy elementy mogą się powtarzać?

Odpowiedzi na te pytania pozwolą na określenie, jakiego typu kontenera należy użyć.

Tak oto szczęśliwie dobrnęliśmy do końca teorii związanej z Javą. W kolejnej części zajmiemy się już praktyką. Napiszemy pierwszy program i poznamy podstawowe narzędzia, które pozwolą nam kontynuować przygodę z programowaniem bez niepotrzebnego stresu.

Source: http://www.javaexpress.pl/article/show/Kubek_Kawy__czyli_alternatywny_kurs_Javy_cz_II