Kubek Kawy - czyli alternatywny kurs Javy, cz. II
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