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:
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:
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
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 whilepo 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.