Professional Documents
Culture Documents
Java
Java
Wydawnictwo HELION
ul. Kościuszki 1c, 44-100 GLIWICE
tel. 32 231 22 19, 32 230 98 63
e-mail: helion@helion.pl
WWW: http://helion.pl (księgarnia internetowa, katalog książek)
Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie/pkjav4_ebook
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
ISBN: 978-83-283-0925-8
b4ac40784ca6e05cededff5eda5a930f
b
Spis treści
Wstęp .............................................................................................. 5
Rozdział 1. Podstawy ......................................................................................... 9
Instalacja JDK .................................................................................................................. 9
Instalacja w systemie Windows ................................................................................ 10
Instalacja w systemie Linux ..................................................................................... 11
Przygotowanie do pracy z JDK ................................................................................ 11
Podstawy programowania ............................................................................................... 13
Lekcja 1. Struktura programu, kompilacja i wykonanie ........................................... 13
Lekcja 2. Podstawy obiektowości i typy danych ...................................................... 16
Lekcja 3. Komentarze ............................................................................................... 19
Rozdział 2. Instrukcje języka ............................................................................ 23
Zmienne .......................................................................................................................... 23
Lekcja 4. Deklaracje i przypisania ........................................................................... 24
Lekcja 5. Wyprowadzanie danych na ekran ............................................................. 27
Lekcja 6. Operacje na zmiennych ............................................................................. 33
Instrukcje sterujące ......................................................................................................... 47
Lekcja 7. Instrukcja warunkowa if…else ................................................................. 47
Lekcja 8. Instrukcja switch i operator warunkowy ................................................... 53
Lekcja 9. Pętle .......................................................................................................... 59
Lekcja 10. Instrukcje break i continue ...................................................................... 66
Tablice ............................................................................................................................ 74
Lekcja 11. Podstawowe operacje na tablicach .......................................................... 74
Lekcja 12. Tablice wielowymiarowe ........................................................................ 79
Rozdział 3. Programowanie obiektowe. Część I ................................................. 91
Podstawy ........................................................................................................................ 91
Lekcja 13. Klasy, pola i metody ............................................................................... 91
Lekcja 14. Argumenty i przeciążanie metod .......................................................... 100
Lekcja 15. Konstruktory ......................................................................................... 110
Dziedziczenie ............................................................................................................... 121
Lekcja 16. Klasy potomne ...................................................................................... 122
Lekcja 17. Specyfikatory dostępu i pakiety ............................................................ 129
Lekcja 18. Przesłanianie metod i składowe statyczne ............................................ 144
Lekcja 19. Klasy i składowe finalne ....................................................................... 156
b4ac40784ca6e05cededff5eda5a930f
b
4 Java. Praktyczny kurs
b4ac40784ca6e05cededff5eda5a930f
b
Wstęp
Zapewne wielu Czytelnikom trudno będzie w to uwierzyć, ale Java była pierwotnie znana
jako Oak (Dąb) i miała być językiem programowania służącym do sterowania urzą-
dzeniami elektronicznymi powszechnego użytku, czyli pralkami, lodówkami czy kuchen-
kami mikrofalowymi, a więc praktycznie każdym urządzeniem, które zawiera mikropro-
cesor. Stąd właśnie wywodzi się jedna z największych zalet Javy, czyli przenośność
— ten sam program, przynajmniej teoretycznie, będzie bowiem działał na różnych
urządzeniach i platformach sprzętowo-systemowych. Będzie go więc można urucho-
mić i na komputerze PC, i na Macintoshu, i w systemie Windows, i w Uniksie.
Takie były właśnie początki Javy w roku 1990, czyli dwadzieścia kilka lat temu. Język zo-
stał opracowany przez zespół kierowany przez Jamesa Goslinga w firmie Sun Microsys-
tems, choć w rzeczywistości prace były prowadzone na Uniwersytecie Stanford. Pro-
jekt ten był gotowy bardzo szybko, już w roku 1991, jednak tak naprawdę aż do roku
1994 nie udało się go spopularyzować. Z tego też powodu prace zostały zawieszone.
Historia informatyki pełna jest jednak przypadków. Były to lata ogromnej ekspansji i wręcz
eksplozji internetu. Nagle okazało się, że Oak przecież doskonale sprawdzałby się w tak
różnorodnym środowisku, jakim jest globalna sieć. W ten oto sposób w roku 1995 ujrzała
światło dzienne Java, a to, co nastąpiło później, zaskoczyło chyba wszystkich, łącznie
z twórcami tego języka. Java niezwykle szybko, nawet jak na technologię informatyczną,
została zaakceptowana przez społeczność internautów i programistów na całym świe-
cie. Z pewnością nie można tu pominąć wpływu kampanii marketingowej producenta,
nie osiągnęłaby ona jednak sukcesu, gdyby nie niewątpliwe zalety tego języka. Java
b4ac40784ca6e05cededff5eda5a930f
b
6 Java. Praktyczny kurs
Narzędzia
Do programowania w Javie będzie potrzebne odpowiednie środowisko programistyczne
zawierające niezbędne narzędzia: minimum to kompilator oraz tzw. maszyna wirtual-
na. Wszystkie przykłady prezentowane w tej książce są oparte na pakiecie JDK (Java
Development Kit). Można skorzystać z wersji sygnowanej przez oficjalnego producenta
Javy — firmę Oracle1 — lub rozwijanej na zasadach wolnego oprogramowania wersji
OpenJDK. Wersja oficjalna dostępna jest pod adresami http://java.sun.com (po wpisaniu
tego adresu nastąpi przekierowanie do domeny oracle.com) i http://www.java.com,
a OpenJDK pod adresem http://openjdk.java.net/. Najlepiej korzystać z jak najnowszej
wersji JDK, tzn. 6 (1.6), 7 (1.7), 8 (1.8) lub wyższej (o ile taka będzie dostępna2),
choć podstawowe przykłady będą działać nawet na wiekowej już wersji 1.1. Jeśli będą
stosowane funkcje dostępne jedynie w konkretnej wersji środowiska, będzie to zaznacza-
ne w tekście.
Oprócz JDK będzie potrzebny jedynie dowolny edytor tekstowy umożliwiający wpi-
sywanie tekstu programów. Może to być nawet tak prosty produkt jak np. Notatnik
z Windowsa. Lepszym rozwiązaniem byłby jednak bardziej rozbudowany produkt oferu-
jący takie udogodnienia jak kolorowanie składni i numerowanie wierszy kodu. Dla śro-
dowiska Windows można polecić np. doskonały i darmowy Notepad++ (http://notepad-
-plus-plus.org/). Dobrym rozwiązaniem będzie też wieloplatformowy jEdit (http://www.
jedit.org/), który został napisany w… Javie.
1
Pierwotny producent — Sun Microsystems — został zakupiony przez Oracle w 2009 roku. Tym samym
obecnie to Oracle oficjalnie odpowiada za rozwój Javy.
2
W trakcie powstawania tej książki kolejna wersja Javy była zapowiadana na rok 2016, biorąc jednak
pod uwagę wcześniejszą historię języka, jest mało prawdopodobne, aby taki termin został dotrzymany.
b4ac40784ca6e05cededff5eda5a930f
b
Wstęp 7
Wersje Javy
Pierwsza wersja Javy pojawiła się w roku 1995 i nosiła numer 1.0, większą popularność
zdobyła jednak dopiero usprawniona wersja 1.1 z roku 1997. Po kolejnych dwóch latach
(rok 1999) światło dzienne ujrzała Java 1.2, która ze względu na znaczne usprawnienia
(zawierała już wtedy ok. 60 pakietów i ponad 1500 klas) została nazwana Platformą Java
2 (ang. Java 2 Platform). W ramach projektu Java 2 powstały trzy wersje narzędzi
JDK i JRE3: 1.2 (1999), 1.3 (2000) i 1.4 (2002), a każda z nich miała od kilku do kil-
kunastu podwersji. Kolejnym krokiem w rozwoju projektu była wersja 1.5 (rok 2004),
która, jak się wydaje, ze względów czysto marketingowych została przemianowana
na 5.0; określa się ją również mianem Java 5. W 2006 roku pojawiła się Java 6 (1.6)
zawierająca nowe rozwiązania, takie jak obsługa typów uogólnionych (ang. generics).
Zrezygnowano też wtedy z używania określenia Java 2 Platform. W roku 2010 pojawiła się
kolejna wersja — 7 (1.7). Najnowsza w chwili opracowywania materiałów do tej książki
była wersja 8 (czyli inaczej 1.8) z roku 2014. Dodatkowo JDK występuje w trzech
osobnych edycjach: standardowej (ang. Standard Edition), dla dużych organizacji (ang.
Enterprise Edition) i dla urządzeń mobilnych (ang. Mobile Edition); są one oznaczane
skrótami SE, EE i ME. Określenie Java SE 8 oznacza w związku z tym platformę Java
Standard Edition w wersji 8. Warto też zauważyć, że wewnętrzna numeracja narzędzi
(widoczna np. w niektórych opcjach kompilatora javac) wciąż bazuje na „starej”, logicz-
nej numeracji (Java 2 5.0 oznacza to samo co Java 1.5, Java 6 — to samo co Java 1.6,
Java 7 — to samo co Java 1.7, a Java 8 — to samo co Java 1.8).
3
Java Runtime Environment — środowisko uruchomieniowe potrzebne do uruchamiania programów
w Javie.
b4ac40784ca6e05cededff5eda5a930f
b
8 Java. Praktyczny kurs
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 1.
Podstawy
Przed wypłynięciem na szerokie wody programowania w Javie trzeba zapoznać się z pod-
stawami. W tym rozdziale zostanie pokazane, jak wygląda struktura programu w Javie,
czym jest i jak przebiega kompilacja oraz jak taki program uruchomić. Przedstawione
zostaną podstawy obiektowości, czyli najważniejsze zagadnienia z dziedziny progra-
mowania obiektowego, oraz pojęcie typu danych. Znajdzie się tu także krótkie omówie-
nie typów danych występujących w Javie. Okaże się, w jaki sposób umieścić w progra-
mie komentarz, co, wbrew pozorom, jest ważną czynnością, ponieważ pozwala na
dokumentowanie kodu, poprawia jego czytelność i ułatwia analizę.
Czytelnicy, którzy znają podstawy programowania, mogą spokojnie pominąć ten roz-
dział i przejść do drugiego, aby zobaczyć, jak w Javie wyglądają typowe konstrukcje
programistyczne, takie jak pętle i instrukcje warunkowe, deklaracje zmiennych oraz
tablice. Natomiast osoby, które dopiero zaczynają swoją przygodę z programowaniem,
powinny przeczytać ten rozdział dokładnie — nie jest on długi i stanowi wprowadze-
nie w świat Javy. Nie należy się przejmować, jeśli nie wszystko od razu będzie całkiem
jasne. Z wieloma pojęciami i konstrukcjami programistycznymi trzeba się po prostu
oswoić, wykonując praktyczne przykłady. Temu przecież służy ta książka.
Instalacja JDK
Aby móc wykonywać prezentowane w książce przykłady, trzeba najpierw zainstalo-
wać w systemie pakiet programistyczny nazywany JDK, czyli Java Development Kit.
Instalacja przebiega typowo, tak jak w przypadku dowolnej innej aplikacji dla danego
systemu operacyjnego, więc nikomu z pewnością nie przysporzy problemów. Poniżej
znajdują się krótkie opisy tego procesu dla JDK 8 i systemów Windows oraz Linux.
b4ac40784ca6e05cededff5eda5a930f
b
10 Java. Praktyczny kurs
Rysunek 1.1.
Okno wyboru
składników JDK
Jeżeli w oknie wyboru JDK nie została wyłączona opcja instalacji JRE (ang. Java
Runtime Environment), czyli środowiska uruchomieniowego pozwalającego na uru-
chamianie aplikacji i apletów napisanych w Javie, a JRE nie było wcześniej instalowane
w systemie jako osobny produkt, w trakcie instalacji JDK na ekranie pojawi się okno
pozwalające na wybór katalogu docelowego JRE (rysunek 1.2). Po kliknięciu Next
JRE również zostanie zainstalowane.
Rysunek 1.2.
Okno wyboru
składników JDK
1
Uwaga: Java 8 oficjalnie nie współpracuje z systemami Windows poniżej wersji 7.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 1. Podstawy 11
np.:
jdk-8-linux-i586.tar.gz
jdk-1.7.0-linux-i586.tar.gz
np.:
jdk-8-linux-i586-rpm
jdk-1.7.0-linux-i586-rpm
b4ac40784ca6e05cededff5eda5a930f
b
12 Java. Praktyczny kurs
Rysunek 1.3.
Konsola systemowa
w Linuksie
Rysunek 1.4.
Uruchamianie wiersza
poleceń w systemie
Windows
Rysunek 1.5.
Konsola systemowa
w systemie Windows
2
Klawisz funkcyjny Windows jest też opisywany jako Start.
3
W starszych systemach (Windows 98, Me) należy uruchomić aplikację command.exe
(Start/Uruchom/command.exe).
4
W systemach Vista i 7 pole Uruchom standardowo nie jest dostępne w menu startowym, ale można je
do niego dodać, korzystając z opcji Dostosuj.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 1. Podstawy 13
Dzięki temu nie będzie trzeba za każdym razem podawać pełnej ścieżki dostępu, aby
uruchomić kompilator czy też inne narzędzie. Ta czynność powinna zostać wykonana
automatycznie, jeśli został wykorzystany instalator w Windowsie lub pakiet RPM
w Linuksie. Jeżeli jednak w wierszu poleceń nie są rozpoznawane komendy javac i java,
aktualizację zmiennej PATH trzeba przeprowadzić samodzielnie lub też za każdym razem
przy wywoływaniu tych poleceń podawać pełną ścieżkę dostępu, np. w Windowsie:
C:\Program Files\Java\jdk1.8.0\bin\javac
lub w Linuksie:
/usr/java/jdk1.8.0/bin/javac
Aktualizacja zmiennej środowiskowej PATH dla bieżącej sesji konsoli w systemie Win-
dows polega na wydaniu polecenia o ogólnej postaci:
path=%path%;ścieżka_dostępu
np.:
path=%path%;c:\Program Files\java\jdk1.8.0\bin
np.:
PATH=$PATH:/usr/java/jdk1.8.0/bin/
Jeżeli zmiany ścieżki mają być zapisane na stałe, należy skorzystać z odpowiednich
mechanizmów systemowych. W Linuksie jest to dopisanie odpowiedniego polecenia do
skryptu startowego powłoki (np. .bash_profile), w Windowsie w oknie właściwości sys-
temu należy odnaleźć panel zmiennych środowiskowych i tam wprowadzić modyfikację
do zmiennej PATH.
Podstawy programowania
Lekcja 1. Struktura programu, kompilacja
i wykonanie
Większość kursów programowania zaczyna się od napisania prostego programu, któ-
rego zadaniem jest jedynie wyświetlenie napisu na ekranie komputera. To bardzo dobry
początek pozwalający oswoić się ze strukturą kodu, wyglądem instrukcji oraz proce-
sem kompilacji. Jak zatem wyglądałby taki program w Javie? Jest zaprezentowany na
listingu 1.1.
b4ac40784ca6e05cededff5eda5a930f
b
14 Java. Praktyczny kurs
Listing 1.1.
class Main {
public static void main(String args[]) {
System.out.println("To jest napis.");
}
}
Kod widoczny na listingu to tak zwany kod źródłowy, czyli zapisany w języku progra-
mowania. Aby taki program wykonać, najpierw trzeba przetworzyć go na instrukcje zro-
zumiałe dla danego procesora (ściślej: dla danej platformy sprzętowo-systemowej).
Proces ten nazywamy kompilacją. Kompilacja z kolei jest wykonywana przez program
nazywany kompilatorem.
Pakiet JDK oczywiście zawiera odpowiedni kompilator — javac (ang. java compiler).
Program z listingu 1.1 należy zapisać w pliku o nazwie Main.java, a następnie podać
tę nazwę jako parametr dla kompilatora wywołanego z wiersza poleceń, pisząc:
javac Main.java
Należy przy tym zwrócić uwagę, aby to polecenie wydawać, gdy katalogiem bieżą-
cym jest ten z plikiem Main.java (aby zmienić katalog bieżący, używa się polecenia cd,
np. cd c:\java\). Jeżeli plik jest w innym katalogu niż bieżący, trzeba dodatkowo
podać jego lokalizację. Przykładowo jeśli Main.java jest zapisany na dysku c: w ka-
talogu java, to komenda kompilująca może mieć postać:
javac c:\java\Main.java
Jeśli po wykonaniu polecenia javac Main.java na ekranie pojawi się komunikat o nie-
rozpoznanym poleceniu javac (rysunki 1.6 i 1.7), należy postąpić zgodnie z opisem
przedstawionym w punkcie „Przygotowanie do pracy z JDK” lub przy kompilacji podać
pełną ścieżkę dostępu do kompilatora. Komenda będzie miała wtedy postać (o ile
kompilator znajduje się w lokalizacji c:\Program Files\java\jdk1.8.0\bin):
c:\Program Files\java\jdk1.8.0\bin\javac Main.java
lub (o ile konieczne jest również podanie pełnej ścieżki dostępu do pliku Main.java):
c:\Program Files\java\jdk1.8.0\bin\javac c:\java\Main.java
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 1. Podstawy 15
O ile nie został popełniony błąd, kompilator nie pokaże żadnego komunikatu, natomiast
w katalogu, w którym znajdował się plik z kodem źródłowym, pojawi się plik Main.class.
To właśnie plik z kodem wykonywalnym. Aby go uruchomić, należy w wierszu poleceń
wydać kolejną komendę:
java Main
Na ekranie pojawi się, co nie powinno budzić najmniejszego zdziwienia, napis: To jest
napis. (rysunek 1.8).
Rysunek 1.8.
Kompilacja i wykonanie
pierwszego programu
w Javie
Zarówno przy pisaniu kodu źródłowego, jak i przy wydawaniu komend w wierszu pole-
ceń należy zwrócić uwagę na wielkość wpisywanych liter. Jakakolwiek pomyłka w więk-
szości przypadków spowoduje, że kompilacja się nie uda.
Dlatego też kompilator Javy generuje tak zwany kod pośredni — b-kod (ang. b-code,
byte-code). Kod ten jest zawarty właśnie w plikach z rozszerzeniem class. Ponieważ
b-kod nie jest zrozumiały bezpośrednio dla żadnego procesora (z wyjątkiem niektó-
rych specjalistycznych procesorów zaprojektowanych do tego celu), potrzeba dodatkowe-
go programu tłumaczącego. Ten program to wirtualna maszyna Javy (ang. Java Virtual
Machine). Zatem java Main.class to polecenie oznaczające: uruchom wirtualną ma-
szynę Javy i przekaż jej do wykonania kod zawarty w pliku Main.class.
b4ac40784ca6e05cededff5eda5a930f
b
16 Java. Praktyczny kurs
Każdy program w Javie składa się ze zbioru tzw. klas (to pojęcie zostanie bliżej wyjaśnio-
ne w lekcji 2. oraz w rozdziale 3.). Każda klasa powinna być zapisana w oddzielnym
pliku o nazwie takiej samej jak nazwa klasy5 i rozszerzeniu java. Zatem klasa o nazwie
Main musi znajdować się w pliku o nazwie Main.java, natomiast b-kod tej klasy musi
znajdować się w pliku Main.class. Na potrzeby pierwszych dwóch rozdziałów przyj-
miemy, że każdy program musi mieć dokładnie taką strukturę jak ta przedstawiona na li-
stingu 1.2. Jej dokładne znaczenie będzie wyjaśnione dopiero w rozdziale 3. Na razie
trzeba uwierzyć na słowo, że wykonywanie takiego programu rozpocznie się w miej-
scu oznaczonym // i właśnie tam należy wpisywać instrukcje przeznaczone do wykonania
(instrukcję można traktować jako polecenie dla komputera wydane w danym języku
programowania).
Listing 1.2.
class Main {
public static void main(String args[]) {
//tutaj instrukcje do wykonania
}
}
Ćwiczenie 1.1.
Napisz program wyświetlający na ekranie napis: To jest mój drugi program. Skom-
piluj go i uruchom.
Ćwiczenie 1.2.
Zapisz kod z listingu 1.1 w pliku o rozszerzeniu innym niż java, np. txt. Spróbuj dokonać
kompilacji tego pliku, zaobserwuj, co się stanie.
5
O tym, że nie jest to tak do końca prawda, przekonamy się w rozdziałach 3. i 5. podczas omawiania
rodzajów klas. Na razie przyjmijmy, że każda klasa musi znajdować się w oddzielnym pliku zgodnym
z jej nazwą.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 1. Podstawy 17
Klasy i obiekty
Każdy program w Javie składa się z klas, które są z kolei opisami obiektów. To podsta-
wowe pojęcia związane z programowaniem obiektowym. Będzie o tym mowa w roz-
dziale 3. Zanim jednak pozna się dokładnie klasy i obiekty, lepiej najpierw zapoznać
się z podstawowymi konstrukcjami języka Java. Osoby, które nie zetknęły się dotychczas
z programowaniem obiektowym, mogą potraktować obiekt jako pewien byt programi-
styczny. Może on przechowywać dane i wykonywać operacje, czyli różne zadania. Klasa
to z kolei definicja, opis takiego obiektu.
Skoro klasa definiuje obiekt, jest również jego typem. Czym jest typ obiektu? Oto jedna
z definicji: „Typ jest przypisany zmiennej, wyrażeniu lub innemu bytowi programistycz-
nemu (danej, obiektowi, funkcji, procedurze, operacji, metodzie, parametrowi, modułowi,
wyjątkowi, zdarzeniu). Specyfikuje on rodzaj wartości, które może przybierać ten byt.
(…) Jest to również ograniczenie kontekstu, w którym odwołanie do tego bytu może być
użyte w programie”6. Innymi słowy, typ obiektu określa po prostu, czym jest dany
obiekt.
Obiektem może być tak naprawdę wszystko. W życiu realnym tym mianem określimy
stół, krzesło, komputer, dom, samochód, radio… Każdy z obiektów ma pewne swoje cechy,
właściwości, które go opisują: wielkość, kolor, powierzchnię, wysokość. Co więcej,
każdy obiekt może składać się z innych obiektów (rysunek 1.9). Na przykład miesz-
kanie składa się z poszczególnych pomieszczeń, z których każde może być obiektem;
w każdym pomieszczeniu mamy z kolei inne obiekty: sprzęty domowe, meble itd.
Rysunek 1.9.
Obiekt może zawierać
inne obiekty
Obiekty, oprócz tego, że mają właściwości, mogą wykonywać różne funkcje, zadania.
Innymi słowy, każdy obiekt ma przypisany pewien zestaw poleceń, które potrafi wyko-
nywać. Na przykład samochód wypełnia polecenia „uruchom silnik”, „zgaś silnik”, „skręć
w prawo”, „przyspiesz” itp. Funkcje te składają się na interfejs udostępniany przez
ten samochód. Dzięki interfejsowi można wpływać na zachowanie samochodu i wy-
dawać mu polecenia.
W programowaniu jest bardzo podobnie. Za pomocą klas programista stara się opisać
obiekty, ich właściwości, zbudować konstrukcje, interfejs, dzięki któremu będzie można
6
K. Subieta, Wytwarzanie, integracja i testowanie systemów informatycznych, PJWSTK, Warszawa 1997.
b4ac40784ca6e05cededff5eda5a930f
b
18 Java. Praktyczny kurs
wydawać polecenia realizowane potem przez obiekty. Obiekt powstaje jednak dopie-
ro w trakcie działania programu jako instancja (wystąpienie, egzemplarz) danej klasy.
Obiektów danej klasy może być bardzo dużo. Przykładowo: jeśli klasą będzie Samochód,
to instancją tej klasy będzie konkretny egzemplarz o danym numerze seryjnym.
Różnią się one od siebie zakresem liczb, które można za ich pomocą reprezentować
— przedstawiono to w tabeli 1.1. Znajduje się w niej również informacja, ile bitów
potrzeba, aby zapisać liczbę przy zastosowaniu danego typu danych. Oczywiście im
szerszy zakres liczb, które można przedstawić, tym więcej potrzeba bitów.
Tabela 1.1. Zakresy typów arytmetycznych w Javie
Typ Liczba bitów Zakres
byte 8 Od –128 do 127
short 16 Od –32 768 do 32 767
int 32 Od –231 do 231–1
long 64 Od –263 do 263–1
7
Uwaga ta dotyczy również pozostałych typów podstawowych.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 1. Podstawy 19
Zakres liczbowy oraz liczbę bitów niezbędnych do zapisania liczby przy zastosowaniu
danego typu przedstawiono w tabeli 1.2.
Typ char
Typ char służy do reprezentacji znaków, czyli liter, cyfr, znaków przestankowych itp.
— ogólnie wszelkich znaków alfanumerycznych. W Javie ten typ jest 16-bitowy
(czyli do zapisania jednego znaku potrzeba dwóch bajtów) i opiera się na standardzie
Unicode (umożliwiającym przedstawienie znaków występujących w większości języków
świata). Ponieważ znaki są tak naprawdę reprezentowane jako 16-bitowe kody liczbowe,
typ ten zalicza się czasem do typów arytmetycznych.
Typ boolean
Typ logiczny ma nazwę boolean. Może on reprezentować jedynie dwie wartości — true
(prawda) i false (fałsz). Może być wykorzystywany przy sprawdzaniu różnych wa-
runków w instrukcjach if, a także w pętlach i innych konstrukcjach programistycznych.
Zostanie to pokazane na konkretnych przykładach już w rozdziale 2.
Lekcja 3. Komentarze
W większości języków programowania dostępna jest konstrukcja komentarza, która
służy do opisywania kodu źródłowego w języku naturalnym. Innymi słowy, służy do
wyrażenia tego, co programista miał na myśli, stosując daną instrukcję programu. Choć
początkowo komentowanie kodu źródłowego programu może wydawać się zupełnie
niepotrzebne, okazuje się, że jest to bardzo pożyteczny nawyk. Bardzo często bowiem
bywa tak, że po pewnym czasie sam programista ma problemy z analizą napisanego
przez siebie programu, nie mówiąc już o innych osobach, które miałyby wprowadzać
poprawki czy modyfikacje.
8
W tabeli podano wartości przybliżone. Zapis 3,4e38 (notacja wykładnicza, naukowa) oznacza
3,4×1038.
b4ac40784ca6e05cededff5eda5a930f
b
20 Java. Praktyczny kurs
W Javie istnieją dwa rodzaje komentarzy, oba zapożyczone z języków C i C++. Są to:
komentarz blokowy,
komentarz liniowy (wierszowy).
Komentarz blokowy
Komentarz blokowy rozpoczyna się od znaków /* i kończy znakami */. Wszystko, co
znajduje się pomiędzy tymi znakami, jest traktowane przez kompilator jako komen-
tarz i pomijane w procesie kompilacji. Przykład takiego komentarza jest widoczny na
listingu 1.3.
Listing 1.3.
class Main{
public static void main(String args[]){
/*
To mój pierwszy program w Javie
Wyświetla on na ekranie napis
*/
System.out.println("To jest napis.");
}
}
Listing 1.4.
class Main {
public /*komentarz*/ static void main(String args[]) {
System.out.println /*komentarz*/ ("To jest napis.");
}
}
Komentarzy blokowych nie wolno zagnieżdżać, to znaczy jeden nie może znaleźć się
w środku drugiego. Jeśli zatem spróbujemy dokonać kompilacji kodu przedstawionego na
listingu 1.5, kompilator zgłosi błąd. Ilustracja takiego błędu jest widoczna na rysunku 1.10.
Rysunek 1.10.
Próba zagnieżdżenia
komentarza blokowego
powoduje błąd
kompilacji
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 1. Podstawy 21
Listing 1.5.
class Main {
public static void main(String args[]) {
/*
Komentarzy blokowych nie
/*w tym miejscu wystąpi błąd*/
wolno zagnieżdżać
*/
System.out.println ("To jest napis.");
}
}
Wbrew pozorom to nie jest całkiem hipotetyczna sytuacja. Może się bowiem zdarzyć,
że programista nie zauważy, że w komentarz blokowy został ujęty większy fragment
kodu, i w jego środku ponownie zastosuje ten typ komentarza (choć trudno o to przy
korzystaniu z dobrego edytora programistycznego z kolorowaniem składni — wtedy
blok komentarza jest wyraźnie widoczny). Ważne, aby w takiej sytuacji od razu rozpoznać
typ błędu i wprowadzić odpowiednie poprawki.
Listing 1.6.
class Main {
public static void main(String args[]) {
//teraz wyświetlam napis
System.out.println ("To jest napis.");
}
}
Takiego komentarza nie można oczywiście użyć w środku instrukcji, gdyż wtedy jej
część stałaby się komentarzem i powstałby błąd kompilacji. Można natomiast w środku
komentarza liniowego wstawić komentarz blokowy, o ile zaczyna się i kończy w tej
samej linii. Taka konstrukcja wyglądałaby następująco:
//komentarz /*komentarz blokowy*/ liniowy
Jest to dopuszczalne i zgodne z regułami składni języka, jednak w praktyce mało przy-
datne. Komentarz liniowy może natomiast znaleźć się w środku komentarza blokowego:
/*
//ta konstrukcja jest poprawna
*/
b4ac40784ca6e05cededff5eda5a930f
b
22 Java. Praktyczny kurs
Ćwiczenie 3.1.
Na początku programu z listingu 1.1 dołącz komentarz blokowy opisujący jego działanie.
Wykonaj kompilację kodu.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.
Instrukcje języka
Java, tak jak każdy język programowania, jest wyposażona w szereg podstawowych in-
strukcji, które pozwalają programiście na wykonywanie najróżniejszych operacji. Ten
rozdział jest im w całości poświęcony. Przedstawione zostanie pojęcie zmiennej oraz to,
w jaki sposób należy deklarować zmienne i jakie operacje można na nich wykonywać.
Okaże się, jak wykorzystywać typy danych prezentowane w rozdziale 1. (przy opisie
zmiennych nie pojawi się jednak dokładne omówienie zmiennych obiektowych, które
zostaną opisane bliżej dopiero w kolejnym rozdziale).
Zmienne
Zmienne to konstrukcje programistyczne, które pozwalają na przechowywanie danych.
Każda zmienna ma swoją nazwę oraz typ. Nazwa to jednoznaczny identyfikator, dzięki
któremu istnieje możliwość odwoływania się do zmiennej w kodzie programu, nato-
miast typ określa, jakiego rodzaju dane zmienna może przechowywać (zmienne moż-
na więc potraktować jak nazwane „pudełka” przechowujące dane). Podstawowe typy
danych zostały przedstawione w lekcji 2. (w rozdziale 1.). Lekcja 4. jest poświęcona
problemowi deklaracji i przypisywania wartości zmiennym, lekcja 5. — wyświetlaniu
wartości zmiennych (ale także znaków specjalnych i napisów) na ekranie, natomiast
najdłuższa w tym podrozdziale lekcja 6. pokaże, jakie operacje (arytmetyczne, logiczne,
bitowe) można wykonywać na zmiennych.
b4ac40784ca6e05cededff5eda5a930f
b
24 Java. Praktyczny kurs
Proste deklaracje
Każda zmienna, zanim zostanie wykorzystana w kodzie programu, musi zostać zadekla-
rowana. Deklaracja polega na podaniu typu oraz nazwy zmiennej. Ogólnie taka konstruk-
cja wygląda następująco:
typ_zmiennej nazwa_zmiennej;
Listing 2.1.
class Main {
public static void main(String args[]) {
int liczba;
}
}
Powstała tu zmienna o nazwie liczba i typie int. Jak wiadomo z lekcji 2., typ int po-
zwala na przechowywanie liczb całkowitych w zakresie od –232 (–2 147 483 648) do
232–1 (2 147 483 647), można zatem przypisać tej zmiennej dowolną liczbę mieszczą-
cą się w tym przedziale. Przypisanie takie odbywa się za pomocą znaku równości. Jeśli
więc chcemy, aby zmienna liczba zawierała wartość 100, napiszemy:
liczba = 100;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 25
Listing 2.2.
class Main {
public static void main(String args[]) {
int liczba;
liczba = 100;
}
}
Zatem w konkretnym przypadku, jeśli chcemy zadeklarować zmienną liczba typu int
i od razu przypisać jej wartość 100, użyjemy konstrukcji w postaci:
int liczba = 100;
Listing 2.3.
class Main {
public static void main(String args[]) {
int liczba1;
byte liczba2, liczba3 = 100;
liczba1 = 10000;
liczba2 = 25;
}
}
b4ac40784ca6e05cededff5eda5a930f
b
26 Java. Praktyczny kurs
Powstały tutaj w sumie trzy zmienne: jedna typu int (liczba1) oraz dwie typu byte
(liczba2, liczba3). Przypomnijmy, że typ byte pozwala na przechowywanie wartości
całkowitych z zakresu od –128 do 127 (lekcja 2.). Jedynie zmienna liczba3 została zaini-
cjowana już w trakcie deklaracji i otrzymała wartość 100. Inicjacja zmiennych liczba1
i liczba2 odbyła się po deklaracji i otrzymały one wartości odpowiednio: 10000 i 25.
Nazwy zmiennych
Przy nazywaniu zmiennych2 obowiązują pewne zasady, których należy przestrzegać.
Nazwa taka może składać się z liter (zarówno małych, jak i wielkich), cyfr oraz znaku
podkreślenia3, nie może jednak zaczynać się od cyfry. Do tej zasady trzeba się bez-
względnie stosować, gdyż umieszczenie innych znaków w nazwie (np. dwukropka, wy-
krzyknika itp.) spowoduje natychmiast błąd kompilacji (rysunek 2.1).
Rysunek 2.1.
Niezgodna z zasadami
nazwa zmiennej
powoduje błąd
kompilacji
Można wykorzystać dowolne znaki będące literami, również te spoza ścisłego alfabetu ła-
cińskiego. Dopuszczalne jest zatem stosowanie wszelkich znaków narodowych (w tym
oczywiście polskich). To, czy będą one używane, zależy wyłącznie od indywidualnych
preferencji programisty i (lub) od specyfiki danego projektu programistycznego4.
2
Ogólniej rzecz ujmując — wszelkich identyfikatorów.
3
Dopuszczalny jest także znak dolara, ale przyjmuje się, że jest zarezerwowany dla narzędzi
przetwarzających kod i nie należy go stosować w nazwach zmiennych.
4
Należy zwrócić uwagę, że stosowanie w kodzie znaków spoza alfabetu łacińskiego może powodować
problemy przy przenoszeniu kodu na inne systemy. W takiej sytuacji najlepiej zapisywać pliki
w kodowaniu UTF-8.
5
Ten sposób zapisu określa się jako Lower Camel Case, w odróżnieniu od Upper Camel Case,
gdzie pierwsza litera pierwszego członu nazwy jest wielka.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 27
w przypadku typów prostych tworzy się zmienną, z której od razu można korzystać.
W przypadku typów referencyjnych zostanie w ten sposób zadeklarowane jedynie odnie-
sienie, inaczej referencja (ang. reference), któremu domyślnie zostanie przypisana
wartość pusta, nazywana w Javie null (w Pascalu stosowane jest słowo nil). Zmien-
nej referencyjnej po deklaracji należy przypisać odniesienie do utworzonego oddziel-
ną instrukcją obiektu. Dopiero wtedy można zacząć z niej korzystać.
Dla osób, które nie zetknęły się z językami obiektowymi takimi jak C++ czy Object
Pascal, jest to zapewne mało zrozumiałe. Pierwsze praktyczne przykłady zostaną
przedstawione już w tym rozdziale, w lekcji omawiającej tablice, natomiast dokład-
niejsze wyjaśnienie znajdzie się w rozdziale 3. opisującym podstawy programowania
obiektowego.
Ćwiczenie 4.1.
Zadeklaruj i jednocześnie zainicjuj dwie zmienne typu short. Nazwy zmiennych i przypi-
sywane wartości możesz wybrać dowolnie. Pamiętaj o zasadach nazewnictwa zmiennych
oraz zakresie wartości, jakie mogą być reprezentowane przez typ short.
Wiadomo już, jak wyświetlić ciąg znaków, jak jednak wyświetlić wartość zmiennej? Jest
to równie proste. Zamiast ujętego w znaki cudzysłowu6 napisu należy podać nazwę
zmiennej — schematycznie taka konstrukcja będzie wyglądała następująco:
System.out.println(nazwa_zmiennej);
6
Ściślej rzecz ujmując, należałoby tu mówić o znakach cudzysłowu prostego lub o znakach zastępczych
cudzysłowu.
b4ac40784ca6e05cededff5eda5a930f
b
28 Java. Praktyczny kurs
Zmodyfikujmy zatem program z listingu 2.2 tak, aby deklaracja i inicjalizacja odbywały
się w jednej linii, oraz wyświetlmy wartość zmiennej liczba na ekranie. Zadanie to można
zrealizować za pomocą kodu przedstawionego na listingu 2.4. Wynik jego działania za-
prezentowano z kolei na rysunku 2.2.
Listing 2.4.
class Main {
public static void main(String args[]) {
int liczba = 100;
System.out.println(liczba);
}
}
Rysunek 2.2.
Wyświetlenie na ekranie
wartości zmiennej typu int
W jaki sposób poradzić sobie, kiedy chce się jednocześnie mieć na ekranie zdefiniowany
przez siebie ciąg znaków oraz wartość danej zmiennej? Można dwukrotnie użyć instrukcji
System.out.println. Lepszym pomysłem jest jednak zastosowanie operatora + (plus)
w postaci:
System.out.println("napis" + nazwa_zmiennej);
Konkretny przykład zastosowania takiej konstrukcji jest widoczny na listingu 2.5, a wynik
jego działania na rysunku 2.3.
Listing 2.5.
class Main {
public static void main(String args[]) {
int liczba = 100;
System.out.println("Zmienna liczba ma wartość " + liczba);
}
}
Rysunek 2.3.
Zastosowanie operatora +
do jednoczesnego
wyświetlenia napisu
i wartości zmiennej
Jeśli wartość zmiennej ma znaleźć się w środku napisu albo też w jednej linii mają
być wyświetlone wartości kilku zmiennych, wystarczy kilkukrotnie użyć operatora +
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 29
i w ten sposób złożyć ciąg znaków, który ma się znaleźć na ekranie, z mniejszych
kawałków. Możliwa jest zatem konstrukcja w postaci:
System.out.println("napis1" + zmienna1 + "napis2" + zmienna2);
Załóżmy więc, że w programie zostaną zadeklarowane dwie zmienne typu short o na-
zwach pierwszaLiczba oraz drugaLiczba, o wartościach początkowych równych 25 i 75.
Zadaniem będzie wyświetlenie napisu: Wartość zmiennej pierwszaLiczba to 25,
a wartość zmiennej drugaLiczba to 75. Program wykonujący opisane czynności jest
przedstawiony na listingu 2.6.
Listing 2.6.
class Main {
public static void main(String args[]) {
short pierwszaLiczba = 25;
short drugaLiczba = 75;
System.out.println(
"Wartość zmiennej pierwszaLiczba to " +
pierwszaLiczba +
", a wartość zmiennej drugaLiczba to " +
drugaLiczba +
"."
);
}
}
Pewnym zaskoczeniem może być rozbicie instrukcji wyświetlającej dane aż na siedem li-
nii. Powód jest prosty: pełna linia ze względu na swoją długość nie zmieściłaby się ani na
ekranie, ani na wydruku. Lepiej więc samodzielnie podzielić ją na mniejsze części,
aby poprawić czytelność listingu. Przy takim podziale najlepiej kierować się zasadą,
że nie wolno przedzielić łańcucha znaków ujętego w cudzysłów, natomiast w każdym
innym miejscu, gdzie występuje spacja, można zamiast niej postawić znak końca linii
(przejść do nowego wiersza po wciśnięciu klawisza Enter).
Listing 2.7.
class Main {
public static void main(String args[]) {
System.out.println("Test polskich znaków:");
System.out.println("Małe litery: ąćęłńóśźż");
System.out.println("Wielkie litery: ĄĆĘŁŃÓŚŹŻ");
}
}
b4ac40784ca6e05cededff5eda5a930f
b
30 Java. Praktyczny kurs
Rysunek 2.4.
Uzyskanie polskich
znaków na konsoli
Windows wymaga
dodatkowych zabiegów
Skąd problem? Otóż podczas pisania treści programu np. w Notatniku polskie znaki
są kodowane w standardzie Windows 1250 (strona kodowa 1250). W Javie jest stosowany
system Unicode. Z kolei na konsoli Windows jest standardowo wykorzystywana stro-
na kodowa 852. Z przekodowaniem znaków z kodu źródłowego ze strony kodowej
1250 na standard Unicode kompilator radzi sobie sam, przyjmuje on jednak, że wyprowa-
dzanie znaków ma się odbywać również przy użyciu strony kodowej 1250 (a nie 852).
To właśnie powoduje, że zamiast polskich liter widać nieprzyjemne krzaczki (choć
w niektórych wersjach JDK i Windows, np. w Java 8 i Windows 8, kodowanie jest
poprawne).
Co zatem można w tej sprawie zrobić? Jednym ze sposobów jest zastosowanie niestan-
dardowego sposobu wyprowadzania znaków na ekran (za pomocą dodatkowych obiek-
tów). Ponieważ wymaga to wykonania szeregu instrukcji, które byłyby w tej chwili nie-
zrozumiałe, sposób ten będzie pokazany dopiero w rozdziale 6., w którym omówimy
system wejścia-wyjścia. Na szczęście drugim rozwiązaniem, i to bardzo prostym w reali-
zacji, jest zmiana strony kodowej oraz czcionki stosowanej w konsoli (wierszu poleceń)
systemu Windows.
W tym celu, po pierwsze, wydajemy polecenie chcp (ang. change code page), podając
numer strony kodowej, czyli: chcp 1250 (rysunek 2.5), a następnie z menu systemo-
wego konsoli wybieramy pozycję Właściwości oraz zakładkę Czcionka (rysunek 2.6).
Rodzaj czcionki należy zmienić na True Type (np. czcionka Lucida Console). Od tej
chwili polskie znaki będą wyświetlane poprawnie. Warto jednak pamiętać, że o ile zmiany
czcionki tym sposobem można dokonać na stałe (czyli każde kolejne okno będzie korzy-
stało z tego ustawienia, jeśli taka opcja została wybrana przy zatwierdzaniu zmian), o tyle
ustawienie strony kodowej za pomocą polecenia chcp dotyczy tylko okna bieżącego.
Rysunek 2.5.
Polecenie chcp pozwala
na zmianę strony
kodowej konsoli
Trzeba też wiedzieć, że kody źródłowe programów (np. treści listingów zapisywane
w plikach z rozszerzeniem .java) również muszą być zapisane w odpowiednim kodowa-
niu. Dla Windowsa kompilator przyjmuje standardowo, że ma do czynienia z kodowa-
niem CP 1250 (Windows-1250). Jeżeli jest to inny standard, np. UTF-8, przy kompilacji
mogą pojawić się komunikaty o błędach podobne do przedstawionych na rysunku 2.7.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 31
Rysunek 2.6.
Okno Właściwości
pozwala na zmianę
rodzaju używanej
czcionki
Rysunek 2.7. Niewłaściwe kodowanie pliku z kodem źródłowym spowodowało błędy kompilacji
np.:
javac –encoding utf8 Main.java
b4ac40784ca6e05cededff5eda5a930f
b
32 Java. Praktyczny kurs
Znaki specjalne
Wiadomo już, że aby wyświetlić na ekranie napis, należy ująć go w znaki cudzysłowu
oraz skorzystać z instrukcji System.out.println, np. System.out.println("napis").
Może więc nasunąć się pytanie: w jaki sposób wyprowadzić na ekran sam znak cudzy-
słowu, skoro jest on częścią instrukcji? Odpowiedź jest prosta: należy go poprzedzić
znakiem ukośnika, tak jak zaprezentowano na listingu 2.8.
Listing 2.8.
class Main {
public static void main(String args[]) {
System.out.println("Oto znak cudzysłowu: \"");
}
}
Jest to tak zwana sekwencja ucieczki (ang. escape sequence). Zaczyna się ona od znaku \
(ang. backslash, lewy ukośnik7), po którym występuje określenie znaku specjalnego,
jaki ma być wyświetlony na ekranie. Powstaje jednak kolejny problem: w jaki sposób
wyświetlić teraz sam znak ukośnika \? Odpowiedź jest dokładnie taka sama jak w po-
przednim przypadku: należy poprzedzić go dodatkowym znakiem \. Taka konstrukcja
będzie zatem wyglądać następująco:
System.out.println("Oto ukośnik: \\");
W ten sam sposób można wyprowadzić również inne znaki specjalne, takie jak znak
nowej linii czy znak apostrofu8. Stosowane w praktyce sekwencje znaków specjalnych są
przedstawione w tabeli 2.1.
7
Spotyka się też określenie „ukośnik wsteczny”.
8
W rzeczywistości należałoby mówić o znaku zastępczym apostrofu lub pseudoapostrofie; spotykane
jest też określenie apostrof prosty.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 33
Instrukcja System.out.print
Do wyświetlania danych na ekranie, oprócz znanej nam już dobrze instrukcji System.out.
println, można wykorzystać również bardzo podobną instrukcję System.out.print.
Jej działanie jest analogiczne, z tą różnicą, że nie następuje przejście do nowego wiersza.
Zatem wykonanie instrukcji w postaci:
System.out.println("napis1");
System.out.println("napis2");
spowoduje wyświetlenie na ekranie tylko jednego wiersza, który będzie zawierał po-
łączony tekst w postaci: napis1napis2.
Ćwiczenie 5.1.
Zadeklaruj dwie zmienne typu double. Przypisz im dwie różne wartości zmiennoprze-
cinkowe, np. 14.5 i 24.45. Wyświetl wartości tych zmiennych na ekranie w dwóch
wierszach. Nie korzystaj z instrukcji System.out.println.
Ćwiczenie 5.2.
Napisz program, który wyświetli na ekranie zbudowany ze znaków alfanumerycznych na-
pis widoczny na rysunku 2.8. Pamiętaj o wykorzystaniu sekwencji znaków specjalnych.
Rysunek 2.8.
Efekt wykonania
ćwiczenia 5.2
b4ac40784ca6e05cededff5eda5a930f
b
34 Java. Praktyczny kurs
operacje na zmiennych i jakie rządzą nimi prawa. Przedstawione zostaną operatory aryt-
metyczne, bitowe, logiczne, przypisania i porównywania, a także ich priorytety, czyli to,
które z nich są silniejsze, a które słabsze.
Operatory arytmetyczne
Operatory arytmetyczne służą, jak nietrudno się domyślić, do wykonywania operacji aryt-
metycznych, czyli znanego wszystkim dobrze mnożenia, dodawania itp. W tej grupie wy-
stępują jednak również mniej znane operatory, takie jak operator inkrementacji i de-
krementacji. Wszystkie one są zebrane w tabeli 2.2.
Podstawowe działania
W praktyce korzystanie z większości z tych operatorów sprowadza się do wykonywa-
nia typowych działań znanych z lekcji matematyki. Jeśli zatem trzeba dodać do siebie
dwie zmienne lub liczbę do zmiennej, używa się operatora +, gdy konieczne jest mnożenie
— operatora * itp. Oczywiście operacje arytmetyczne wykonuje się na zmiennych
arytmetycznych. Załóżmy zatem, że w kodzie są trzy zmienne typu całkowitoliczbowego
int i chcemy wykonać na nich kilka prostych operacji. Zobrazowano to na listingu 2.9;
dla zwiększenia czytelności opisu poszczególne linie zostały ponumerowane.
Listing 2.9.
class Main {
public static void main(String args[]) {
/*1*/ int a, b, c;
/*2*/ a = 10;
/*3*/ b = 20;
/*4*/ System.out.println("a = " + a + ",b = " + b);
/*5*/ c = b - a;
/*6*/ System.out.println("b - a = " + c);
/*7*/ c = a / 2;
/*8*/ System.out.println("a / 2 = " + c);
/*9*/ c = a * b;
/*10*/ System.out.println("a * b = " + c);
/*11*/ c = a + b;
/*12*/ System.out.println("a + b = " + c);
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 35
Rysunek 2.9.
Proste działania
arytmetyczne nie
sprawiają żadnych
problemów
Do operatorów arytmetycznych należy również znak %, przy czym, jak zostało wspo-
mniane wcześniej, nie oznacza on obliczania procentów, ale dzielenie modulo, czyli
resztę z dzielenia. Przykładowo: działanie 10 % 3 da w wyniku 1. Trójka zmieści się
bowiem w dziesięciu trzy razy, pozostawiając resztę 1 (3×3 = 9, 9+1 = 10). Podobnie
21%8 = 5, bo 2×8 = 16, 16+5 = 21.
Inkrementacja i dekrementacja
Operatory inkrementacji, czyli zwiększania (++), oraz dekrementacji, czyli zmniejsza-
nia (--), są z pewnością nieobce osobom znającym języki takie jak C, C++ czy PHP,
nowością będą natomiast dla programujących w Pascalu. Operator ++ zwiększa po
prostu wartość zmiennej o 1, a -- zmniejsza ją o 1. To jednak nie wszystko. Mogą one
bowiem występować w formie przedrostkowej (preinkrementacyjnej) lub przyrostkowej
(postinkrementacyjnej). Przykładowo: dla zmiennej o nazwie x forma przedrostkowa
będzie miała postać ++x, natomiast przyrostkowa — x++.
Listing 2.10.
class Main {
public static void main(String args[]) {
/*1*/ int x = 3, y;
/*2*/ System.out.println (x++);
b4ac40784ca6e05cededff5eda5a930f
b
36 Java. Praktyczny kurs
Rysunek 2.10.
Efekt wykonania
programu ilustrującego
działanie operatora ++
Listing 2.11.
class Main {
public static void main(String args[]) {
/*1*/ int x = 3, y;
/*2*/ System.out.println (x--);
/*3*/ System.out.println (--x);
/*4*/ System.out.println (x);
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 37
Rysunek 2.11.
Ilustracja działania
operatora dekrementacji
/*5*/ y = x--;
/*6*/ System.out.println (y);
/*7*/ y = --x;
/*8*/ System.out.println (y);
/*9*/ System.out.println (--y);
}
}
W linii 1. powstały zmienne x i y. Pierwszej z nich przypisano wartość 3, dokładnie tak jak
w programie z listingu 2.10. W linii 2. zastosowano formę przyrostkową operatora --,
zatem najpierw wartość zmiennej x (x = 3) pojawi się na ekranie, a dopiero potem
będzie zmniejszona o 1 (x = 2). W linii 3. jest dokładnie odwrotnie, to znaczy przez zasto-
sowanie formy przedrostkowej najpierw wartość zmiennej x zmniejsza się o 1 (x = 1),
a dopiero potem pojawia się na ekranie. W linii 4. jedyną operacją jest ponowne wy-
świetlenie wartości x (x = 1).
Jednak zarówno a, jak i c były typu int, czyli mogły przechowywać jedynie liczby cał-
kowite. Co się stanie, jeśli wynikiem dzielenia a / 2 nie będzie liczba całkowita? Czy
pojawi się ostrzeżenie kompilatora lub program podczas działania niespodziewanie
zasygnalizuje błąd? Można się o tym przekonać, kompilując i uruchamiając kod wi-
doczny na listingu 2.12.
b4ac40784ca6e05cededff5eda5a930f
b
38 Java. Praktyczny kurs
Listing 2.12.
class Main {
public static void main(String args[]) {
int a, b;
a = 9;
b = a / 2;
System.out.println("a = " + a);
System.out.println("Wynik działania a / 2 = " + b);
b = 8 / 3;
System.out.println("b = 8 / 3");
System.out.println("Wartość b to " + b);
}
}
Rysunek 2.12.
Wynik działania
programu z listingu 2.12
Podobnie jeśli do zmiennej b trafi wynik bezpośredniego dzielenia liczb (linia b = 8 / 3;),
prawdziwy wynik zostanie zaokrąglony w dół. Innymi słowy, Java sama dopasowuje
typy danych (mówiąc fachowo: dokonuje konwersji typu). Początkujący programiści
powinni zwrócić na ten fakt szczególną uwagę, gdyż niekiedy prowadzi to do wystąpie-
nia trudnych do wykrycia błędów w aplikacjach.
Czemu jednak kompilator nie ostrzega o tym, że taka konwersja zostanie dokonana?
Przede wszystkim wynik większości działań jest znany dopiero w trakcie działania pro-
gramu, zazwyczaj nie ma więc możliwości sprawdzenia już na etapie kompilacji, czy
wynik jest właściwego typu. Nie można też dopuścić, by program zgłaszał błędy lub
ostrzeżenia za każdym razem, kiedy wynik działania nie będzie dokładnie takiego typu
jak zmienna, której jest przypisywany. Dlatego też istnieją zdefiniowane w języku pro-
gramowania ogólne zasady konwersji typów, które są stosowane automatycznie (tzw.
konwersje automatyczne), kiedy tylko jest to możliwe. Jedna z takich reguł mówi właśnie,
że jeśli wynikiem operacji jest wartość zmiennoprzecinkowa, a wynik ten ma być
przypisany zmiennej całkowitoliczbowej, to część ułamkowa zostaje odrzucona.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 39
Listing 2.13.
class Main {
public static void main(String args[]) {
int a = 9.5;
System.out.println("Zmienna a ma wartość = " + a);
}
}
Jak widać na rysunku 2.13, kompilacja się nie udała, a kompilator zgłosił błąd. Tym ra-
zem bowiem następuje próba bezpośredniego przypisania liczby ułamkowej zmiennej
typu int, która takich wartości przechowywać nie może.
Rysunek 2.13. Próba przypisania wartości ułamkowej zmiennej typu int kończy się błędem kompilacji
Listing 2.14.
class Main {
public static void main(String args[]) {
int a = 2147483648;
System.out.println("Zmienna a ma wartość = " + a);
}
}
b4ac40784ca6e05cededff5eda5a930f
b
40 Java. Praktyczny kurs
Rysunek 2.14. Próba przypisania do zmiennej liczby przekraczającej dopuszczalny zakres wartości
Listing 2.15.
class Main {
public static void main(String args[]) {
int liczba = 2147483647;
liczba = liczba + 1;
System.out.println("Zmienna liczba ma wartość = " + liczba);
}
}
Na początku została zadeklarowana zmienna liczba typu int i przypisano jej maksymalną
wartość, którą można za pomocą tego typu przedstawić. Następnie do wartości tej
zmiennej dodano 1 (liczba = liczba + 1). Tym samym maksymalna wartość została
przekroczona. W ostatnim wierszu znajduje się instrukcja wyświetlająca zawartość
zmiennej liczba na ekranie. Czy taki program da się skompilować? Oczywiście tak.
Tym razem, inaczej niż w poprzednim przykładzie, zakres zostaje przekroczony dopiero
w trakcie działania aplikacji, w wyniku wykonania działań arytmetycznych. Co zatem
zostanie wyświetlone na ekranie? To, co widać na rysunku 2.15.
Rysunek 2.15.
Wynik działania
programu z listingu 2.15
Początkowo wynik może wydawać się dziwny. Da się jednak zauważyć, że wartość, która
pojawiła się na ekranie, to dolny zakres dla typu int (por. tabela 1.1), czyli minimalna
wartość, jaką może przyjąć zmienna tego typu. Zatem przekroczenie dopuszczalnej
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 41
wartości nie powoduje błędu, ale „zawinięcie” liczby. Zobrazowano to na rysunku 2.16.
Arytmetyka wygląda w tym przypadku następująco:
INT_MAX+1 = INT_MIN
INT_MAX+2 = INT_MIN+1
INT_MAX+3 = INT_MIN+2,
jak również:
INT_MIN–1 = INT_MAX
INT_MIN–2 = INT_MAX+1
INT_MIN–3 = INT_MAX+2,
gdzie INT_MIN to minimalna wartość, jaką może przyjąć zmienna typu int, czyli –231,
a INT_MAX to wartość maksymalna, czyli 231–1.
Operatory bitowe
Operatory bitowe służą, jak sama nazwa wskazuje, do wykonywania operacji na bitach.
Aby się z nimi bliżej zapoznać, warto przypomnieć sobie najpierw przynajmniej pod-
stawowe informacje na temat systemów liczbowych. W systemie dziesiętnym, z którego
korzystamy na co dzień, używanych jest dziesięć cyfr, od 0 do 9. W systemie dwójkowym
będą z kolei wykorzystywane jedynie dwie cyfry: 0 i 1 (w systemie szesnastkowym cyfry
od 0 do 9 i dodatkowo litery od A do F, a w systemie ósemkowym cyfry od 0 do 7). Ko-
lejne liczby są budowane z tych dwóch cyfr dokładnie tak samo jak w systemie dziesięt-
nym; przedstawiono to w tabeli 2.3. Widać wyraźnie, że np. 4 dziesiętnie to 100 dwójkowo,
a 10 dziesiętnie to 1010 dwójkowo.
b4ac40784ca6e05cededff5eda5a930f
b
42 Java. Praktyczny kurs
Operatory logiczne
Operacje logiczne można wykonywać na argumentach, które mają wartość logiczną
prawda lub fałsz. W językach programowania wartości te są oznaczane jako true
i false. W przypadku Javy oba te słowa muszą być zapisane dokładnie w taki sposób,
to znaczy małymi literami, inaczej nie zostaną rozpoznane przez kompilator. Jeśli za-
tem mamy przykładowe wyrażenie 0 < 1, to ma ono wartość logiczną true (prawda),
jako że niewątpliwie zero jest mniejsze od jedności, z kolei wyrażenie 2 > 5 ma wartość
logiczną false (fałsz), bo dwa nie jest większe od pięciu. Operacje logiczne to znane
ze szkoły logiczne AND (iloczyn), OR (suma) oraz NOT (negacja). Przedstawiono je w ta-
beli 2.5.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 43
Iloczyn logiczny
Wynikiem operacji AND (iloczyn logiczny) jest wartość true wtedy i tylko wtedy, kiedy
oba argumenty mają wartość true. W każdym innym przypadku wynikiem jest false.
Przedstawiono to w tabeli 2.6.
Suma logiczna
Wynikiem operacji OR (suma logiczna) jest wartość false wtedy i tylko wtedy, kiedy
oba argumenty mają wartość false. W każdym innym przypadku wynikiem jest true.
Przedstawiono to w tabeli 2.7.
Negacja logiczna
Operacja NOT (negacja logiczna) zmienia po prostu wartość argumentu na przeciwną.
Jeśli więc argument miał wartość true, będzie miał wartość false i odwrotnie — jeśli
miał wartość false, będzie miał wartość true. Zobrazowano to w tabeli 2.8.
b4ac40784ca6e05cededff5eda5a930f
b
44 Java. Praktyczny kurs
Operatory przypisania
Operacje przypisania są dwuargumentowe i powodują przypisanie argumentu prawo-
stronnego argumentowi lewostronnemu. Najprostsza taka operacja była już stosowana
wielokrotnie, odbywa się ona przy użyciu operatora = (równa się). Napisanie a = 5
oznacza, że zmiennej a ma być przypisana wartość 5.
Oprócz prostego operatora = w Javie występuje szereg operatorów złożonych, tzn. takich,
w których przypisaniu towarzyszy dodatkowa operacja arytmetyczna lub bitowa. Przykła-
dowo istnieje operator +=, który oznacza: przypisz argumentowi z lewej strony war-
tość wynikającą z dodawania argumentu znajdującego się z lewej strony i argumentu
znajdującego się z prawej strony operatora.
Choć brzmi to z początku nieco zawile, w rzeczywistości jest bardzo proste i znacznie
upraszcza niektóre konstrukcje programistyczne. Przykładowy zapis:
a += b
Listing 2.16.
class Main {
public static void main(String args[]) {
int a = 15;
a += 10;
System.out.println("Zmienna a ma wartość = " + a);
}
}
W Javie występuje cała grupa tych operatorów (są one zebrane w tabeli 2.9). Schematycz-
nie można przedstawić ich znaczenie następująco:
arg1 op= arg2
co oznacza działanie:
arg1 = arg1 op arg2
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 45
Priorytety operatorów
Sama znajomość operatorów to jednak nie wszystko. Trzeba jeszcze wiedzieć, jaki mają
one priorytet, czyli jaka jest kolejność ich wykonywania. Wiadomo z matematyki, że
np. mnożenie jest silniejsze od dodawania, zatem najpierw mnożymy, potem dodajemy.
W Javie jest podobnie — siła każdego operatora jest ściśle określona. Zostało to przed-
stawione w tabeli 2.119. Im wyższa pozycja w tabeli, tym wyższy priorytet operatora.
Operatory znajdujące się na jednym poziomie (w jednym wierszu) mają ten sam priorytet.
9
Tabela nie przedstawia wszystkich operatorów występujących w Javie, ale jedynie omówione w bieżącej
lekcji.
b4ac40784ca6e05cededff5eda5a930f
b
46 Java. Praktyczny kurs
Ćwiczenie 6.1.
Zadeklaruj trzy zmienne typu int: a, b i c. Przypisz im dowolne wartości całkowite. Wy-
konaj działanie a % b % c. Wynik tego działania przypisz czwartej zmiennej o nazwie
wynik, jej wartość wyświetl na ekranie. Spróbuj tak dobrać wartości zmiennych, aby
wynikiem nie było 0.
Ćwiczenie 6.2.
Zadeklaruj zmienną typu int o dowolnej nazwie. Przypisz jej wartość 256. Wykonaj trzy
działania: przesunięcie bitowe w prawo o dwa miejsca, przesunięcie bitowe w lewo o dwa
miejsca oraz przesunięcie bitowe w prawo o dwa miejsca z wypełnieniem zerami. Wynik
wszystkich trzech działań wyświetl na ekranie. Zaobserwuj zachodzące prawidłowości.
Ćwiczenie 6.3.
Zadeklaruj dwie zmienne typu int. Przypisz im dowolne wartości oraz wykonaj na nich
działania sumy bitowej oraz iloczynu bitowego. Wyświetl wyniki działań na ekranie.
Ćwiczenie 6.4.
Zadeklaruj zmienną typu int o dowolnej nazwie i przypisz jej dowolną wartość cał-
kowitą. Dwukrotnie wykonaj na niej operację XOR, wykorzystując jako drugi argument
również dowolną liczbę całkowitą. Użyj operatora przypisania ^=. Zaobserwuj otrzymany
wynik i zastanów się, czemu ma on właśnie taką postać.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 47
Ćwiczenie 6.5.
Umieść w programie zmienną a o wartości 55 i zmienną b o wartości 88 oraz instrukcję
wykonującą następujące działanie:
a += (b - 3 ^ 4 << 2 | 8 - b >>> 8 * 2) - (a & a - 1);
Instrukcje sterujące
Lekcja 7. Instrukcja warunkowa if…else
Instrukcje warunkowe służą, jak sama nazwa wskazuje, do sprawdzania warunków.
Dzięki temu w zależności od tego, czy dany warunek jest prawdziwy, czy nie, można
wykonać różne działania. W Javie instrukcja warunkowa ma ogólną postać:
if (warunek) {
//instrukcje do wykonania, kiedy warunek jest prawdziwy
}
else {
//instrukcje do wykonania, kiedy warunek jest fałszywy
}
Zobaczmy, jak to wygląda w praktyce: sprawdzimy, czy zmienna typu int jest większa
od 0, i wyświetlimy odpowiedni komunikat na ekranie. Kod realizujący takie zadanie
jest widoczny na listingu 2.17.
Listing 2.17.
class Main {
public static void main(String args[]) {
int liczba = 15;
if (liczba > 0) {
System.out.println("Zmienna liczba jest większa od zera.");
}
else {
10
Nawiasy klamrowe można pominąć, jeśli w bloku ma być wykonana tylko jedna instrukcja.
b4ac40784ca6e05cededff5eda5a930f
b
48 Java. Praktyczny kurs
Na początku deklarujemy zmienną liczba typu int i przypisujemy jej wartość 15. Na-
stępnie za pomocą instrukcji if sprawdzamy, czy jest ona większa od 0. Wykorzystu-
jemy w tym celu operator porównywania < (lekcja 6.). Ponieważ zmienna liczba
otrzymała stałą wartość równą 15, która na pewno jest większa od 0, zostanie oczywiście
wyświetlony napis: Zmienna liczba jest większa od zera. Jeśli przypiszemy jej
wartość ujemną lub równą 0, to zostanie wykonany blok else.
Listing 2.18.
class Main {
public static void main (String args[]) {
//deklaracja zmiennych
int A = 1, B = 1, C = -2;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 49
Kolejny krok to sprawdzenie, czy delta jest mniejsza od 0. Jeśli jest, oznacza to, że
równanie nie ma rozwiązań w zbiorze liczb rzeczywistych, wyświetlamy więc sto-
sowny komunikat na ekranie. Gdy jednak delta nie jest mniejsza od 0, przystępujemy
do sprawdzenia kolejnych warunków. Jeżeli delta jest równa 0, można od razu obli-
b4ac40784ca6e05cededff5eda5a930f
b
50 Java. Praktyczny kurs
oraz:
wynik = (-B - Math.sqrt(delta)) / (2 * A);
Rezultat obliczeń wyświetlamy rzecz jasna na ekranie, tak jak na rysunku 2.17. Nie-
znana nam do tej pory instrukcja Math.sqrt(delta) powoduje obliczenie pierwiastka
kwadratowego (drugiego stopnia) z wartości zawartej w zmiennej delta.
Rysunek 2.17.
Program obliczający
rozwiązania równania
kwadratowego
Zagnieżdżanie instrukcji if sprawdza się dobrze w tak prostym przykładzie jak po-
wyższy, jednak z każdym kolejnym poziomem staje się coraz bardziej nieczytelne.
Nadmiernemu zagnieżdżaniu można zapobiec przez zastosowanie nieco zmodyfiko-
wanej instrukcji w postaci if…else if. Załóżmy, że mamy znaną już z listingu 2.18
konstrukcję w postaci:
if(warunek1){
instrukcje1
}
else{
if(warunek2){
instrukcje2
}
else{
if(warunek3){
instrukcje3
}
else{
instrukcje4
}
}
}
Innymi słowy, trzeba sprawdzić po kolei warunki warunek1, warunek2 i warunek3 i w za-
leżności od tego, które są prawdziwe, wykonać instrukcje instrukcje1, instrukcje2,
instrukcje3 lub instrukcje4. Zatem instrukcje1 są wykonywane, kiedy warunek1
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 51
Taki zapis tłumaczy się następująco: jeśli prawdziwy jest warunek1, wykonaj instrukcje1;
w przeciwnym razie, jeżeli prawdziwy jest warunek2, wykonaj instrukcje2; w prze-
ciwnym razie, jeśli prawdziwy jest warunek3, wykonaj instrukcje3; w przeciwnym
razie wykonaj instrukcje4.
Taka konstrukcja pozwala uprościć nieco przykładowy kod z listingu 2.18 obliczający
pierwiastki równania kwadratowego. Zamiast sprawdzać, czy delta jest mniejsza, więk-
sza, czy równa 0, za pomocą zagnieżdżonej instrukcji if, łatwiej skorzystać z instrukcji
if…else if. Zostało to zobrazowane na listingu 2.19.
Listing 2.19.
class Main {
public static void main (String args[]) {
//deklaracja zmiennych
int A = 1, B = 1, C = -2;
b4ac40784ca6e05cededff5eda5a930f
b
52 Java. Praktyczny kurs
}
//jeśli delta większa od zera
else if(delta > 0) {
double wynik;
//obliczenie wyników
wynik = (- B + Math.sqrt(delta)) / (2 * A);
System.out.print ("Rozwiązanie: x1 = " + wynik);
wynik = (- B - Math.sqrt(delta)) / (2 * A);
System.out.println (", x2 = " + wynik);
}
}
}
}
Przy takim zapisie, jeżeli dwa pierwsze warunki są fałszywe, ostatni musi być prawdziwy.
Przecież jeśli delta (która jest zmienną typu double) nie jest mniejsza od 0 (warunek
pierwszy) i nie jest równa 0 (warunek drugi), to z pewnością jest większa od 0. Można
więc pominąć sprawdzanie warunku trzeciego. Równie dobrze można użyć zatem frag-
mentu w postaci:
if(delta < 0){
//instrukcje
}
else if(delta == 0){
//instrukcje
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 53
}
else{
//instrukcje
}
Ćwiczenie 7.1.
Zadeklaruj dwie zmienne typu int: a i b. Przypisz im dowolne wartości całkowite. Użyj
instrukcji if do sprawdzenia, czy dzielenie modulo a przez b daje w wyniku 0. Wyświetl
stosowny komunikat na ekranie.
Ćwiczenie 7.2.
Napisz program, którego zadaniem będzie stwierdzenie, czy równanie kwadratowe
ma rozwiązanie w zbiorze liczb rzeczywistych.
Ćwiczenie 7.3.
Napisz program obliczający pole prostokąta o długości i wysokości podanej w postaci
dwóch zmiennych. Program powinien reagować odpowiednio w sytuacji, gdy dane
będą niepoprawne (jedna lub obie wartości będą ujemne lub równe zero) — nie należy
wtedy obliczać pola, ale wyświetlić stosowny komunikat.
Ćwiczenie 7.4.
Napisz program podający informację, czy kwadrat o danej długości boku może być
wpisany w okrąg o danym promieniu.
Ćwiczenie 7.5.
Napisz program podający informację, czy z trzech odcinków o zadanych długościach
da się zbudować trójkąt prostokątny.
b4ac40784ca6e05cededff5eda5a930f
b
54 Java. Praktyczny kurs
Instrukcja switch
Instrukcja switch pozwala w wygodny i przejrzysty sposób sprawdzić ciąg warunków
i wykonywać różny kod w zależności od tego, czy są one prawdziwe, czy fałszywe.
W najprostszej postaci może być ona odpowiednikiem ciągu if…else if, w którym jako
warunek jest wykorzystywane porównywanie zmiennej z wybraną liczbą. Daje ona
programiście dodatkowe możliwości, jak choćby wykonania tego samego kodu dla
kilku warunków. Taki oto przykładowy zapis:
if(liczba == 1){
instrukcje1
}
else if (liczba == 2){
instrukcje2
}
else if(liczba == 3){
instrukcje3
}
else{
instrukcje4
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 55
Należy ją rozumieć jako: sprawdź wartość wyrażenia wyrażenie; jeśli jest to wartość1,
wykonaj instrukcje1 i przerwij wykonywanie bloku switch (instrukcja break); jeżeli
jest to wartość2, wykonaj instrukcje2 i przerwij wykonywanie bloku switch; jeśli
jest to wartość3, wykonaj instrukcje3 i przerwij wykonywanie bloku switch; jeżeli
w żadnym z powyższych przypadków dopasowanie nie nastąpiło, wykonaj instrukcje4
i zakończ blok switch. Oczywiście w praktyce liczba bloków case nie jest ograniczona
i może ich być dowolnie wiele.
Listing 2.20.
class Main {
public static void main (String args[]) {
int liczba = 25;
switch(liczba){
case 25 :
System.out.println("liczba = 25");
break;
case 15 :
System.out.println("liczba = 15");
break;
default :
System.out.println("Zmienna liczba nie jest równa ani 15, ani 25.");
}
}
}
Na początku deklarujemy zmienną o nazwie liczba i typie int, czyli taką, która może
przechowywać liczby całkowite, i przypisujemy jej wartość 25. Następnie używamy
instrukcji switch do sprawdzenia stanu zmiennej. W tym wypadku wartością wyrażenia
będącego parametrem instrukcji switch jest oczywiście wartość zapisana w zmiennej
liczba. Nic nie stoi jednak na przeszkodzie, aby parametr ten był wyliczany dynamicz-
nie w samej instrukcji. Jest to widoczne w przykładzie na listingu 2.21.
Listing 2.21.
class Main {
public static void main (String args[]) {
int liczba1 = 2;
int liczba2 = 1;
switch(liczba1 * 5 / (liczba2 + 1)){
case 5 :
System.out.println("Wartość wyrażenia to 5.");
break;
case 15 :
System.out.println("Wartość wyrażenia to 15.");
break;
default :
System.out.println("Wartość wyrażenia nie jest równa ani 5, ani 15.");
}
b4ac40784ca6e05cededff5eda5a930f
b
56 Java. Praktyczny kurs
}
}
Szczególną uwagę należy jednak zwrócić na instrukcję break, która przerywa wykonywa-
nie danego bloku case i tym samym powoduje opuszczenie instrukcji switch. Jej przypad-
kowe pominięcie może doprowadzić do nieoczekiwanych wyników i błędów w progra-
mie. Aby przekonać się, w jaki sposób działa instrukcja switch bez instrukcji break,
wystarczy zmodyfikować listing 2.20, usuwając z niego wszystkie słowa break. Powstanie
wtedy kod widoczny na listingu 2.22.
Listing 2.22.
class Main {
public static void main (String args[]) {
int liczba = 25;
switch(liczba){
case 25:
System.out.println("liczba = 25");
case 15:
System.out.println("liczba = 15");
default:
System.out.println("Zmienna liczba nie jest równa ani 15, ani 25.");
}
}
}
Wynik jego działania może zadziwić — jest widoczny na rysunku 2.18. Ten program
raczej nie jest w stanie podać, jaką tak naprawdę wartość ma zmienna liczba, praw-
da? Jak zatem działa przedstawiony kod? Otóż jeśli w którymś z bloków (przypadków)
case zostanie wykryta zgodność z wyrażeniem występującym za switch, zostaną wy-
konane wszystkie dalsze instrukcje, aż do napotkania instrukcji break (a dokładniej:
dowolnej instrukcji powodującej przerwanie działania bloku switch) lub dotarcia do
końca instrukcji switch.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 57
Rysunek 2.18.
Wynik braku instrukcji
break w bloku switch
W kodzie z listingu 2.22 zgodność jest stwierdzona już w pierwszym przypadku (case
25), zostaje zatem wykonana instrukcja System.out.println("liczba = 25"). Ponieważ
jednak nie występuje po niej break, w dalszej kolejności zostaje wykonana instrukcja
System.out.println("liczba = 15") (nie ma znaczenia, że należy ona do przypadku
case 15!). Po tej instrukcji również nie ma break, zatem zostanie wykonana instrukcja
występująca po default, czyli System.out.println("Zmienna liczba nie jest równa
ani 15, ani 25.").
Ktoś może spytać: czemu w takim razie break nie jest obligatoryjny, tak jak np. w języ-
ku C#11? Chodzi o to, że takie właśnie zachowanie bloku switch dla wprawnego pro-
gramisty może być ułatwieniem przy pisaniu kodu (bo pozwala na wykonanie wielu
bloków case dla jednej wartości wyrażenia). Właściwość tę można sprawnie wykorzy-
stać przy tworzeniu aplikacji. Początkujący powinni jednak zapamiętać, że po każdym
przypadku case należy umieścić instrukcję break, co pozwoli uniknąć niespodziewanych
problemów.
Operator warunkowy
Operator warunkowy ma postać:
warunek ? wartość1 : wartość2
Oznacza ona: jeżeli warunek jest prawdziwy, podstaw za wartość wyrażenia wartość1,
w przeciwnym wypadku podstaw wartość2. Sprawdźmy w praktyce, jak może wyglądać
jego wykorzystanie — zobrazowano to w kodzie widocznym na listingu 2.23.
Listing 2.23.
class Main {
public static void main (String args[]){
int liczba1 = 10;
int liczba2;
liczba2 = liczba1 < 0 ? -1 : 1;
System.out.println(liczba2);
}
}
11
W języku C# każdy przypadek case musi kończyć się instrukcją przekazującą sterowanie poza blok
switch. Najczęściej jest to instrukcja break, ale można ją również zastąpić np. przez instrukcję goto.
b4ac40784ca6e05cededff5eda5a930f
b
58 Java. Praktyczny kurs
Ćwiczenie 8.1.
Napisz instrukcję switch zawierającą 10 bloków case sprawdzających kolejne wartości
całkowite od 0 do 9. Pamiętaj o instrukcjach break.
Ćwiczenie 8.2.
Zadeklaruj zmienną typu boolean. Wykorzystaj wyrażenie warunkowe do sprawdzenia,
czy wynikiem dowolnego dzielenia modulo jest wartość 0. Jeśli tak, przypisz tej zmiennej
wartość true, w przeciwnym wypadku przypisz wartość false.
Ćwiczenie 8.3.
Zmień kod z listingu 2.23 tak, aby badanie stanu zmiennej liczba1 było wykonywane
za pomocą instrukcji warunkowej if…else.
Ćwiczenie 8.4.
Zadeklaruj zmienną przechowującą liczby całkowite i przypisz jej dowolną wartość po-
czątkową. Napisz instrukcję, która w przypadku gdy wartość zmiennej jest mniejsza od
zera, zamieni tę wartość na dodatnią (zachowa się jak wartość bezwzględna w mate-
matyce). Użyj operatora warunkowego.
Ćwiczenie 8.5.
Napisz instrukcję switch badającą wartość pewnej zmiennej typu całkowitoliczbowego
i wyświetlającą komunikaty w następujących sytuacjach: a) zmienna ma wartość 1, 4,
8; b) zmienna ma wartość 2, 3, 7; c) wszystkie inne przypadki. Postaraj się całość zapisać
możliwie krótko.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 59
Lekcja 9. Pętle
Pętle są konstrukcjami programistycznymi, które pozwalają na wykonywanie powta-
rzających się czynności. Przykładowo aby wyświetlić na ekranie dziesięć razy dowol-
ny napis, najłatwiej skorzystać właśnie z odpowiedniej pętli. Oczywiście można też
dziesięć razy napisać w kodzie programu System.out.println("napis"), jednak będzie
to z pewnością niezbyt wygodne. W tej lekcji zostaną przedstawione wszystkie wy-
stępujące w Javie postacie pętli, czyli pętle for, while oraz do…while, a także wpro-
wadzona w Java 5.0 (1.5) pętla typu foreach. Omówione zostaną także różnice wy-
stępujące między nimi oraz przykłady wykorzystania różnych typów pętli.
Pętla for
Pętla for ma ogólną postać:
for (wyrażenie początkowe; wyrażenie warunkowe; wyrażenie modyfikujące){
instrukcje do wykonania
}
Listing 2.24.
class Main {
public static void main (String args[]) {
for(int i = 0; i < 10; i++){
System.out.println("Pętle w Javie");
}
}
}
Taką konstrukcję należy rozumieć następująco: zadeklaruj zmienną i i przypisz jej war-
tość 0 (int i = 0), następnie tak długo, jak długo wartość i jest mniejsza od 10 (i < 10),
wykonuj instrukcję System.out.println("Pętle w Javie") oraz zwiększaj wartość i o 1
(i++). Dzięki temu na ekranie pojawi się dziesięć razy napis Pętle w Javie (rysunek 2.19).
Zmienna i jest nazywana zmienną iteracyjną, czyli kontrolującą kolejne przebiegi (ite-
racje) pętli.
Jeśli chcemy zobaczyć, jak zmienia się stan zmiennej i w trakcie kolejnych przebiegów,
możemy zmodyfikować instrukcję wyświetlającą napis, tak aby podawała i tę informację.
Wystarczy drobna poprawka w postaci:
for(int i = 0; i < 10; i++){
System.out.println("[i = " + i + "] Pętle w Javie");
}
b4ac40784ca6e05cededff5eda5a930f
b
60 Java. Praktyczny kurs
Rysunek 2.19.
Wynik działania prostej
pętli typu for
Po jej wprowadzeniu w każdej linii będzie wyświetlana również wartość i, tak jak zostało
to przedstawione na rysunku 2.20.
Rysunek 2.20.
Niewielka modyfikacja
kodu pozwala podejrzeć
stan zmiennej iteracyjnej
Zmieńmy zatem program z listingu 2.24 tak, aby wyrażenie modyfikujące znalazło się
wewnątrz pętli. Zostało to zobrazowane na listingu 2.25.
Listing 2.25.
class Main {
public static void main (String args[]) {
for(int i = 0; i < 10;){
System.out.println("Pętle w Javie");
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 61
i++;
}
}
}
Skoro udało nam się usunąć wyrażenie modyfikujące z nawiasu okrągłego pętli, spróbuj-
my dokonać takiego zabiegu również z wyrażeniem początkowym. Jest to prosty zabieg
techniczny. Schematycznie taka konstrukcja wygląda następująco:
wyrażenie początkowe;
for (; wyrażenie warunkowe;){
instrukcje do wykonania
wyrażenie modyfikujące;
}
Jak może to wyglądać w praktyce, zobrazowano na listingu 2.26. Całe wyrażenie począt-
kowe zostało przeniesione po prostu przed pętlę. To jest nadal w pełni funkcjonalny
odpowiednik programu z listingu 2.24. Ponownie należy zwrócić uwagę na umiejsco-
wienie średników pętli for. Oba są niezbędne do prawidłowego działania kodu.
Listing 2.26.
class Main {
public static void main (String args[]) {
int i = 0;
for(; i < 10;){
System.out.println("Pętle w Javie");
i++;
}
}
}
Listing 2.27.
class Main {
public static void main (String args[]) {
int i = 0;
for(; i++ < 10;){
System.out.println("Pętle w Javie");
}
}
}
b4ac40784ca6e05cededff5eda5a930f
b
62 Java. Praktyczny kurs
Pętla while
Pętla typu while służy, podobnie jak pętla for, do wykonywania powtarzających się
czynności. Pętlę for zwykle wykorzystuje się, kiedy liczba powtarzanych operacji jest
znana (na przykład zapisana w jakiejś zmiennej), natomiast while, kiedy nie jest z góry
znana (na przykład wynika z działania jakiejś funkcji). Jest to jednak podział umowny:
oba typy pętli można zapisać w taki sposób, aby były swoimi funkcjonalnymi odpowied-
nikami. Ogólna postać pętli while wygląda następująco:
while (wyrażenie warunkowe){
instrukcje;
}
Listing 2.28.
class Main {
public static void main (String args[]) {
int i = 0;
while(i < 10){
System.out.println("Pętle w Javie");
i++;
}
}
}
Nic nie stoi na przeszkodzie, aby tak jak w przypadku pętli for, wyrażenie warunkowe
było jednocześnie wyrażeniem modyfikującym. Pętla taka wyglądałaby wtedy jak na
listingu 2.29.
Listing 2.29.
class Main {
public static void main (String args[]) {
int i = 0;
while(i++ < 10){
System.out.println("Pętle w Javie");
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 63
}
}
}
Należy jednak przy tym zwrócić uwagę, że choć programy z listingów 2.28 i 2.29
wykonują to samo zadanie, nie są to w pełni funkcjonalne odpowiedniki. Można to
zauważyć po dodaniu instrukcji wyświetlającej stan zmiennej i w obu wersjach kodu.
Wystarczy zmodyfikować instrukcję System.out.println("Pętle w Javie") tak samo
jak w przypadku pętli for: System.out.println("[i = " + i + "] Pętle w Javie").
Wynik działania kodu, kiedy zmienna i jest modyfikowana wewnątrz pętli (tak jak na li-
stingu 2.28), będzie taki sam jak na rysunku 2.20, natomiast wynik jego działania, kiedy
zmienna ta jest modyfikowana w wyrażeniu warunkowym (tak jak na listingu 2.29),
przedstawiony jest na rysunku 2.21.
Rysunek 2.21.
Stan zmiennej i, kiedy
jest modyfikowana
w wyrażeniu
warunkowym
Pętla do…while
Odmianą pętli while jest pętla do…while, której schematyczna postać wygląda nastę-
pująco:
do{
instrukcje;
}
while(warunek);
b4ac40784ca6e05cededff5eda5a930f
b
64 Java. Praktyczny kurs
Listing 2.30.
class Main {
public static void main (String args[]) {
int i = 0;
do{
System.out.println("[i = " + i + "] Pętle w Javie");
}
while(i++ < 9);
}
}
Zwróćmy uwagę na to, jak w tej chwili wygląda warunek. Od razu widać podstawową
różnicę w stosunku do pętli while. Otóż w pętli while najpierw jest sprawdzany waru-
nek, a dopiero potem są wykonywane instrukcje. W przypadku pętli do…while jest dokład-
nie odwrotnie — najpierw są wykonywane instrukcje, a dopiero potem jest sprawdza-
ny warunek. Dlatego też tym razem trzeba sprawdzić, czy i jest mniejsze od 9. Gdyby
pozostawić starą postać wyrażenia warunkowego: i++ < 10, napis zostałby wyświetlony
jedenaście razy.
Warunek teraz pozostaje w starej postaci i otrzymujemy odpowiednik pętli while. Ten
sam efekt można też uzyskać bez przesuwania wyrażenia modyfikującego do wnętrza
pętli, wystarczy użyć przedrostkowej wersji operatora ++. Niech to jednak pozostanie
zadaniem do samodzielnego wykonania.
12
Osoby znające język programowania Pascal z pewnością zauważą w tej konstrukcji podobieństwo
do pętli repeat…until.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 65
}
while(i < 0);
Warunek w tej postaci jest ewidentnie fałszywy, jako że zmienna i już w trakcie inicjacji
jest równa 0 (nie może być więc jednocześnie mniejsza od 0!). Mimo to po wykona-
niu powyższego kodu na ekranie pojawi się jeden napis Pętle w Javie. Jest to najlepszy
dowód na to, że warunek jest sprawdzany nie przed przebiegiem pętli, ale dopiero po
każdym przebiegu.
W takim przypadku w kolejnych przebiegach pętli pod nazwa będzie podstawiana war-
tość kolejnej komórki. Pętla będzie działała tak długo, aż zostaną przejrzane wszyst-
kie elementy tablicy (lub kolekcji) typu typ bądź też zostanie przerwana za pomocą
jednej z instrukcji pozwalających na taką operację (lekcja 10.). Przykład użycia takiej
pętli został przedstawiony na listingu 2.31.
Listing 2.31.
class Main {
public static void main (String args[]) {
int tab[] = {1, 2, 3, 4, 5, 4, 3, 2, 1};
for(int val: tab){
System.out.println(val);
}
}
}
Ćwiczenie 9.1.
Wykorzystując pętlę for, napisz program, który wyświetli liczby całkowite od 1 do 10
podzielne przez 2.
13
Ściślej rzecz ujmując: po obiekcie udostępniającym tzw. iterator.
b4ac40784ca6e05cededff5eda5a930f
b
66 Java. Praktyczny kurs
Ćwiczenie 9.2.
Nie zmieniając żadnej instrukcji wewnątrz pętli, zmodyfikuj kod z listingu 2.28 w taki
sposób, aby był funkcjonalnym odpowiednikiem kodu z listingu 2.29.
Ćwiczenie 9.3.
Wykorzystując pętlę while, napisz program, który wyświetli liczby całkowite od 1 do 20
niepodzielne przez 3.
Ćwiczenie 9.4.
Zmodyfikuj kod z listingu 2.30 w taki sposób, aby w wyrażeniu warunkowym pętli
do…while zamiast operatora < wykorzystać <=.
Ćwiczenie 9.5.
Napisz program, który wyświetli na ekranie liczby od 1 do 20 i zaznaczy przy każdej
z nich, czy jest to liczba parzysta, czy nieparzysta. Zrób to:
a) wykorzystując pętlę for,
b) wykorzystując pętlę while,
c) wykorzystując pętlę do…while.
Instrukcja break
Instrukcja break pojawiła się już w lekcji 8. przy omawianiu instrukcji switch. Znacze-
nie break w języku programowania jest zgodne z nazwą, w języku angielskim break
znaczy „przerywać”. Dokładnie tak zachowywała się ta konstrukcja w przypadku in-
strukcji switch, tak też zachowuje się w przypadku pętli — po prostu przerywa ich
wykonanie. Dzięki temu można np. tak zmodyfikować pętlę for, aby wyrażenie wa-
runkowe znalazło się wewnątrz niej. Kod realizujący takie zadanie został przedstawiony
na listingu 2.32.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 67
Listing 2.32.
class Main {
public static void main (String args[]) {
for(int i = 0; ; i++){
System.out.println("Pętle w Javie " + i);
if(i == 9){
break;
}
}
}
}
Sama pętla działa w taki sposób, że w każdym przebiegu wykonywana jest instrukcja
warunkowa if sprawdzająca, czy zmienna i osiągnęła wartość 9, czyli badająca warunek
i == 9. Jeśli ten warunek będzie prawdziwy, będzie to oznaczało, że na ekranie zostało
wyświetlonych 10 napisów, zostanie więc wykonana instrukcja break, która przerwie
działanie pętli.
Na tym etapie można też pokazać, jak pozbyć się wszystkich wyrażeń z nawiasu okrągłe-
go pętli. Wyrażenie początkowe przenosi się przed pętlę, a modyfikujące i warunko-
we do jej wnętrza. Zostało to przedstawione na listingu 2.33. Tego typu konstrukcja
niegdyś była tylko ciekawostką programistyczną, raczej niestosowaną w praktycz-
nych rozwiązaniach, obecnie jednak coraz częściej widuje się ją w różnych projektach
programistycznych, gdzie pełni rolę zamiennika dla pętli while.
Listing 2.33.
class Main {
public static void main (String args[]) {
int i = 0;
for(; ;){
System.out.println("Pętle w Javie " + i);
if(i++ >= 9){
break;
}
}
}
}
Należy pamiętać, że instrukcja break przerywa działanie pętli, w której się znajduje.
Jeśli zatem mamy zagnieżdżone pętle for, a instrukcja break występuje w pętli wewnętrz-
nej, zostanie przerwana jedynie pętla wewnętrzna. Pętla zewnętrzna nadal będzie
działać. Spójrzmy na kod znajdujący się na listingu 2.34: to właśnie dwie zagnieżdżone
pętle for.
b4ac40784ca6e05cededff5eda5a930f
b
68 Java. Praktyczny kurs
Listing 2.34.
class Main {
public static void main (String args[]) {
for(int i = 0; i < 3; i++){
for(int j = 0; j < 3; j++){
System.out.println(i + " " + j);
}
}
}
}
Wynikiem działania takiego programu będzie ciąg liczb widoczny na rysunku 2.22.
Pierwszy pionowy ciąg liczb określa stan zmiennej i, natomiast drugi — stan zmien-
nej j. Ta konstrukcja działa w ten sposób, że w każdym przebiegu pętli zewnętrznej
są wykonywane trzy przebiegi pętli wewnętrznej (a zatem instrukcja wyświetlająca
stan zmiennych wykonywana jest w sumie dziewięć razy). Stąd też ciągi liczb, które
pojawiają się na ekranie.
Rysunek 2.22.
Wynik działania dwóch
zagnieżdżonych pętli
typu for
Zgodnie z tym, co zostało napisane powyżej, za każdym razem, kiedy i osiągnie wartość 2,
przerywana będzie pętla wewnętrzna, a sterowanie będzie przekazywane do pętli ze-
wnętrznej (zostało to zobrazowane na rysunku 2.23). Tym samym po uruchomieniu
programu znikną ciągi liczb wyświetlane, kiedy i było równe 2 (rysunek 2.24). Instrukcja
break powoduje bowiem przejście do kolejnej iteracji zewnętrznej pętli.
Zastosowanie instrukcji break nie ogranicza się oczywiście jedynie do pętli typu for.
Może być ona również stosowana w połączeniu z pozostałymi rodzajami pętli, po-
dobnie jak instrukcja continue, która zostanie przedstawiona już za chwilę.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 69
Rysunek 2.23.
Instrukcja break
powoduje przerwanie
wykonywania pętli
wewnętrznej
Rysunek 2.24.
Zastosowanie
instrukcji warunkowej
w połączeniu z break
Instrukcja continue
O ile instrukcja break powodowała przerwanie wykonywania pętli oraz jej opuszczenie,
o tyle instrukcja continue powoduje przejście do kolejnej iteracji. A zatem jeśli we-
wnątrz pętli znajdzie się instrukcja continue, bieżąca iteracja (przebieg) zostanie
przerwana oraz rozpocznie się kolejna (chyba że bieżąca iteracja była ostatnią). Zo-
baczmy, jak to działa, na konkretnym przykładzie. Na listingu 2.35 jest widoczna pę-
tla for, która wyświetla liczby całkowite z zakresu 1 – 20 podzielne przez 2 (por.
ćwiczenia do samodzielnego wykonania z lekcji 9.).
Listing 2.35.
class Main {
public static void main (String args[]) {
for(int i = 1; i <= 20; i++){
if(i % 2 != 0) continue;
System.out.println(i);
}
}
}
Wynik działania tego programu jest widoczny na rysunku 2.25. Sama pętla jest skon-
struowana w dobrze nam znany sposób. W środku znajduje się instrukcja warunkowa
sprawdzająca warunek i % 2 != 0, czyli badająca, czy reszta z dzielenia i przez 2 jest
różna od 0. Jeśli warunek jest prawdziwy (reszta jest różna od 0), oznacza to, że wartość
zawarta w i nie jest podzielna przez 2, wykonywana jest zatem instrukcja continue.
Jak wiadomo, zgodnie z tym, co zostało napisane powyżej, powoduje ona rozpoczęcie
kolejnej iteracji pętli, czyli zwiększenie wartości zmiennej i o 1 i przejście na początek
pętli (do pierwszej instrukcji). Tym samym jeśli wartość i jest niepodzielna przez 2,
nie zostanie wykonana znajdująca się za warunkiem instrukcja System.out.println(i),
zatem dana wartość nie pojawi się na ekranie, a to właśnie było naszym celem.
b4ac40784ca6e05cededff5eda5a930f
b
70 Java. Praktyczny kurs
Rysunek 2.25.
Wynik działania
programu
wyświetlającego liczby
z zakresu 1 – 20
podzielne przez 2
Rzecz jasna to zadanie można wykonać bez użycia instrukcji continue (podpunkt „Ćwi-
czenia do samodzielnego wykonania”), ale bardzo dobrze ilustruje ono istotę jej działania.
Schemat działania instrukcji continue dla pętli for pokazano na rysunku 2.26.
Rysunek 2.26.
Schemat działania
instrukcji continue
dla pętli typu for
Instrukcja continue w przypadku pętli zagnieżdżonych działa tak jak instrukcja break,
to znaczy jej działanie dotyczy tylko pętli, w której się znajduje. A zatem jeśli jest umiesz-
czona w pętli wewnętrznej, powoduje przejście do kolejnej iteracji pętli wewnętrznej,
a jeśli znajduje się w pętli zewnętrznej — do kolejnej iteracji pętli zewnętrznej.
Schemat tego działania został przedstawiony na rysunku 2.27.
Rysunek 2.27.
Sposób działania
instrukcji continue
w przypadku
zagnieżdżonych
pętli for
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 71
Etykiety
Instrukcje break i continue oprócz tego, że można je wykorzystać w postaci omówionej
powyżej, występują również w połączeniu z etykietami. Postacie te nazywa się czasami
etykietowanym break (ang. labeled break) oraz etykietowanym continue (ang. labeled
continue). Etykieta to wyznaczone miejsce w kodzie programu. Ustala się ją poprzez
podanie jej nazwy zakończonej znakiem dwukropka, np.:
etykieta1:
jest formalnie poprawny i uda się go skompilować, jednak taka etykieta nie będzie miała
praktycznego znaczenia. Zupełnie inaczej będzie, jeśli umieścimy ją tuż przed instruk-
cją iteracji (pętlą). Widać to na listingu 2.36 — jest to modyfikacja przykładu z za-
gnieżdżonymi pętlami for, który był omawiany podczas objaśniania działania zwykłej
instrukcji break.
Listing 2.36.
class Main {
public static void main (String args[]) {
etykieta1:
for(int i = 0; i < 3; i++){
for(int j = 0; j < 3; j++){
if(i == 1) break etykieta1;
System.out.println(i + " " + j);
}
}
}
}
Do kodu została dodana etykieta o nazwie etykieta1 umieszczona prawidłowo tuż przed
pętlami for. W wewnętrznej pętli for znajduje się instrukcja break etykieta1, która
oznacza: przerwij działanie wszystkich pętli aż do etykiety etykieta1. Innymi słowy,
przerwij działanie pętli i idź do etykiety etykieta1 (dokładniej: do bloku oznaczonego
tą etykietą). Instrukcja ta zostanie wykonana, kiedy zmienna i osiągnie wartość 1, zatem
po uruchomieniu programu na ekranie pojawią się tym razem jedynie trzy rzędy liczb
(rysunek 2.28).
b4ac40784ca6e05cededff5eda5a930f
b
72 Java. Praktyczny kurs
Rysunek 2.28.
Efekt działania
etykietowanej
instrukcji break
Rysunek 2.29.
Schemat działania
etykietowanej
instrukcji break
w przypadku pętli for
Rysunek 2.30.
Schemat działania
etykietowanej
instrukcji continue
w przypadku pętli for
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 73
Dlatego też jeśli zmodyfikujemy program z listingu 2.36 tak, aby zamiast instrukcji
break etykieta1 występowała instrukcja continue etykieta1, jak jest to widoczne na
listingu 2.37, wynik jego działania będzie taki jak na rysunku 2.31. Jest to najlepszy
dowód, że inaczej niż etykietowane break, etykietowane continue powoduje ponowne
wejście do pętli. W przeciwnym wypadku w pierwszej kolumnie nie pojawiłyby się
cyfry 2 (por. rysunek 2.28).
Listing 2.37.
class Main {
public static void main (String args[]) {
etykieta1:
for(int i = 0; i < 3; i++){
for(int j = 0; j < 3; j++){
if(i == 1) continue etykieta1;
System.out.println(i + " " + j);
}
}
}
}
Rysunek 2.31.
Wynik działania
programu z listingu 2.37
Ćwiczenie 10.1.
Napisz program, który wyświetli na ekranie nieparzyste liczby z zakresu 1 – 20. Wyko-
rzystaj pętlę for i instrukcję continue.
Ćwiczenie 10.2.
Napisz program, który wyświetli na ekranie nieparzyste liczby z zakresu 1 – 20. Wyko-
rzystaj pętlę while i instrukcję continue.
Ćwiczenie 10.3.
Napisz program, który wyświetli na ekranie liczby z zakresu 1 – 30 niepodzielne
przez 3. Wykonaj trzy warianty ćwiczenia:
b4ac40784ca6e05cededff5eda5a930f
b
74 Java. Praktyczny kurs
Ćwiczenie 10.4.
Napisz program, który wyświetli na ekranie liczby z zakresu 1 – 100 podzielne przez
4, ale niepodzielne przez 8 i przez 10. Wykorzystaj instrukcję continue.
Ćwiczenie 10.5.
Zmodyfikuj program znajdujący się na listingu 2.35 tak, aby wynik jego działania po-
został bez zmian, ale nie było potrzeby używania instrukcji continue.
Tablice
Tablica to stosunkowo prosta struktura danych pozwalająca na przechowanie uporządko-
wanego zbioru elementów danego typu. Obrazowo zostało to przedstawione na rysun-
ku 2.32. Jak widać, struktura ta składa się z ponumerowanych kolejno komórek (nu-
meracja zaczyna się od 0). Każda taka komórka może przechowywać pewną porcję
danych. To, jakiego rodzaju będą to dane, jest określone przez typ tablicy. Jeśli zostanie
zadeklarowana tablica typu całkowitoliczbowego (int), będzie ona mogła zawierać liczby
całkowite. Jeżeli będzie to natomiast typ znakowy (char), poszczególne komórki będą
mogły zawierać różne znaki.
Rysunek 2.32.
Schematyczna struktura
tablicy
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 75
Jest ona zatem bardzo podobna do deklaracji zwykłej zmiennej typu prostego (takiego
jak int, char, short itp.), wyróżnikiem jest natomiast nawias kwadratowy. Taka de-
klaracja to jednak nie wszystko, powstała dopiero zmienna o nazwie nazwa_tablicy,
dzięki której będzie można odwoływać się do tablicy, ale samej tablicy jeszcze wcale nie
ma! Trzeba ją dopiero utworzyć, korzystając z operatora new w postaci:
new typ_tablicy[liczba_elementów];
lub:
typ_tablicy[] nazwa_tablicy = new typ_tablicy[liczba_elementów];
bądź też rozbić te czynności na dwie instrukcje. Schemat postępowania wygląda wtedy
następująco:
typ_tablicy nazwa_tablicy[];
/*tutaj mogą znaleźć się inne instrukcje*/
nazwa_tablicy = new typ_tablicy[liczba_elementów];
Jak widać, pomiędzy deklaracją a utworzeniem tablicy można umieścić również inne in-
strukcje. W przypadku prostych programów najczęściej wykorzystuje się jednak sposób
pierwszy, to znaczy jednoczesną deklarację zmiennej tablicowej i utworzenie tablicy.
Od razu warto zobaczyć, jak to wygląda w praktyce. Zadeklarujemy tablicę liczb cał-
kowitych (typu int) o nazwie tab i wielkości jednego elementu. Temu elementowi
przypiszemy dowolną wartość, a następnie wyświetlimy ją na ekranie. Kod realizujący
to zadanie jest widoczny na listingu 2.38.
Listing 2.38.
class Main {
public static void main (String args[]) {
int tab[] = new int[1];
tab[0] = 10;
System.out.println("Pierwszy element tablicy ma wartość: " + tab[0]);
}
}
b4ac40784ca6e05cededff5eda5a930f
b
76 Java. Praktyczny kurs
Sprawdźmy teraz, co się stanie, jeśli się pomylimy i spróbujemy odwołać się do nieist-
niejącego elementu tablicy. Przykładowo: jeśli zadeklarujemy tablicę 10-elementową,
ale zapomnimy, że tablice są indeksowane od 0, i spróbujemy odwołać się do elementu
o indeksie 10 (element o takim indeksie oczywiście nie istnieje). Taki scenariusz zo-
stał zrealizowany we fragmencie kodu przedstawionym na listingu 2.39.
Listing 2.39.
class Main {
public static void main (String args[]) {
int tab[] = new int[10];
tab[10] = 5;
System.out.println("Dziesiąty element tablicy ma wartość: " + tab[10]);
}
}
Taki program da się bez problemu skompilować, jednak próba jego uruchomienia przy-
niesie rezultat widoczny na rysunku 2.33. Choć wygląda to bardzo groźnie, tak na-
prawdę nie stało się nic strasznego. Wykonywanie programu zostało rzecz jasna
przerwane, jednak najważniejsze jest to, że próba nieprawidłowego odwołania się do
tablicy została wykryta przez środowisko uruchomieniowe (maszynę wirtualną Javy)
i samo odwołanie, które mogłoby naruszyć stabilność systemu, nie nastąpiło. Zamiast
tego został wygenerowany tak zwany wyjątek (wyjątkami zajmiemy się w rozdziale 4.)
o nazwie ArrayIndexOutOfBoundsException (indeks tablicy poza zakresem) i program
zakończył działanie.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 77
To bardzo ważna cecha Javy. Analogicznie działający program w C czy C++ (tzn.
odwołujący się do nieistniejących elementów tablic) potrafi poczynić spore spustoszenie
w systemie. Jego działanie jest też zupełnie nieprzewidywalne. Mimo wystąpienia
błędu może on dalej działać poprawnie (to bardzo optymistyczne założenie), może dzia-
łać, ale dawać nieprawidłowe wyniki albo może nastąpić zamknięcie programu przez
system operacyjny, a w ekstremalnym przypadku także naruszenie stabilności samego
systemu operacyjnego14. W Javie sprawa jest prosta — jeśli nastąpi przekroczenie zakresu
tablicy, system wygeneruje wyjątek.
Ważną sprawą jest inicjalizacja tablicy, czyli przypisanie jej komórkom wartości po-
czątkowych. W przypadku niewielkich tablic takiego przypisania można dokonać,
ujmując żądane wartości w nawias klamrowy. Nie trzeba wtedy korzystać z operatora
new. Środowisko Javy utworzy tablicę samodzielnie i zapisze w jej kolejnych komór-
kach podane wartości. Schematycznie deklaracja taka wygląda następująco:
typ_tablicy nazwa_tablicy[] = {wartość1, wartość2, ... , wartośćn}
Przykładowo aby zadeklarować 6-elementową tablicę liczb całkowitych typu int i przypi-
sać jej kolejnym komórkom wartości od 1 do 6, należy zastosować konstrukcję:
int tablica[] = {1, 2, 3, 4, 5, 6}
O tym, że taka konstrukcja jest prawidłowa, można przekonać się, uruchamiając kod
widoczny na listingu 2.40, gdzie ta tablica została zadeklarowana. Do wyświetlenia jej
zawartości na ekranie została użyta pętla typu for (por. lekcja 9.) i instrukcja System.out.
println. Wynik działania tego programu jest widoczny na rysunku 2.34.
Listing 2.40.
class Main {
public static void main (String args[]) {
int tablica[] = {1, 2, 3, 4, 5, 6};
for(int i = 0; i < 6; i++){
System.out.println("tab[" + i + "] = " + tablica[i]);
}
}
}
Rysunek 2.34.
Wynik działania pętli for
użytej do wyświetlenia
zawartości tablicy
14
Nie powinno się to jednak zdarzyć w żadnym z nowoczesnych systemów operacyjnych.
b4ac40784ca6e05cededff5eda5a930f
b
78 Java. Praktyczny kurs
Właściwość length
Kiedy rozmiar tablicy jest większy, zamiast stosować przedstawione wyżej przypisa-
nie w nawiasie klamrowym, lepiej do wypełnienia jej danymi wykorzystać zwyczajną
pętlę. A zatem jeśli mamy np. 20-elementową tablicę liczb typu int i chcemy zapisać
w każdej z jej komórek liczbę 15, możemy wykorzystać w tym celu konstrukcję:
for(int i = 0; i < 20, i++){
tab[i] = 15;
}
Gdy jednak pisze się w ten sposób, łatwo o pomyłkę. Może się np. zdarzyć, że progra-
mista zmieni rozmiar tablicy, ale zapomni zmodyfikować pętlę. Nierzadko zdarza się też
sytuacja, kiedy rozmiar tablicy nie jest z góry znany i zostaje ustalony dopiero w trakcie
działania programu. Na szczęście ten problem został rozwiązany w bardzo prosty
sposób. Dzięki temu, że tablice w Javie są obiektami, każda z nich ma przechowującą
jej rozmiar właściwość length, która może być tylko odczytywana. A zatem przy
użyciu konstrukcji w postaci:
nazwa_tablicy.length
Listing 2.41.
class Main {
public static void main (String args[]) {
short tablica[] = new short[10];
for(short i = 0; i < tablica.length; i++){
tablica[i] = i;
}
for(short i = 0; i < tablica.length; i++){
System.out.println("tablica[" + i + "] = " + tablica[i]);
}
}
}
Ćwiczenie 11.1.
Napisz program, w którym zostanie utworzona 10-elementowa tablica liczb typu int.
Za pomocą pętli for zapisz w kolejnych komórkach liczby od 101 do 110. Zawartość
tablicy wyświetl na ekranie.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 79
Ćwiczenie 11.2.
Napisz program, w którym zostanie utworzona 10-elementowa tablica liczb typu int.
Użyj pętli for do wypełnienia jej danymi w taki sposób, aby w kolejnych komórkach
znalazły się liczby od 10 do 100 (czyli 10, 20, 30 itd.). Zawartość tablicy wyświetl na
ekranie.
Ćwiczenie 11.3.
Napisz program, w którym zostanie utworzona 20-elementowa tablica typu boolean.
Komórkom o indeksie parzystym przypisz wartość true, a o indeksie nieparzystym —
false (zero możesz uznać za wartość parzystą). Zawartość tablicy wyświetl na ekranie.
Ćwiczenie 11.4.
Spróbuj rozwiązać ćwiczenie 11.3 w taki sposób, aby zapisywanie danych w tablicy
odbywało się za pomocą jednej instrukcji przypisania (korzystającej z operatora =).
Ćwiczenie 11.5.
Napisz program, w którym zostanie utworzona 100-elementowa tablica liczb typu int.
Komórkom o indeksach 0, 10, 20, … , 90 przypisz wartość 0, komórkom 1, 11, 21, … , 91
wartość 1, komórkom 2, 12, 22, … , 92 wartość 2 itd.
Ćwiczenie 11.6.
Utwórz 26-elementową tablicę typu char. Zapisz w kolejnych komórkach małe litery
alfabetu od a do z.
Ćwiczenie 11.7.
Zmodyfikuj program z listingu 2.41 w taki sposób, aby do wypełnienia tablicy danymi
została wykorzystana pętla typu while.
b4ac40784ca6e05cededff5eda5a930f
b
80 Java. Praktyczny kurs
Tablice dwuwymiarowe
W lekcji 11. zostały przedstawione podstawowe tablice jednowymiarowe, to znaczy takie,
które są wektorami elementów, czyli strukturami zaprezentowanymi na rysunku 2.32. Ta-
blice nie muszą być jednak jednowymiarowe, wymiarów może być więcej, np. dwa. Wte-
dy taka struktura wygląda tak jak na rysunku 2.35. Widać wyraźnie, że do wyznaczenia
konkretnej komórki trzeba tym razem podać dwie liczby określające rząd oraz kolumnę.
Rysunek 2.35.
Przykład tablicy
dwuwymiarowej
Taką tablicę trzeba zadeklarować oraz utworzyć. Odbywa się to tak samo jak w przy-
padku tablic jednowymiarowych. Jeśli schematyczna deklaracja tablicy jednowymia-
rowej wyglądała:
typ nazwa_tablicy[]
Ten zapis można rozbić na kilka wierszy w celu zwiększenia czytelności, np.:
typ_tablicy nazwa_tablicy[][] = {
{wartość1, wartość2, ... , wartośćN},
{wartość1, wartość2, ... , wartośćN}
}
Aby zatem utworzyć tablicę taką jak na rysunku 2.35, wypełnić ją kolejnymi liczbami
całkowitymi i wyświetlić jej zawartość na ekranie, należy wykorzystać program wi-
doczny na listingu 2.42. Tablica ta będzie wtedy wyglądać tak jak na rysunku 2.36.
15
Możliwa jest oczywiście również deklaracja w postaci typ[][] nazwa_tablicy.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 81
Rysunek 2.36.
Schematyczna postać
tablicy dwuwymiarowej
po wypełnieniu danymi
Listing 2.42.
class Main {
public static void main (String args[]) {
int tab[][] = {
{1, 2, 3, 4},
{5, 6, 7, 8}
};
for(int i = 0; i < 2; i++){
for(int j = 0; j < 4; j++){
System.out.print("tab[" + i + "," + j + "] = ");
System.out.println(tab[i][j]);
}
}
}
}
Do wyświetlenia zawartości tablicy na ekranie zostały użyte dwie pętle typu for. Pętla
zewnętrzna ze zmienną iteracyjną i przebiega kolejne wiersze tablicy, a wewnętrzna
ze zmienną iteracyjną j — kolejne komórki w danym wierszu. Aby odczytać zawartość
konkretnej komórki, stosuje się odwołanie:
tab[i][j]
Rysunek 2.37.
Wynik działania
programu
wypełniającego tablicę
dwuwymiarową
b4ac40784ca6e05cededff5eda5a930f
b
82 Java. Praktyczny kurs
new typ_tablicy[liczba_elementów][liczba_elementów];
lub też rozbić te czynności na dwie instrukcje. Schemat postępowania wygląda wtedy
następująco:
typ_tablicy nazwa_tablicy[][];
/*tutaj mogą się znaleźć inne instrukcje*/
nazwa_tablicy = new typ_tablicy[liczba_elementów][liczba_elementów];
Jeśli zatem ma powstać tablica liczb typu int, o dwóch wierszach i czterech komórkach
w każdym z nich (czyli dokładnie taka jak w przykładzie z listingu 2.42), należy zasto-
sować instrukcję:
int tab[][] = new int[2][4];
Taką tablicę można wypełnić danymi przy użyciu zagnieżdżonych pętli for, tak jak
zostało to zaprezentowane na listingu 2.43.
Listing 2.43.
class Main {
public static void main (String args[]) {
int tab[][] = new int[2][4];
int licznik = 1;
for(int i = 0; i < 2; i++){
for(int j = 0; j < 4; j++){
tab[i][j] = licznik++;
}
}
for(int i = 0; i < 2; i++){
for(int j = 0; j < 4; j++){
System.out.print("tab[" + i + "," + j + "] = ");
System.out.println(tab[i][j]);
}
}
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 83
Właściwość length
Zaprezentowany na listingu 2.43 sposób wypełniania tablicy (oraz wyświetlania jej za-
wartości na ekranie) ma jeden mankament. W przypadku obu pętli zakładamy, że roz-
miar tablicy jest znany. Jak sobie poradzić w sytuacji, kiedy rozmiar pozostaje nieznany?
W przypadku tablic jednowymiarowych mamy do dyspozycji właściwość length. Czy
istnieje ona również w tablicach wielowymiarowych? Jeśli tak, to jakie ma znaczenie?
oznacza tablicę tablic typu int, innymi słowy: tablicę składającą się z innych tablic
typu int. Zatem pełna deklaracja:
int[][] tablica = new int[n][m]
da wielkość tablicy znajdującej się pod indeksem indeks (w każdym przypadku będzie
to rzecz jasna m). Prześledźmy to na przykładzie konkretnej deklaracji:
int[][] tablica = new int[2][4]
b4ac40784ca6e05cededff5eda5a930f
b
84 Java. Praktyczny kurs
Listing 2.44.
class Main {
public static void main (String args[]) {
int tab[][] = new int[2][4];
int licznik = 1;
for(int i = 0; i < tab.length; i++){
for(int j = 0; j < tab[i].length; j++){
tab[i][j] = licznik++;
}
}
for(int i = 0; i < tab.length; i++){
for(int j = 0; j < tab[i].length; j++){
System.out.print("tab[" + i + "," + j + "] = ");
System.out.println(tab[i][j]);
}
}
}
}
Tablice nieregularne
Tablice wielowymiarowe wcale nie muszą mieć regularnie prostokątnych kształtów,
tak jak prezentowane wcześniej w tym punkcie. Prostokątnych, to znaczy takich, w któ-
rych w każdym wierszu znajduje się taka sama liczba komórek (tak jak zaprezentowano to
na rysunkach 2.35 i 2.36). Nic nie stoi na przeszkodzie, aby stworzyć strukturę trój-
kątną (rysunek 2.39a) lub też całkiem nieregularną (rysunek 2.39b). Przy tworzeniu
takich struktur czeka nas jednak więcej pracy niż w przypadku tablic regularnych,
gdyż prawdopodobnie każdy wiersz trzeba będzie tworzyć oddzielnie16.
Jak tworzyć tego typu struktury? Wiadomo już, że tablice wielowymiarowe to tak na-
prawdę tablice tablic jednowymiarowych (tablica dwuwymiarowa to tablica jednowymia-
rowa zawierająca szereg tablic jednowymiarowych, tablica trójwymiarowa to tablica
16
Zależy to od kształtu tablicy, który chce się uzyskać.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 85
Rysunek 2.39.
Przykłady
nieregularnych tablic
wielowymiarowych
Ta deklaracja tworzy zmienną tablicową o nazwie tab, której można przypisywać tablice
dwuwymiarowe przechowujące liczby typu int. Skoro struktura ma wyglądać jak na ry-
sunku 2.39b, należy teraz utworzyć 4-elementową tablicę, która będzie mogła przecho-
wywać tablice jednowymiarowe liczb typu int. Trzeba więc użyć operatora new w postaci:
new int[4][]
Teraz kolejnym elementom: tab[0], tab[1], tab[2] i tab[3], trzeba przypisać nowo
utworzone tablice jednowymiarowe liczb typu int, tak aby w komórce tab[0] zna-
lazła się tablica 4-elementowa, w tab[1] — tablica 2-elementowa, w tab[2] — tablica
1-elementowa, w tab[3] — tablica 3-elementowa (rysunek 2.40). Należy zatem wykonać
ciąg instrukcji:
tab[0] = new int[4];
tab[1] = new int[2];
tab[2] = new int[1];
tab[3] = new int[3];
b4ac40784ca6e05cededff5eda5a930f
b
86 Java. Praktyczny kurs
Rysunek 2.40.
Tworzenie nieregularnej
tablicy dwuwymiarowej
Rysunek 2.41.
Nieregularna tablica
wypełniona danymi
Listing 2.45.
class Main {
public static void main (String args[]) {
int[][] tab = new int[4][];
tab[0] = new int[4];
tab[1] = new int[2];
tab[2] = new int[1];
tab[3] = new int[3];
int licznik = 1;
for(int i = 0; i < tab.length; i++){
for(int j = 0; j < tab[i].length; j++){
tab[i][j] = licznik++;
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 87
Do wyświetlenia danych również są używane dwie zagnieżdżone pętle for, ale inaczej niż
w przykładzie z listingu 2.44, dane dotyczące jednego wiersza tablicy zostają wyświetlone
w jednej linii ekranu, co tworzy obraz widoczny na rysunku 2.42. Jest to możliwe
dzięki użyciu instrukcji System.out.print zamiast System.out.println. W pętli ze-
wnętrznej jest umieszczona instrukcja System.out.print("tab[" + i + "] = ") wy-
świetlająca numer aktualnie przetwarzanego wiersza tabeli, natomiast w pętli wewnętrznej
instrukcja System.out.print(tab[i][j] + " ") wyświetlająca zawartość komórek w da-
nym wierszu.
Rysunek 2.42.
Wyświetlenie danych
z tablicy nieregularnej
Spróbujmy teraz utworzyć tablicę, której struktura została przedstawiona na rysunku 2.39A.
Po wyjaśnieniach z ostatniego przykładu nie powinno to przysporzyć najmniejszych pro-
blemów. Wypełnijmy ją danymi tak samo jak w poprzednim przypadku — umieszczając
w komórkach kolejne wartości od 1 do 10. Deklaracja i inicjalizacja będą wyglądać
następująco:
int[][] tab = new int[4][];
b4ac40784ca6e05cededff5eda5a930f
b
88 Java. Praktyczny kurs
Listing 2.46.
class Main {
public static void main (String args[]) {
int[][] tab = new int[4][];
tab[0] = new int[4];
tab[1] = new int[3];
tab[2] = new int[2];
tab[3] = new int[1];
int licznik = 1;
for(int i = 0; i < tab.length; i++){
for(int j = 0; j < tab[i].length; j++){
tab[i][j] = licznik++;
}
}
for(int i = 0; i < tab.length; i++){
System.out.print("tab[" + i + "] = ");
for(int j = 0; j < tab[i].length; j++){
System.out.print(tab[i][j] + " ");
}
System.out.println("");
}
}
}
Warto w tym miejscu zwrócić uwagę na jedną rzecz: tablica taka jak na rysunku 2.39a,
mimo że nazwana nieregularną, powinna być raczej nazywana nieprostokątną, gdyż
w rzeczywistości jej trójkątny kształt można traktować jako regularny. Jeśli tak, nie
trzeba jej tworzyć ręcznie za pomocą serii (w naszym wypadku czterech) instrukcji.
Zamiast pisać:
tab[0] = new int[4];
tab[1] = new int[3];
tab[2] = new int[2];
tab[3] = new int[1];
można również wykorzystać odpowiednio skonstruowaną pętlę typu for. Skoro każdy
kolejny wiersz ma o jedną komórkę mniej, powinna być to pętla zliczająca od 4 do 1,
czyli powinna wyglądać następująco:
for(int i = 0; i < 4; i++){
tab[i] = new int[4 - i];
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2. Instrukcje języka 89
Ćwiczenie 12.1.
Zmodyfikuj kod z listingu 2.45 tak, aby w kolejnych komórkach tablicy znalazły się
liczby malejące od 10 do 1.
Ćwiczenie 12.2.
Zmodyfikuj program z listingu 2.46 tak, aby do wypełnienia tablicy danymi były wyko-
rzystywane pętle typu while.
Ćwiczenie 12.3.
Utwórz tablicę liczb typu int zaprezentowaną na rysunku 2.43. Wypełnij kolejne ko-
mórki wartościami malejącymi od 10 do 1. Do utworzenia tablicy i wypełnienia jej
danymi wykorzystaj pętle typu for.
Rysunek 2.43.
Odwrócona tablica
trójkątna (ilustracja
do ćwiczenia 12.3)
Ćwiczenie 12.4.
Utwórz trójwymiarową tablicę dla wartości typu int (będzie to struktura, którą można
sobie wyobrazić jako prostopadłościan składający się z sześcianów; każdy sześcian będzie
pojedynczą komórką). Powinna umożliwiać przechowywanie trzydziestu wartości.
Poszczególne komórki wypełnij liczbami od 30 do 59. Zawartość wyświetl na ekranie.
Zastanów się, ile wersji tego typu tablicy można utworzyć.
Ćwiczenie 12.5.
Utwórz tablicę dwuwymiarową, w której liczba komórek w kolejnych rzędach będzie
równa dziesięciu kolejnym wartościom ciągu Fibonacciego, poczynając od elementu
o wartości 1 (1, 1, 2, 3, 5 itd.). Wartość każdej komórki powinna być jej numerem w da-
nym wierszu w kolejności malejącej (czyli dla wiersza o długości trzech komórek kolejne
wartości to 3, 2, 1). Zwartość tablicy wyświetl na ekranie.
b4ac40784ca6e05cededff5eda5a930f
b
90 Java. Praktyczny kurs
Ćwiczenie 12.6.
Zmodyfikuj kod z ćwiczenia 12.5 w taki sposób, aby w kolejnych rzędach znajdowała się
liczba komórek wynikająca z kolejnych wartości ciągu Fibonacciego, ale tylko tych
nieparzystych (czyli 1, 1, 3, 5, 13, 21 itd.).
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.
Programowanie
obiektowe. Część I
Każdy program w Javie składa się z jednej lub wielu klas. W przykładach przedstawio-
nych dotychczas była to tylko jedna klasa o nazwie Main. Przypomnijmy sobie nasz
pierwszy program wyświetlający na ekranie napis. Jego kod wyglądał następująco:
class Main {
public static void main(String args[]){
System.out.println("To jest napis");
}
}
Podstawy
Pierwsza część rozdziału 3. składa się z trzech lekcji, w których są omówione podstawy
programowania obiektowego w Javie. Lekcja 13. opisuje budowę klas oraz tworzenie
obiektów, ponadto zostały w niej przedstawione pola i metody, sposoby ich deklaracji
oraz wywoływania. Lekcja 14. jest poświęcona argumentom metod oraz technice
przeciążania metod, została w niej również przybliżona wykorzystywana już wcześniej
metoda main. W ostatniej lekcji, 15., został zaprezentowany temat konstruktorów,
czyli specjalnych metod wywoływanych podczas tworzenia obiektów.
b4ac40784ca6e05cededff5eda5a930f
b
92 Java. Praktyczny kurs
Zajmiemy się tworzeniem klas, ich strukturą i deklaracjami, omówiony zostanie też
związek między klasą i obiektem. W tej lekcji zostaną również przedstawione składowe
klasy, czyli pola i metody, nie pominiemy także kwestii domyślnych wartości pól.
Okaże się też, jakie relacje występują między zadeklarowaną na stosie zmienną obiek-
tową (inaczej referencyjną, odnośnikową) a utworzonym na stercie obiektem.
Pierwsza klasa
Jak wiadomo z rozdziału 1., klasy są opisami obiektów, czyli bytów programistycznych,
które mogą przechowywać dane oraz wykonywać polecone przez programistę zadania.
Każdy obiekt jest instancją, czyli wystąpieniem jakiejś klasy. W związku z tym klasa
określa także typ danego obiektu. Przypomnijmy, że typ określa rodzaj wartości, jakie
może przyjmować dany byt programistyczny, na przykład zmienna. Jeśli np. mamy
zmienną typu int, to może ona przyjmować jedynie wartości typu int, czyli liczby
z zakresu od –2 147 483 648 do 2 147 483 647. Z obiektami jest podobnie: zmienna
obiektowa hipotetycznej klasy Punkt może przechowywać obiekty klasy (typu) Punkt1.
Klasa to zatem nic innego jak definicja nowego typu danych. Ponieważ dla osób nieobez-
nanych z programowaniem obiektowym to wszystko może brzmieć zawile, zobaczmy
od razu, jak to wygląda w praktyce.
W treści klasy definiuje się pola i metody. Pola służą do przechowywania danych,
metody do wykonywania różnych operacji (oba te elementy są nazywane składowy-
mi klasy). W przypadku klasy, która ma przechowywać dane dotyczące współrzędnych
x i y, wystarczą więc dwa pola typu int2. Pozostaje jeszcze wybór nazwy dla takiej
klasy. Występują tu takie same ograniczenia jak w przypadku nazewnictwa zmien-
nych (lekcja 4.), czyli nazwa klasy może składać się jedynie z liter (zarówno małych,
jak i dużych), cyfr oraz znaku podkreślenia, ale nie może zaczynać się od cyfry. Nie
zaleca się również stosowania w nazwach klas polskich znaków diakrytycznych, szcze-
gólnie że nazwa klasy musi3 być zgodna z nazwą pliku, w którym dana klasa została
zapisana (lekcja 1.).
Utworzoną klasę nazwiemy zatem, jakżeby inaczej, Punkt i będzie ona miała postać
widoczną na listingu 3.1. Kod ten należy zapisać w pliku o nazwie Punkt.java.
1
Jak zobaczymy w dalszej części książki, takiej zmiennej można również przypisać obiekty klas potomnych
lub nadrzędnych w stosunku do klasy Punkt.
2
Załóżmy, że chodzi np. o punkty ekranowe (piksele) i wystarczy, jeśli będą mogły przyjmować tylko
współrzędne całkowite.
3
Jak zostanie to wyjaśnione w dalszej części książki, to stwierdzenie dotyczy tylko klas publicznych.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 93
Tu ponownie trzeba zwrócić uwagę na to, że każdą klasę zapisuje się w oddzielnym pliku
o nazwie zgodnej z jej nazwą oraz rozszerzeniu java. A zatem klasę Punkt zapisuje się
w pliku Punkt.java, klasę Main w pliku Main.java, a klasę Cos w pliku Cos.java. W dalszej
części książki na jednym listingu będzie znajdowało się nawet kilka klas, ale należy je
również zapisywać w oddzielnych plikach w tym samym katalogu, chyba że zostanie
wyraźnie napisane, by zrobić inaczej. Dokładne wyjaśnienie tej kwestii znajdzie się
w lekcji 17.
Listing 3.1.
class Punkt {
int x;
int y;
}
Jak widać, klasa zawiera tylko dwa pola o nazwach x i y, które za pomocą wartości
całkowitych opisują współrzędne położenia punktu. Po zdefiniowaniu klasy Punkt można
zadeklarować zmienną typu Punkt. Robi się to w dokładnie taki sam sposób, w jaki
deklarowane były zmienne typów prostych (np. short, int, char), pisząc:
typ_zmiennej nazwa_zmiennej;
Rzecz jasna, podobnie jak w przypadku zmiennych typów prostych (lekcja 4.), również
tutaj można oddzielić deklarację zmiennej od jej inicjalizacji, w związku z czym rów-
nie poprawna jest konstrukcja w postaci:
Punkt przykladowyPunkt;
przykladowyPunkt = new Punkt();
4
Osoby programujące w C++ powinny zwrócić na to uwagę, gdyż w tym języku już sama deklaracja
zmiennej typu klasowego powoduje wywołanie domyślnego konstruktora i utworzenie obiektu.
b4ac40784ca6e05cededff5eda5a930f
b
94 Java. Praktyczny kurs
Rysunek 3.1.
Zależność
między zmienną
odnośnikową
a wskazywanym
przez nią obiektem
Aby odwołać się do danego pola klasy, należy skorzystać z operatora . (kropka), czyli
użyć konstrukcji:
nazwa_zmiennej_obiektowej.nazwa_pola_obiektu
Przypisanie wartości 100 polu x obiektu klasy Punkt reprezentowanego przez zmienną
przykladowyPunkt będzie zatem wyglądało następująco:
przykladowyPunkt.x = 100;
Listing 3.2.
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
punkt.x = 100;
punkt.y = 100;
System.out.println("punkt.x = " + punkt.x);
System.out.println("punkt.y = " + punkt.y);
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 95
Od tej chwili zmienna punkt wskazuje na obiekt klasy Punkt, można się zatem posłu-
giwać nią tak jak samym obiektem. Pisząc punkt.x = 100, przypisujemy wartość 100
polu x, a pisząc punkt.y = 100, przypisujemy wartość 100 polu y. W ostatnich dwóch
liniach korzystamy z instrukcji System.out.println, aby wyświetlić wartość obu pól
na ekranie. Wystarczy teraz skompilować i uruchomić program. Efekt jest widoczny
na rysunku 3.2.
Rysunek 3.2.
Wynik działania klasy
Main z listingu 3.2
Nie trzeba oddzielnie kompilować klas Main i Punkt. Ponieważ w klasie Main jest
wykorzystywana klasa Punkt, wydanie polecenia javac Main.java spowoduje kompi-
lację zarówno pliku Main.java, jak i Punkt.java. Obie klasy muszą natomiast znaj-
dować się w tym samym katalogu.
Metody klas
Klasy oprócz pól przechowujących dane zawierają także metody, które wykonują za-
pisane przez programistę operacje. Definiuje się je w ciele (czyli wewnątrz) klasy
pomiędzy znakami nawiasu klamrowego. Każda metoda może przyjmować argumenty
(nazywane także parametrami; oba terminy często stosowane są zamiennie) oraz
zwracać wynik. Schematyczna deklaracja metody wygląda następująco:
typ_wyniku nazwa_metody(argumenty_metody) {
//instrukcje metody
}
Jeśli metoda nie zwraca żadnego wyniku, jako typ wyniku należy zastosować słowo
void, jeśli natomiast nie przyjmuje żadnych argumentów, pomiędzy znakami nawiasu
okrągłego nie należy nic wpisywać. Aby zobaczyć, jak to wygląda w praktyce, dodajmy
do klasy Punkt prostą metodę, której zadaniem będzie wyświetlenie wartości współ-
rzędnych x i y na ekranie. Nadamy jej nazwę wyswietlWspolrzedne. Powinna zatem
wyglądać następująco:
void wyswietlWspolrzedne() {
System.out.println("współrzędna x = " + x);
System.out.println("współrzędna y = " + y);
}
b4ac40784ca6e05cededff5eda5a930f
b
96 Java. Praktyczny kurs
Słowo void oznacza, że metoda nie zwraca żadnego wyniku, a brak argumentów po-
między znakami nawiasu okrągłego mówi, że metoda ta nie przyjmuje żadnych ar-
gumentów. Wewnątrz metody znajdują się dwie dobrze nam znane instrukcje, które
wyświetlają na ekranie współrzędne punktu. Jeśli umieścimy powyższy kod wewnątrz
klasy Punkt, będzie ona miała postać widoczną na listingu 3.3.
Listing 3.3.
class Punkt {
int x;
int y;
void wyswietlWspolrzedne() {
System.out.println("współrzędna x = " + x);
System.out.println("współrzędna y = " + y);
}
}
Po utworzeniu obiektu danej klasy można wywołać metodę (czyli wykonać zawarte
w niej instrukcje) w sposób identyczny z tym, w jaki odwołujemy się do pól klasy, tzn.
korzystając z operatora . (kropka). Jeśli zatem przykładowa zmienna punkt zawiera refe-
rencję do obiektu klasy Punkt, prawidłowym wywołaniem metody wyswietlWspolrzedne
będzie:
punkt.wyswietlWspolrzedne();
Oczywiście jeśli dana metoda nie ma argumentów, po prostu się je pomija. Wykorzystaj-
my teraz klasę Main do przetestowania nowej konstrukcji. Zmodyfikujemy program
z listingu 3.2 tak, aby korzystał z metody wyswietlWspolrzedne. Odpowiedni kod jest za-
prezentowany na listingu 3.4. Wynik jego działania łatwo przewidzieć (rysunek 3.3).
Listing 3.4.
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
punkt.x = 100;
punkt.y = 100;
punkt.wyswietlWspolrzedne();
}
}
Rysunek 3.3.
Wynik działania metody
wyswietlWspolrzedne
klasy Punkt
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 97
Zobaczmy teraz, w jaki sposób napisać metody, które będą potrafiły zwracać wyniki.
Typ wyniku należy podać przed nazwą metody, zatem jeśli ma ona zwracać liczbę typu
int, deklaracja powinna wyglądać następująco:
int nazwa_metody() {
//instrukcje metody
}
Sam wynik zwraca się natomiast przez zastosowanie instrukcji return. Najlepiej zo-
baczyć to na praktycznym przykładzie. Do klasy Punkt dodamy zatem dwie metody
— jedna będzie podawała wartość współrzędnej x, druga y. Nazwiemy je odpowiednio
pobierzX i pobierzY. Wygląd metody pobierzX będzie następujący:
int pobierzX() {
return x;
}
Przed nazwą metody znajduje się określenie typu zwracanego przez nią wyniku —
skoro jest to int, oznacza to, że metoda ta musi zwrócić jako wynik liczbę całkowitą
z przedziału określonego przez typ int (tabela 1.1 z rozdziału 1.). Wynik jest zwracany
dzięki instrukcji return. Zapis return x oznacza zwrócenie przez metodę wartości zapisa-
nej w polu x.
Jak łatwo się domyślić, metoda pobierzY będzie wyglądała analogicznie, z tym że będzie
w niej zwracana wartość zapisana w polu y. Pełny kod klasy Punkt po dodaniu tych
dwóch metod będzie wyglądał tak, jak to zostało przedstawione na listingu 3.5. Na li-
stingu 3.6 widać z kolei kod przykładowej klasy Main wykorzystującej nowe metody
klasy Punkt.
Listing 3.5.
class Punkt {
int x;
int y;
int pobierzX() {
return x;
}
int pobierzY() {
return y;
}
void wyswietlWspolrzedne() {
System.out.println("współrzędna x = " + x);
System.out.println("współrzędna y = " + y);
}
}
Listing 3.6.
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
punkt.x = 100;
punkt.y = 100;
b4ac40784ca6e05cededff5eda5a930f
b
98 Java. Praktyczny kurs
To, że metoda zwraca jakąś wartość, jest równoznaczne z tym, że ta wartość jest pod-
stawiana w miejscu wywołania metody. A zatem w tym przypadku w miejscu wystą-
pienia wywołania:
punkt.pobierzX()
zostanie podstawiona wartość 100 (pole x ma wartość 100). Tak samo będzie też przy
wywołaniu:
punkt.pobierzY()
Listing 3.7.
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
System.out.println("punkt.x = " + punkt.x);
System.out.println("punkt.y = " + punkt.y);
}
}
Rysunek 3.4.
Wartości domyślne
niezainicjowanych pól
klasy Punkt
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 99
Ćwiczenie 13.1.
Napisz przykładową klasę LiczbaCalkowita, która będzie przechowywała wartość
całkowitą. Klasa ta powinna zawierać metodę wyswietlLiczbe, która będzie wyświetlała
na ekranie przechowywaną wartość, oraz metodę pobierzLiczbę zwracającą przecho-
wywaną wartość.
Ćwiczenie 13.2.
Napisz kod przykładowej klasy Prostokat zawierającej cztery pola przechowujące
współrzędne czterech rogów prostokąta. Wykorzystaj obiekty klasy Punkt.
Ćwiczenie 13.3.
Do utworzonej w ćwiczeniu 13.2 klasy Prostokat dopisz metody zwracające wartości po-
szczególnych współrzędnych oraz metodę wyświetlającą wszystkie współrzędne.
Ćwiczenie 13.4.
Napisz przykładową klasę Main testującą zachowanie klas LiczbaCalkowita (z ćwi-
czenia 13.1) oraz Prostokat (z ćwiczenia 13.3).
Ćwiczenie 13.5.
Napisz klasę Prostokat przechowującą jedynie współrzędne lewego górnego i prawego
dolnego rogu (wystarczają one do jednoznacznego wyznaczenia prostokąta na płasz-
czyźnie). Dodaj metody podające współrzędne każdego rogu.
b4ac40784ca6e05cededff5eda5a930f
b
100 Java. Praktyczny kurs
Ćwiczenie 13.6.
Do klasy Prostokat z ćwiczenia 13.2 lub 13.3 dopisz metodę sprawdzającą, czy wpro-
wadzone współrzędne rzeczywiście definiują prostokąt (cztery punkty na płaszczyźnie
dają dowolny czworokąt, który nie musi być prostokątem).
Argumenty metod
Dzięki lekcji 13. wiadomo, że każda metoda może mieć argumenty. Argumenty me-
tody to inaczej dane, które można jej przekazać. Metoda może mieć dowolną liczbę
argumentów umieszczonych w nawiasie okrągłym za jej nazwą. Poszczególne argu-
menty oddziela się znakiem przecinka. Schematycznie wygląda to następująco:
typ_wyniku nazwa_metody(typ_parametru_1 nazwa_parametru_1, typ_parametru_2
nazwa_parametru_2, ... , typ_parametru_n nazwa_parametru_n) {
/*treść metody*/
}
natomiast druga:
void ustawY(int wspY) {
y = wspY;
}
Metody te nie zwracają żadnych wyników, co sygnalizuje słowo void, przyjmują na-
tomiast jeden argument typu int. W ciele każdej z metod następuje z kolei przypisa-
nie wartości przekazanej w parametrze odpowiedniemu polu: x w przypadku metody
ustawX oraz y w przypadku metody ustawY.
W podobny sposób można napisać metodę, która będzie jednocześnie ustawiała pola x
i y obiektów klasy Punkt. Oczywiście będzie przyjmowała dwa argumenty. Jak wiadomo,
w deklaracji należy oddzielić je przecinkiem, zatem będzie ona wyglądała następująco:
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 101
Taka metoda ustawXY nie zwraca żadnego wyniku, ale przyjmuje dwa argumenty: wspX,
wspY, oba typu int. We wnętrzu tej metody parametr wspX (dokładniej jego wartość)
zostaje przypisany polu x, a wspY polu y. Jeśli teraz dodamy do klasy Punkt wszystkie
trzy opisane powyżej metody, powstanie kod widoczny na listingu 3.8.
Listing 3.8.
class Punkt {
int x;
int y;
int pobierzX() {
return x;
}
int pobierzY() {
return y;
}
void ustawX(int wspX) {
x = wspX;
}
void ustawY(int wspY) {
y = wspY;
}
void ustawXY(int wspX, int wspY) {
x = wspX;
y = wspY;
}
void wyswietlWspolrzedne() {
System.out.println("współrzędna x = " + x);
System.out.println("współrzędna y = " + y);
}
}
Warto teraz dodatkowo napisać klasę Main, która przetestuje nowe metody. Dzięki
temu będzie można sprawdzić, czy wszystkie trzy działają zgodnie z założeniami. Taka
przykładowa klasa jest widoczna na listingu 3.9.
Listing 3.9.
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
Punkt nowyPunkt = new Punkt();
punkt.ustawX(100);
punkt.ustawY(100);
System.out.println("punkt:");
punkt.wyswietlWspolrzedne();
nowyPunkt.ustawXY(200, 200);
b4ac40784ca6e05cededff5eda5a930f
b
102 Java. Praktyczny kurs
System.out.println("nowyPunkt:");
nowyPunkt.wyswietlWspolrzedne();
}
}
Na początku tworzymy dwa obiekty typu (klasy) Punkt, jeden z nich przypisujemy
zmiennej o nazwie punkt, drugi — zmiennej o nazwie nowy_punkt.
Tak naprawdę (jak już wiadomo z wcześniejszego opisu) obu zmiennym zostały przypi-
sane referencje do utworzonych na stercie obiektów, jednak od tego miejsca książki
niekiedy będzie stosowana również taka uproszczona, ale często spotykana termino-
logia, czyli utożsamianie zmiennej ze wskazywanym przez nią obiektem. Zatem je-
śli zostanie napisane na przykład, że zmiennej jest przypisywany obiekt, w rzeczy-
wistości będzie chodziło o to, że jest jej przypisywana referencja do tego obiektu.
Rysunek 3.5.
Wykonanie programu
z listingu 3.9
5
Dokładniej: polom obiektu wskazywanego przez zmienną punkt. Stosowana jest tu jednak już
wspomniana uproszczona, bardziej potoczna i łatwiejsza w komunikacji terminologia.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 103
Parametrem (argumentem) jest w tej chwili obiekt punkt klasy Punkt. W ciele metody
następuje skopiowanie wartości pól z obiektu przekazanego jako argument do obiektu
bieżącego, czyli przypisanie polu x wartości zapisanej w punkt.x, a polu y wartości
zapisanej w punkt.y.
Jak widać, nie przyjmuje ona żadnych argumentów, nie ma przecież takiej potrzeby;
z deklaracji wynika jednak, że zwraca obiekt klasy Punkt. W ciele metody najpierw
tworzymy nowy obiekt klasy Punkt, przypisując go zmiennej referencyjnej o nazwie
punkt, a następnie przypisujemy jego polom wartości pól x i y z obiektu bieżącego.
Ostatecznie za pomocą instrukcji return powodujemy, że obiekt punkt staje się wartością
zwracaną przez metodę. Klasa Punkt po wprowadzeniu takich modyfikacji będzie
miała postać widoczną na listingu 3.10.
Listing 3.10.
class Punkt {
int x;
int y;
int pobierzX() {
return x;
}
int pobierzY() {
return y;
}
void ustawX(int wspX) {
x = wspX;
}
void ustawY(int wspY) {
y = wspY;
}
void ustawXY(Punkt punkt) {
x = punkt.x;
y = punkt.y;
}
Punkt pobierzWspolrzedne() {
Punkt punkt = new Punkt();
punkt.x = x;
punkt.y = y;
return punkt;
}
void wyswietlWspolrzedne() {
System.out.println("współrzędna x = " + x);
System.out.println("współrzędna y = " + y);
}
}
b4ac40784ca6e05cededff5eda5a930f
b
104 Java. Praktyczny kurs
Aby lepiej uzmysłowić sobie sposób działania wymienionych metod, warto napisać
teraz krótki program, który będzie je wykorzystywał. Powstanie oczywiście kolejna
wersja klasy Main. Jest ona widoczna na listingu 3.11.
Listing 3.11.
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
Punkt drugiPunkt;
punkt.ustawX(100);
punkt.ustawY(200);
drugiPunkt = punkt.pobierzWspolrzedne();
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 105
ta jako argument przyjmuje obiekt klasy Punkt, w tym przypadku obiekt drugiPunkt.
Zatem po wykonaniu instrukcji wartości pól x i y obiektu trzeciPunkt będą takie same jak
pól x i y obiektu drugiPunkt (rysunek 3.7c). Nic więc dziwnego, że wynik działania
programu z listingu 3.11 jest taki jak zaprezentowany na rysunku 3.6. Z kolei na ry-
sunku 3.7 przedstawiono schematyczne zależności pomiędzy zmiennymi i obiektami
występującymi w klasie Main.
Rysunek 3.6.
Utworzenie trzech takich
samych obiektów
różnymi metodami
Rysunek 3.7. Kolejne etapy powstawania zmiennych i obiektów w programie z listingu 3.11
b4ac40784ca6e05cededff5eda5a930f
b
106 Java. Praktyczny kurs
Metoda main
Każdy program musi zawierać punkt startowy, czyli miejsce, od którego zacznie się jego
wykonywanie. W Javie takim miejscem jest metoda o nazwie main i następującej deklaracji:
public static void main(String[] args) {
}
Jeśli w danej klasie znajdzie się metoda w takiej postaci, od niej właśnie zacznie się
wykonywanie kodu programu. Teraz powinno być już jasne, czemu w dwóch pierwszych
rozdziałach przykładowe programy miały schematyczną konstrukcję:
class Main {
public static void main(String args[]) {
//tutaj instrukcje do wykonania
}
}
Przy użyciu tej konstrukcji była po prostu tworzona pomocnicza klasa o nazwie Main,
a w niej metoda main, od której zaczynało się wykonywanie kodu aplikacji. Rzecz jasna
klasa ta mogłaby mieć dowolną inną nazwę, np. KlasaUruchomieniowa lub podobną.
Metoda main nie może zwracać żadnej wartości, czyli typem zwracanym musi być void.
Przed słowem void występują jeszcze dwa terminy, public i static, jednak nie będą
w tej chwili omawiane — przyjdzie na to czas w dalszej części książki. Warto natomiast
w tym miejscu zwrócić uwagę na argument metody main, którym jest, jak już wiadomo
z rozdziału 2., tablica typu String. Typ String opisuje obiekty, które są ciągami zna-
ków, czyli napisami. Innymi słowy, args to tablica ciągów znaków.
Metodę main można umieścić w dowolnej klasie, po prostu tam, gdzie jest to najwygod-
niejsze. Co więcej, nawet w ramach jednej aplikacji każda z klas wchodzących w jej
skład może mieć swoją własną metodę main. O tym, która z tych metod zostanie wy-
konana, decyduje to, która klasa będzie klasą uruchomieniową. Klasa uruchomieniowa to
ta klasa, której nazwę podaje się jako parametr dla maszyny wirtualnej. Czyli jeśli
uruchamiamy aplikację, pisząc:
java Main
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 107
Rysunek 3.8.
Program wyświetlający
parametry jego
wywołania
klasą uruchomieniową jest Punkt. W praktyce zazwyczaj stosuje się tylko jedną metodę
main dla całej aplikacji.
Powróćmy teraz do przykładów związanych z klasą Punkt. Skoro możliwe jest umiesz-
czenie punktu startowego w dowolnej klasie, może to być także klasa Punkt. Dotychczas
do przetestowania działania obiektów klasy Punkt pisaliśmy dodatkową klasę Main, jed-
nak okazuje się, że nie jest to wcale potrzebne (zastosowanie dwóch klas jest jednak
bardziej czytelne dla osób początkujących).
Spójrzmy teraz ponownie na listingi 3.1 i 3.2 z lekcji 13. Na pierwszym z nich znajduje
się podstawowa wersja klasy Punkt, która zawiera jedynie pola x i y. Drugi zawiera
klasę Main, która testuje zachowanie obiektu klasy Punkt. Połączmy je zatem w taki spo-
sób, aby metoda main znalazła się w klasie Punkt, bez zmieniania samej treści tej meto-
dy. Rozwiązanie tego zadania jest przedstawione na listingu 3.13.
Listing 3.13.
class Punkt {
int x;
int y;
Co warte uwagi, nie dokonaliśmy tutaj żadnych poważnych zmian. Kod metody main
pozostaje niezmieniony, a jedyną modyfikacją w klasie Punkt jest dodanie tej właśnie
metody. Program będzie się zachowywał dokładnie tak samo jak ten z listingów 3.1 i 3.2
składający się z klas Main i Punkt. Oczywiście tym razem należy skompilować jedynie
klasę Punkt oraz wykorzystać ją jako klasę uruchomieniową, stosując wywołanie maszyny
wirtualnej w postaci:
java Punkt
b4ac40784ca6e05cededff5eda5a930f
b
108 Java. Praktyczny kurs
Rysunek 3.9.
Klasa Punkt jako klasa
uruchomieniowa
Przeciążanie metod
W trakcie pracy nad kodem klasy Punkt powstały dwie metody o takiej samej nazwie,
ale różnym kodzie. Chodzi oczywiście o metody ustawXY. Pierwsza wersja przyjmowała
jako argumenty dwie liczby typu int, a druga miała tylko jeden argument, którym był
obiekt klasy Punkt. Okazuje się, że takie dwie metody mogą współistnieć w klasie
Punkt i z obu z nich można korzystać w kodzie programu.
Ogólnie rzecz ujmując, w każdej klasie może istnieć dowolna liczba metod, które
mają takie same nazwy, o ile tylko różnią się argumentami. Mogą one — ale nie muszą —
różnić się również typem zwracanego wyniku. Taką technikę nazywa się przeciąża-
niem metod (ang. overloading). Skonstruujmy zatem taką klasę Punkt, w której znajdą
się obie wersje metody ustawXY. Kod tej klasy jest przedstawiony na listingu 3.14.
Listing 3.14.
class Punkt {
int x;
int y;
Klasa ta zawiera w tej chwili dwie przeciążone metody o nazwie ustawXY. Jest to możliwe,
ponieważ przyjmują one różne argumenty: pierwsza metoda — dwie liczby typu int,
druga — jeden obiekt klasy Punkt. Obie metody realizują takie samo zadanie, tzn. usta-
wiają nowe wartości w polach x i y. Ich działanie można przetestować poprzez dopisanie
w klasie Punkt metody main w przykładowej postaci przedstawionej na listingu 3.15.
Listing 3.15.
public static void main (String args[]) {
Punkt punkt = new Punkt();
Punkt drugiPunkt = new Punkt();
punkt.ustawXY(100, 100);
drugiPunkt.ustawXY(200,200);
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 109
punkt.ustawXY(drugiPunkt);
Działanie tej metody jest proste i nie wymaga wielu wyjaśnień. Na początku tworzy-
my dwa obiekty klasy Punkt i przypisujemy je zmiennym punkt oraz drugiPunkt. Na-
stępnie korzystamy z pierwszej wersji przeciążonej metody ustawXY, aby przypisać
polom x i y pierwszego obiektu wartość 100, a polom x i y drugiego obiektu — 200.
Dalej wyświetlamy zawartość obiektu punkt na ekranie. Potem wykorzystujemy dru-
gą wersję metody ustawXY, aby zmienić zawartość pól obiektu punkt, tak by zawierały
wartości zapisane w obiekcie drugiPunkt. Następnie ponownie wyświetlamy wartości
pól obiektu punkt na ekranie.
Ćwiczenie 14.1.
Napisz program, który wyświetli ponumerowaną listę parametrów przekazanych mu
z wiersza poleceń — od argumentu ostatniego aż do pierwszego.
Ćwiczenie 14.2.
Napisz program, który połączy wszystkie przekazane mu argumenty w jeden ciąg znaków
i wyświetli go na ekranie.
Ćwiczenie 14.3.
Do klasy Punkt z listingu 3.8 dopisz metody ustawX i ustawY, które jako argument będą
przyjmowały obiekt klasy Punkt.
Ćwiczenie 14.4.
W klasie Punkt z listingu 3.8 zmień kod metod ustawX i ustawY, tak aby zwracały one po-
przednią wartość zapisywanych pól. Zadaniem metody ustawX jest więc zmiana wartości
pola x i zwrócenie jego poprzedniej wartości. Metoda ustawY ma wykonywać analogiczne
czynności w stosunku do pola y.
Ćwiczenie 14.5.
Do klasy Punkt dopisz metodę ustawXY przyjmującą jako argument obiekt klasy Punkt.
Polom x i y należy przypisać wartości pól x i y przekazanego obiektu. Metoda ma nato-
miast zwrócić obiekt klasy Punkt zawierający stare wartości x i y.
b4ac40784ca6e05cededff5eda5a930f
b
110 Java. Praktyczny kurs
Inicjalizacja obiektu
Jak wiadomo z lekcji 13., po utworzeniu obiektu w pamięci wszystkie jego pola zawierają
wartości domyślne. Wartości te dla poszczególnych typów danych zostały przedsta-
wione w tabeli 3.1. Najczęściej jednak oczekuje się, by pola te zawierały jakieś konkretne
wartości. Przykładowo można byłoby życzyć sobie, aby każdy obiekt powstałej w lekcji
13. (listing 3.1) klasy Punkt otrzymywał współrzędne x = 1 i y = 1. Oczywiście można
po każdym utworzeniu obiektu przypisywać wartości tym polom, np.:
Punkt punkt = new Punkt();
punkt.x = 1;
punkt.y = 1;
Można też dopisać do klasy Punkt dodatkową metodę, na przykład o nazwie init,
initialize, inicjuj lub podobnej, w postaci:
void init() {
x = 1;
y = 1;
}
Metoda będąca konstruktorem nigdy nie zwraca żadnego wyniku i musi mieć nazwę
zgodną z nazwą klasy — schematycznie wygląda to następująco:
class nazwa_klasy {
nazwa_klasy() {
//kod konstruktora
}
}
Przed definicją nie umieszcza się słowa void, tak jak miałoby to miejsce w przypadku
zwykłej metody. To, co będzie robił konstruktor, czyli jakie wykona czynności, zależy
już tylko od programisty.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 111
Dopiszmy zatem do klasy Punkt z listingu 3.1 (czyli jej najprostszej wersji) konstruktor,
który będzie przypisywał polom x i y każdego obiektu wartość 1. Wygląd takiej klasy
został zaprezentowany na listingu 3.16.
Listing 3.16.
class Punkt {
int x;
int y;
Punkt() {
x = 1;
y = 1;
}
}
Jak widać, wszystko jest tu zgodne z podanym wyżej schematem. Konstruktor nie
zwraca żadnej wartości i ma nazwę zgodną z nazwą klasy. Przed nazwą nie występuje
słowo void. W jego wnętrzu następuje proste przypisanie wartości polom obiektu. O tym,
że konstruktor działa, można się przekonać, pisząc dodatkową klasę Main i używając
w niej obiektu nowej klasy Punkt (można by też dopisać metodę main do klasy Punkt).
Taka przykładowa klasa Main jest widoczna na listingu 3.17.
Listing 3.17.
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
System.out.println("punkt.x = " + punkt.x);
System.out.println("punkt.y = " + punkt.y);
}
}
Rysunek 3.10.
Konstruktor klasy Punkt
faktycznie został
wykonany
Argumenty konstruktorów
Konstruktor nie musi być bezargumentowy, może również przyjmować argumenty, któ-
re zostaną wykorzystane, bezpośrednio lub pośrednio, np. do zainicjowania pól obiektu.
Argumenty przekazuje się dokładnie tak samo jak w przypadku zwykłych metod (lek-
cja 14.), budowa takiego konstruktora byłaby więc następująca:
b4ac40784ca6e05cededff5eda5a930f
b
112 Java. Praktyczny kurs
class nazwa_klasy {
nazwa_klasy(typ1 argument1, typ2 argument2,..., typN argumentN) {
}
}
Jeśli konstruktor przyjmuje argumenty, przy tworzeniu obiektu należy je podać, czyli
zamiast stosowanej do tej pory konstrukcji:
nazwa_klasy zmienna = new nazwa_klasy()
Kiedy zostanie umieszczony w klasie Punkt, przyjmie ona postać widoczną na listingu 3.18.
Listing 3.18.
class Punkt {
int x;
int y;
Punkt(int wspX, int wspY) {
x = wspX;
y = wspY;
}
}
Teraz podczas każdej próby utworzenia obiektu klasy Punkt będzie trzeba podawać jego
współrzędne. Przykładowo: jeśli początkowa współrzędna x ma mieć wartość 100, a po-
czątkowa współrzędna y — 200, należy zastosować konstrukcję:
Punkt punkt = new Punkt(100, 200);
Przeciążanie konstruktorów
Konstruktory, tak jak zwykłe metody, mogą być przeciążane, tzn. każda klasa może mieć
kilka konstruktorów, o ile tylko różnią się one przyjmowanymi argumentami. Do tej
pory powstały dwa konstruktory klasy Punkt: pierwszy bezargumentowy i drugi przyj-
mujący dwa argumenty typu int. Dopiszmy zatem jeszcze trzeci, który jako argument
będzie przyjmował obiekt klasy Punkt. Jego postać będzie następująca:
Punkt(Punkt punkt) {
x = punkt.x;
y = punkt.y;
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 113
Zasada działania jest chyba jasna: polu x jest przypisywana wartość pola x obiektu
przekazanego jako argument, natomiast polu y — wartość pola y tego obiektu. Można
teraz zebrać wszystkie trzy napisane dotychczas konstruktory i umieścić je w klasie
Punkt. Będzie ona wtedy miała postać przedstawioną na listingu 3.19.
Listing 3.19.
class Punkt {
int x;
int y;
Punkt() {
x = 1;
y = 1;
}
Punkt(int wspX, int wspY) {
x = wspX;
y = wspY;
}
Punkt(Punkt punkt) {
x = punkt.x;
y = punkt.y;
}
}
Taka budowa klasy Punkt pozwala na niezależne wywoływanie każdego z trzech kon-
struktorów, w zależności od tego, który z nich jest najbardziej odpowiedni w danej
sytuacji. Warto teraz na konkretnym przykładzie przekonać się, że tak jest w istocie.
Napiszmy więc klasę Main, w której powstaną trzy obiekty klasy Punkt, a każdy z nich
będzie tworzony za pomocą innego konstruktora. Taka przykładowa klasa jest wi-
doczna na listingu 3.20.
Listing 3.20.
class Main {
public static void main (String args[]) {
Punkt punkt1 = new Punkt();
System.out.println("punkt1:");
System.out.println("x = " + punkt1.x);
System.out.println("y = " + punkt1.y);
System.out.println("");
System.out.println("punkt2:");
System.out.println("x = " + punkt2.x);
System.out.println("y = " + punkt2.y);
System.out.println("");
System.out.println("punkt3:");
System.out.println("x = " + punkt3.x);
System.out.println("y = " + punkt3.y);
}
}
b4ac40784ca6e05cededff5eda5a930f
b
114 Java. Praktyczny kurs
Rysunek 3.11.
Wykorzystanie trzech
różnych konstruktorów
klasy Punkt
Co należy wpisać w jego treści, aby spełniał swoje zadanie? Gdyby postępować podob-
nie jak w przypadku klasy z listingu 3.18, powstałaby konstrukcja:
Punkt(int x, int y) {
x = x;
y = x;
}
6
Chociaż formalnie taki zapis jest w pełni poprawny.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 115
Otóż jeśli chcemy zaznaczyć, że chodzi o składową klasy (pole, metodę), korzystamy
z odwołania w postaci:
this.nazwa_pola
lub:
this.nazwa_metody
Instrukcję this.x = x rozumiemy jako: przypisz polu x wartość przekazaną jako argu-
ment o nazwie x, a instrukcję this.y = y odpowiednio jako: przypisz polu y wartość
przekazaną jako argument o nazwie y.
Listing 3.21.
class Punkt {
int x;
int y;
b4ac40784ca6e05cededff5eda5a930f
b
116 Java. Praktyczny kurs
Punkt() {
ustawXY(1, 1);
}
Punkt(int x, int y) {
ustawXY(x, y);
}
Punkt(Punkt punkt) {
ustawXY(punkt);
}
void ustawXY(int wspX, int wspY) {
x = wspX;
y = wspY;
}
void ustawXY(Punkt punkt) {
x = punkt.x;
y = punkt.y;
}
}
Jest to w pełni funkcjonalny odpowiednik klasy z listingu 3.19. Można się o tym przeko-
nać, uruchamiając program testowy zamieszczony na listingu 3.20. Wynik jego działania
będzie dokładnie taki sam jak w poprzednim przypadku, czyli taki, jaki przedstawiono
na rysunku 3.11.
Po cofnięciu się do opisu słowa kluczowego this można sobie przypomnieć, że w sytu-
acji, kiedy pola obiektu oraz argumenty metody mają takie same nazwy, występuje
problem z rozróżnieniem, czy chodzi o pola, czy o argumenty. Czy w tym przypadku
również istnieje taka wątpliwość i czy w związku z tym ten konstruktor jest na pewno po-
prawny? Oczywiście jest poprawny. Jeśli bowiem gdziekolwiek we wnętrzu metody
odwołujemy się do nazwy argumentu, to niezależnie od tego, czy istnieje pole o takiej
nazwie, czy nie, kompilator zawsze przyjmuje, że chodzi o argument. Można powiedzieć,
że w ciele metody argument ma większą siłę niż nazwa pola. Tak więc w powyższym
przykładzie, jeśli napiszemy x lub y, to na pewno chodzi o argumenty, a jeśli chcemy się
odwołać do pól, musimy napisać this.x lub this.y. Dzięki temu sytuacja jest jedno-
znaczna i zawsze wiadomo, o jakie odwołanie chodzi.
Przydatne okazuje się znane nam już słowo kluczowe this. Jeżeli za nim poda się listę
argumentów umieszczonych w nawiasie okrągłym, czyli:
this(argument1, argument2, ... , argumentn)
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 117
Listing 3.22.
class Dane {
int liczba1;
double liczba2;
Dane(int liczba) {
liczba1 = liczba;
}
Dane(double liczba) {
liczba2 = liczba;
}
Dane(int liczba1, double liczba2) {
this(liczba1);
this.liczba2 = liczba2;
}
void wyswietlDane() {
System.out.println("liczba1 = " + liczba1);
System.out.println("liczba2 = " + liczba2);
}
}
Jest to hipotetyczna klasa o nazwie Dane, która zawiera jedynie dwa pola: pierwsze
typu int o nazwie liczba1 i drugie typu double o nazwie liczba2. Do dyspozycji są też
trzy konstruktory. Pierwszy z nich przyjmuje jeden argument typu int i w jego ciele
następuje przypisanie wartości tego argumentu polu liczba1. Drugi konstruktor również
przyjmuje tylko jeden argument, ale typu double, służy zatem do zainicjowania pola
liczba2. Najciekawszy z naszego punktu widzenia jest trzeci konstruktor. Przyjmuje on
dwa argumenty — pierwszy typu int, drugi typu double. W jego wnętrzu nie następuje
jednak bezpośrednie przypisanie przekazanych wartości obu polom. Zamiast tego
najpierw wywołujemy pierwszy konstruktor, przekazując mu argument liczba1 (czyli
wartość typu int), a dopiero w drugiej linii wykonujemy klasyczne przypisanie war-
tości argumentu liczba2 polu liczba2. Rzecz jasna, ponieważ argument ma taką samą
nazwę jak pole, wykorzystujemy znaną nam już dobrze konstrukcję ze słowem klu-
czowym this.
W tym miejscu pojawi się zapewne pytanie, czy nie można byłoby tu zastosować nastę-
pującej postaci konstruktora:
Dane(int liczba1, double liczba2) {
this(liczba1);
this(liczba2);
}
Wydaje się ona bardziej logiczna: skoro bowiem istnieje konstruktor przyjmujący je-
den argument typu int oraz drugi przyjmujący jeden argument typu double, w kon-
struktorze dwuargumentowym należałoby wywołać je oba (tak jak na listingu powyżej).
Niestety nie wolno nam tego zrobić. Zasada jest tutaj następująca: konstruktor można
wywołać jawnie tylko w innym konstruktorze i musi on być pierwszą wykonywaną
instrukcją. Oznacza to, że można wywołać tylko jeden konstruktor, a przed nim nie
powinna się znaleźć żadna inna instrukcja.
b4ac40784ca6e05cededff5eda5a930f
b
118 Java. Praktyczny kurs
Do przetestowania klasy Dane z listingu 3.22 można wykorzystać zawartą w niej me-
todę wyswietlDane. Taki przykładowy program jest widoczny na listingu 3.23. Wynik
jego działania można obejrzeć na rysunku 3.12.
Listing 3.23.
class Main {
public static void main (String args[]) {
Dane dane1 = new Dane(100);
Dane dane2 = new Dane(99.9);
Dane dane3 = new Dane(200, 88.8);
System.out.println("dane1:");
dane1.wyswietlDane();
System.out.println("");
System.out.println("dane2:");
dane2.wyswietlDane();
System.out.println("");
System.out.println("dane3:");
dane3.wyswietlDane();
}
}
Rysunek 3.12.
Wynik działania
programu z listingu 3.23
Metoda finalize
Osoby, które programowały w językach obiektowych, takich jak np. C++ czy Object
Pascal, zwykle zastanawiają się, jak w Javie wygląda destruktor i kiedy zwalniać pa-
mięć zarezerwowaną dla obiektów. Skoro bowiem operator new pozwala na utworzenie
obiektu, a tym samym na zarezerwowanie dla niego pamięci operacyjnej, logicznym
założeniem jest, że po jego wykorzystaniu pamięć tę należy zwolnić. Ponieważ jednak ta-
kie podejście, tzn. zrzucenie na barki programistów konieczności zwalniania przydzielonej
obiektom pamięci, powodowało powstawanie wielu błędów, w Javie zastosowano inne
rozwiązanie. Otóż za zwalnianie pamięci odpowiada maszyna wirtualna, a programista
praktycznie nie ma nad tym procesem kontroli7.
7
Wywołując metodę System.gc(), można jednak wymusić zainicjowanie procesu odzyskiwania pamięci.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 119
Zajmuje się tym tak zwany odśmiecacz (ang. garbage collector), który czuwa nad
optymalnym wykorzystaniem pamięci i uruchamia proces jej odzyskiwania w momencie,
kiedy wolna ilość oddana do dyspozycji programu zbytnio się zmniejszy. Jest to wy-
jątkowo wygodne podejście dla programisty, zwalnia go bowiem z obowiązku zarządza-
nia pamięcią. Zwiększa jednak nieco narzuty czasowe związane z wykonaniem programu,
bo sam proces odśmiecania musi zająć czas procesora. Niemniej dzisiejsze maszyny
wirtualne są na tyle dopracowane, że w większości przypadków nie ma najmniejszej
potrzeby zaprzątania sobie głowy tym problemem.
Należy jednak zdawać sobie sprawę, że Java jest w stanie automatycznie zarządzać
wykorzystywaniem pamięci, ale tylko tej, która jest alokowana standardowo, czyli za
pomocą operatora new. W nielicznych przypadkach, np. gdyby stworzony obiekt wykorzy-
stywał mechanizmy alokacji pamięci specyficzne dla danej platformy systemowej czy
też odwoływał się do modułów napisanych w innych językach programowania, o po-
sprzątanie systemu i zwolnienie zarezerwowanej pamięci trzeba byłoby zadbać samemu.
Java udostępnia w tym celu metodę finalize, która jest wykonywana zawsze, kiedy
obiekt jest niszczony, usuwany z pamięci. Wystarczy więc, że klasa będzie zawierała taką
metodę, a przy niszczeniu obiektu zostanie ona wykonana. Wewnątrz tej metody można
wykonać dowolne instrukcje sprzątające. Jej deklaracja wygląda następująco:
public void finalize() {
//tutaj treść metody
}
Koniecznie jednak należy sobie uświadomić jedną rzecz: nie ma żadnej gwarancji, że
ta metoda zostanie wykonana w trakcie działania programu! Przypomnijmy: proces
odzyskiwania pamięci, a więc niszczenia nieużywanych już obiektów, zaczyna się
wtedy, kiedy garbage collector „uzna” to za stosowne. Czyli wtedy, kiedy przyjmie,
że ilość wolnej pamięci dostępnej dla programu zbytnio się zmniejszyła. Może się
więc okazać, że pamięć zostanie zwolniona dopiero po zakończeniu pracy aplikacji.
Dlatego jeśli niezbędne jest wykonanie dodatkowych czynności porządkowych w jakimś
określonym miejscu podczas działania programu, lepiej napisać dodatkową metodę
sprzątającą i jawnie ją wywołać.
Żeby się przekonać, że metoda finalize jest faktycznie wykonywana, możemy napisać
prostą klasę. Niech nosi nazwę Test. Została przedstawiona razem z testową klasą Main
na listingu 3.24 (kod można zapisać w dwóch osobnych plikach Test.java i Main.java
lub też tylko w jednym — Main.java).
Listing 3.24.
class Test {
public void finalize() {
System.out.println("Niszczenie obiektu.");
}
}
class Main {
public static void main (String args[]) {
Test test = null;
for(int i = 0; i < 10; i++){
b4ac40784ca6e05cededff5eda5a930f
b
120 Java. Praktyczny kurs
Klasa Test nie zawiera żadnych pól, a jej jedyną metodą jest finalize. Deklaracja tej
metody musi być właśnie taka, tzn. public void finalize (znaczenie słowa public
zostanie podane w dalszej części książki, w lekcji 17.). W jej ciele znajduje się tylko jedna
instrukcja powodująca wyświetlenie na ekranie napisu Niszczenie obiektu.
W klasie Main z kolei deklarujemy zmienną klasy Test o nazwie test i początkowo przy-
pisujemy jej wartość pustą — null. Następnie w pętli dziesięć razy tworzymy nowy
obiekt klasy Test i przypisujemy referencję do niego zmiennej test. Należy zauważyć, że
każde takie przypisanie powoduje utracenie poprzedniej wartości zmiennej test (czyli
poprzedniej referencji), a tym samym oznaczenie wcześniej przypisanego obiektu jako
nieużywanego. To z kolei oznacza, że po wykonaniu całej pętli zmienna test będzie za-
wierała referencję do ostatnio przypisanego, dziesiątego obiektu, natomiast wszystkie po-
przednie obiekty zostaną utracone i będą mogły zostać poddane procesowi odśmiecania.
Ćwiczenie 15.1.
Napisz klasę, której zadaniem będzie przechowywanie liczb typu int. Dołącz jednoargu-
mentowy konstruktor przyjmujący argument typu int. Polu klasy nadaj nazwę liczba,
tak samo nazwij argument konstruktora.
8
Dokładniej, należy tę instrukcję traktować jako sugestię dla maszyny wirtualnej, aby uruchomiła proces
odzyskiwania pamięci. Nie ma stuprocentowej gwarancji, że zostanie on uruchomiony dokładnie w chwili
wykonania tej instrukcji, niemniej odbędzie się to w najbliższym możliwym momencie, chyba że
aplikacja wcześniej zakończy działanie.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 121
Rysunek 3.13.
Metoda finalize jest
wykonywana podczas
niszczenia obiektu
Ćwiczenie 15.2.
Do klasy powstałej w ćwiczeniu 15.1 dopisz przeciążony konstruktor bezargumentowy
ustawiający jej pole na wartość –1.
Ćwiczenie 15.3.
Napisz klasę zawierającą dwa pola: pierwsze typu double i drugie typu char. Dopisz
cztery przeciążone konstruktory: pierwszy przyjmujący jeden argument typu double,
drugi przyjmujący jeden argument typu char, trzeci przyjmujący dwa argumenty —
pierwszy typu double, drugi typu char — i czwarty przyjmujący również dwa argumenty
— pierwszy typu char, drugi typu double.
Ćwiczenie 15.4.
Zmień kod klasy powstałej w ćwiczeniu 15.3 tak, aby w konstruktorach dwuargumento-
wych były wykorzystywane konstruktory jednoargumentowe.
Ćwiczenie 15.5.
Zmodyfikuj program z listingu 3.24 w taki sposób, aby przed wywołaniem odśmiecacza
w programie nie było odwołania do żadnego obiektu na stercie (liczba obiektów po-
winna pozostać niezmienna). Ilu napisów o niszczeniu obiektu możesz się spodziewać
po uruchomieniu programu? Sprawdź również zachowanie aplikacji po usunięciu
znajdujących się na jej końcu pętli opóźniających.
Dziedziczenie
Dziedziczenie to jeden z fundamentów programowania obiektowego. Umożliwia sprawne
i łatwe wykorzystywanie raz napisanego kodu czy budowanie hierarchii klas przejmują-
cych swoje właściwości. Ten podrozdział zawiera cztery lekcje przybliżające temat dzie-
dziczenia. W lekcji 16. zaprezentowane są podstawy, czyli sposoby tworzenia klas
potomnych oraz zachowania konstruktorów klasy bazowej i potomnej. W lekcji 17.
b4ac40784ca6e05cededff5eda5a930f
b
122 Java. Praktyczny kurs
Dziedziczenie
Na początku lekcji 13. powstała klasa Punkt, która przechowywała informację o współ-
rzędnych punktu na płaszczyźnie. W trakcie dalszych ćwiczeń została rozbudowana
o dodatkowe metody, które pozwalały na ustawianie i pobieranie tych współrzędnych.
Zastanówmy się teraz, co należałoby zrobić, gdyby potrzebne było określenie położenia
punktu nie w dwóch, ale w trzech wymiarach, czyli gdyby do współrzędnych x i y trzeba
było dodać współrzędną z. Pomysłem, który od razu przychodzi do głowy, jest napisanie
dodatkowej klasy, np. o nazwie Punkt3D, w postaci:
class Punkt3D {
int x;
int y;
int z;
}
Do tej klasy należałoby dalej dopisać pełny zestaw metod, które znajdowały się w klasie
Punkt, takich jak pobierzX, pobierzY, ustawX, ustawY itd., oraz dodatkowe metody ope-
rujące na współrzędnej z. Zauważmy jednak, że w takiej sytuacji w dużej części po prostu
powtarza się już raz napisany kod. Czym bowiem będzie się różniła metoda ustawX
klasy Punkt od metody ustawX klasy Punkt3D? Oczywiście niczym. Po prostu Punkt3D
jest pewnego rodzaju rozszerzeniem klasy Punkt. Rozszerza ją o dodatkowe możliwości
(pola, metody), pozostawiając stare właściwości bez zmian. Zamiast więc pisać cał-
kiem od nowa klasę Punkt3D, lepiej spowodować, aby przejęła ona wszystkie możliwości
klasy Punkt, wprowadzając dodatkowo swoje własne pola i metody. Jest to tak zwane
dziedziczenie, czyli jeden z fundamentów programowania obiektowego. Powiemy, że
klasa Punkt3D dziedziczy po klasie Punkt9, czyli przejmuje jej pola i metody oraz dodaje
swoje własne. Wtedy klasę Punkt nazwiemy klasą bazową (nadrzędną, nadklasą, ang.
superclass), a klasę Punkt3D — klasą potomną (pochodną, podrzędną, podklasą, ang.
subclass).
W Javie dziedziczenie jest wyrażane za pomocą słowa extends, a cała definicja sche-
matycznie wygląda następująco:
9
Często spotyka się również potoczną formę „dziedziczyć z”, np. „Klasa Punkt3D dziedziczy z klasy Punkt”.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 123
Taki zapis oznacza, że klasa potomna dziedziczy po klasie bazowej. Zobaczmy, jak
taka deklaracja będzie wyglądała w praktyce dla wspomnianych klas Punkt i Punkt3D.
Jest to bardzo proste:
class Punkt3D extends Punkt {
int z;
}
Taki zapis oznacza, że klasa Punkt3D przejęła wszystkie właściwości klasy Punkt, a do-
datkowo otrzymała pole typu int o nazwie z. Przekonajmy się, że tak jest w istocie. Niech
klasy Punkt i Punkt3D wyglądają tak jak na listingu 3.25 (najlepiej zapisać je w dwóch
plikach: Punkt.java i Punkt3D.java).
Listing 3.25.
class Punkt {
int x;
int y;
int pobierzX() {
return x;
}
int pobierzY() {
return y;
}
void ustawX(int wspX) {
x = wspX;
}
void ustawY(int wspY) {
y = wspY;
}
void ustawXY(int wspX, int wspY) {
x = wspX;
y = wspY;
}
void ustawXY(Punkt punkt) {
x = punkt.x;
y = punkt.y;
}
void wyswietlWspolrzedne() {
System.out.println("współrzędna x = " + x);
System.out.println("współrzędna y = " + y);
}
}
b4ac40784ca6e05cededff5eda5a930f
b
124 Java. Praktyczny kurs
Jeśli napiszemy teraz jeszcze klasę Main widoczną na listingu 3.26, testującą obiekt klasy
Punkt3D, przekonamy się, że na takim obiekcie zadziałają wszystkie metody, które
znajdowały się w klasie Punkt.
Listing 3.26.
class Main {
public static void main (String args[]) {
Punkt3D punkt = new Punkt3D();
punkt.ustawX(100);
punkt.ustawY(200);
punkt.ustawXY(300, 400);
Klasa Punkt3D nie jest jednak w takiej postaci w pełni funkcjonalna, należałoby prze-
cież dopisać metody operujące na nowym polu z. Na pewno przydatne będą ustawZ,
pobierzZ oraz ustawXYZ. Oczywiście metoda ustawZ będzie przyjmowała jeden argument
typu int i przypisywała jego wartość polu z, metoda pobierzZ będzie zwracała wartość
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 125
Rysunek 3.14.
Klasa Punkt3D przejęła
pola i metody klasy
Punkt
pola z, natomiast ustawXYZ będzie przyjmowała trzy argumenty typu int i przypisywała je
polom x, y i z. Łatwo się domyślić, że metody te będą wyglądały tak, jak zaprezentowano
na listingu 3.27. Można się również zastanowić nad dopisaniem metod analogicznych do
ustawXY, czyli metod ustawXZ oraz ustawYZ, to jednak będzie dobrym ćwiczeniem do sa-
modzielnego wykonania (podpunkt „Ćwiczenia do samodzielnego wykonania”).
Listing 3.27.
class Punkt3D extends Punkt {
int z;
void ustawZ(int wspZ) {
z = wspZ;
}
int pobierzZ() {
return z;
}
void ustawXYZ(int wspX, int wspY, int wspZ) {
x = wspX;
y = wspY;
z = wspZ;
}
}
b4ac40784ca6e05cededff5eda5a930f
b
126 Java. Praktyczny kurs
Co się stanie, jeśli dopiszemy je do kodu z listingu 3.25 i użyjemy razem z progra-
mem z listingu 3.26? Odpowiedni konstruktor z klasy Punkt zostanie wtedy wykonany
(dzieje się tak dlatego, że obiekt klasy Punkt3D, można powiedzieć, zawiera w sobie
obiekt klasy nadrzędnej Punkt). Niestety, co nie powinno być zaskoczeniem, żaden
z konstruktorów klasy Punkt nie zajmuje się polem z, którego w klasie Punkt po prostu
nie ma. Jeśli więc po opisanej zmianie skompilujemy i uruchomimy program testowy
z listingu 3.26, przekonamy się, że tak jest w istocie. Widać to na rysunku 3.15. Pola x i y
zostały zainicjowane wartościami 1, natomiast w polu z pozostała wartość domyślna 0.
Rysunek 3.15.
Efekt działania
konstruktorów
odziedziczonych
po klasie Punkt
Konstruktory dla klasy Punkt3D trzeba więc napisać samodzielnie. Nie jest to skompliko-
wane zadanie, konstruktory te są zaprezentowane na listingu 3.28.
Listing 3.28.
class Punkt3D extends Punkt {
int z;
Punkt3D() {
x = 1;
y = 1;
z = 1;
}
Punkt3D(int wspX, int wspY, int wspZ) {
x = wspX;
y = wspY;
z = wspZ;
}
Punkt3D(Punkt3D punkt) {
x = punkt.x;
y = punkt.y;
z = punkt.z;
}
/*
…pozostałe metody klasy Punkt3D…
*/
}
Jak widać, pierwszy konstruktor nie przyjmuje żadnych argumentów i przypisuje wszyst-
kim polom wartość 1. Drugi konstruktor przyjmuje trzy argumenty: wspX, wspY oraz wspZ,
wszystkie typu int, i przypisuje otrzymane wartości polom x, y i z. Trzeci konstruktor
otrzymuje jako argument obiekt klasy Punkt3D i kopiuje z niego wartości pól. Oczywiście
pozostałe metody klasy Punkt3D pozostają bez zmian, nie zostały one uwzględnione
na listingu, aby nie zaciemniać obrazu oraz nie marnować miejsca na stronie (są na-
tomiast uwzględnione w listingach dostępnych na serwerze FTP).
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 127
Przyglądając się dokładnie napisanym przed chwilą konstruktorom, nie da się nie za-
uważyć, że ich kod w znacznej części dubluje się z kodem konstruktorów klasy Punkt.
Dokładniej, są to te same instrukcje, uzupełnione dodatkowo o instrukcje operujące na
wartościach pola z. Przecież konstruktory:
Punkt3D(int wspX, int wspY, int wspZ) {
x = wspX;
y = wspY;
z = wspZ;
}
oraz:
Punkt(int wspX, int wspY) {
x = wspX;
y = wspY;
}
Ważne jest, aby metoda super(), czyli wywołanie konstruktora klasy bazowej, była
pierwszą instrukcją konstruktora klasy potomnej. Próba umieszczenia tej instrukcji
w innym miejscu spowoduje wygenerowanie błędu kompilacji. Jest on widoczny na
rysunku 3.16.
Rysunek 3.16. Metoda super musi być pierwszą instrukcją w konstruktorze klasy potomnej
b4ac40784ca6e05cededff5eda5a930f
b
128 Java. Praktyczny kurs
Listing 3.29.
class Punkt3D extends Punkt {
int z;
Punkt3D() {
super();
z = 1;
}
Punkt3D(int wspX, int wspY, int wspZ) {
super(wspX, wspY);
z = wspZ;
}
Punkt3D(Punkt3D punkt) {
super(punkt);
z = punkt.z;
}
/*
…pozostałe metody klasy Punkt3D…
*/
}
ale przecież przyjmuje on argument typu Punkt, a nie Punkt3D. Tymczasem klasa z listingu
3.29 kompiluje się bez żadnych problemów! Jak to możliwe? Przecież nie zgadzają się
typy argumentów! Otóż okazuje się, że jeśli oczekiwany jest argument klasy X, a podany
zostanie argument klasy Y, która jest klasą potomną dla X, błędu nie będzie. W takiej sytu-
acji nastąpi tak zwane rzutowanie typu obiektu, które zostanie omówione dokładniej
dopiero w rozdziale 5. W tej chwili warto zapamiętać zasadę: w miejscu, gdzie powi-
nien być zastosowany obiekt pewnej klasy X, można zastosować również obiekt klasy
potomnej dla X10.
10
Istnieją jednak sytuacje, kiedy nie będzie to możliwe. Zajmiemy się dokładniej tym tematem w dalszej
części książki.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 129
Ćwiczenie 16.1.
Zmodyfikuj kod klasy Punkt z listingu 3.25 w taki sposób, aby nazwy argumentów
w metodach ustawX, ustawY oraz ustawXY miały takie same nazwy jak nazwy pól, czyli
x i y. Zatem nagłówki tych metod powinny wyglądać następująco:
void ustawX(int x)
void ustawY(int y)
void ustawXY(int x, int y)
Ćwiczenie 16.2.
Dopisz do klasy Punkt3D zaprezentowanej na listingu 3.27 metodę ustawXZ oraz ustawYZ.
Ćwiczenie 16.3.
Napisz przykładową klasę Main wykorzystującą wszystkie trzy konstruktory klasy
Punkt3D z listingu 3.28 (lub 3.29).
Ćwiczenie 16.4.
Zmodyfikuj kod z listingu 3.28 w taki sposób, aby w żadnym z konstruktorów nie
występowało bezpośrednie przypisanie wartości do pól klasy. Zamiast tego w każdym
konstruktorze wywołaj metodę ustawXYZ.
Ćwiczenie 16.5.
Napisz kod klasy KolorowyPunkt będącej rozszerzeniem klasy Punkt o informację o kolo-
rze. Kolor ma być określany dodatkowym polem o nazwie kolor i typie int (lub in-
nym). Dopisz metody ustawKolor i pobierzKolor, a także odpowiednie konstruktory.
Ćwiczenie 16.6.
Dopisz do klasy Punkt3D z listingu 3.29 konstruktor, który jako argument będzie przyj-
mował obiekt klasy Punkt. Wykorzystaj w tym konstruktorze metodę super.
b4ac40784ca6e05cededff5eda5a930f
b
130 Java. Praktyczny kurs
Domyślnie, jeżeli przed składową klasy nie występuje żadne określenie, dostęp jest pa-
kietowy, co oznacza, że dostęp do tej składowej mają wszystkie klasy wchodzące w skład
danego pakietu (pakiety zostaną omówione w dalszej części lekcji). Dostęp publiczny
jest określany słowem public, prywatny — słowem private, a chroniony — słowem
protected.
Listing 3.30.
class Punkt {
public int x;
public int y;
}
Specyfikator dostępu należy zatem umieścić przed nazwą typu, co schematycznie wygląda
następująco:
specyfikator_dostępu nazwa_typu nazwa_pola
Przykładowo gdyby do klasy Punkt z listingu 3.30 dopisać publiczne metody pobierzX
i pobierzY oraz ustawX i ustawY, miałaby ona postać przedstawioną na listingu 3.31.
Listing 3.31.
class Punkt {
public int x;
public int y;
public int pobierzX() {
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 131
return x;
}
public int pobierzY() {
return y;
}
public void ustawX(int wspX) {
x = wspX;
}
public void ustawY(int wspY) {
y = wspY;
}
}
Listing 3.32.
class Punkt {
private int x;
private int y;
}
O tym, że obydwa pola takiej klasy Punkt rzeczywiście są prywatne, przekonamy się,
pisząc testową klasę Main, w której spróbujemy się odwołać do dowolnego z nich.
Taka przykładowa klasa jest widoczna na listingu 3.33.
Listing 3.33.
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
punkt.x = 100;
punkt.y = 200;
System.out.println("punkt.x = " + punkt.x);
System.out.println("punkt.y = " + punkt.y);
}
}
b4ac40784ca6e05cededff5eda5a930f
b
132 Java. Praktyczny kurs
W jaki zatem sposób odwołać się do pola prywatnego? Przypomnijmy sobie opis prywat-
nej składowej klasy: jest to taka składowa, która jest dostępna z wnętrza danej klasy,
czyli dostęp do niej mają wszystkie metody tej klasy. Wystarczy więc napisać publiczne
metody pobierające i ustawiające pola prywatne, a będzie można wykonywać na nich
operacje. W przypadku klasy Punkt z listingu 3.32 byłyby niezbędne metody ustawX,
ustawY oraz pobierzX i pobierzY. Taka klasa jest przedstawiona na listingu 3.34.
Listing 3.34.
class Punkt {
private int x;
private int y;
public int pobierzX() {
return x;
}
public int pobierzY() {
return y;
}
public void ustawX(int wspX) {
x = wspX;
}
public void ustawY(int wspY) {
y = wspY;
}
}
należy napisać:
punkt.ustawX(100);
Zamiast:
System.out.println("punkt.x = " + punkt.x);
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 133
należy napisać:
System.out.println("punkt.x = " + punkt.pobierzX());
Analogiczne konstrukcje należy zastosować przy dostępie do pola y. W tym miejscu warto
też zwrócić uwagę, że składowe prywatne nie będą dostępne nawet dla klas potom-
nych. Jeżeli z klasy Punkt z listingu 3.32 wyprowadzimy klasę Punkt3D w postaci:
class Punkt3D extends Punkt {
private int z;
}
Listing 3.35.
class Punkt {
protected int x;
protected int y;
}
Jeśli teraz z klasy Punkt wyprowadzimy klasę Punkt3D w postaci widocznej na listingu
3.36, to będzie ona miała (inaczej niż w przypadku składowych prywatnych) pełny dostęp
do składowych x i y klasy Punkt.
Listing 3.36.
class Punkt3D extends Punkt {
protected int z;
}
11
Chyba że jest to klasa dostępna w ramach tego samego pakietu co klasa Punkt.
b4ac40784ca6e05cededff5eda5a930f
b
134 Java. Praktyczny kurs
znajduje się we wnętrzu, jest ukryte, dzięki temu można całkowicie zmienić wewnętrzną
konstrukcję klasy, nie zmieniając zupełnie sposobu korzystania z niej. Mówimy wtedy
o hermetyzacji lub enkapsulacji (ang. encapsulation).
To, że takie podejście jest przydatne, można pokazać nawet na przykładzie tak prostej
klasy, jaką jest nieśmiertelna klasa Punkt. Załóżmy, że ma ona postać widoczną na li-
stingu 3.34. Pola x i y są ukryte, operacje na współrzędnych można wykonywać wyłącznie
dzięki publicznym metodom: pobierzX, pobierzY, ustawX, ustawY. Program przedsta-
wiony na listingu 3.37 będzie zatem działał poprawnie.
Listing 3.37.
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
punkt.ustawX(100);
punkt.ustawY(200);
System.out.println("punkt.x = " + punkt.pobierzX());
System.out.println("punkt.y = " + punkt.pobierzY());
}
}
Rysunek 3.18.
Położenie punktu
reprezentowane za
pomocą współrzędnych
biegunowych
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 135
Niewątpliwie trzeba zamienić pola x i y typu int na pola reprezentujące kąt i odległość
(tzw. moduł). Kąt najlepiej reprezentować za pomocą jego funkcji trygonometrycznej
— wybierzmy np. sinus. Nowe pola nazwijmy sinusalfa oraz modul. Zatem podstawowa
wersja nowej klasy Punkt będzie miała postać:
class Punkt {
private double sinusalfa;
private double modul;
}
Dopisać należy teraz wszystkie cztery metody. Aby to zrobić, niezbędne będą wzory
przekształcające wartości współrzędnych kartezjańskich (tzn. współrzędne [x, y]) na układ
biegunowy (czyli kąt i moduł) oraz wzory odwrotne, czyli przekształcające współ-
rzędne biegunowe na kartezjańskie. Wyprowadzenie tych wzorów nie jest skompliko-
wane, wystarczy znajomość podstawowych funkcji trygonometrycznych oraz twierdzenia
Pitagorasa. Ponieważ ta książka to kurs programowania w Javie, a nie lekcja matematy-
ki, zostaną podane w gotowej postaci12. I tak (dla oznaczeń jak na rysunku 3.18, bez
używania polskich liter):
y modul sin()
oraz:
modul x 2 y 2
y
sin()
modul
Deklarujemy zmienną y typu double i przypisujemy jej wynik mnożenia wartości pól
modul oraz sinusalfa zgodnie z podanymi wyżej wzorami. Ponieważ metoda ma zwrócić
wartość int, a wynikiem obliczeń jest wartość double, przed zwróceniem wyniku do-
konujemy konwersji na typ int. Odpowiada za to konstrukcja (int) y (dokładniejszy opis
tego typu konstrukcji zostanie przedstawiony w dalszej części książki). Analogicznie
napiszemy metodę pobierzX, choć będzie trzeba wykonać nieco więcej obliczeń.
Metoda ta przyjmie następującą postać:
12
W celu uniknięcia umieszczania w kodzie klasy dodatkowych instrukcji warunkowych zaciemniających
sedno zagadnienia przedstawiony kod i wzory są poprawne dla dodatnich współrzędnych x. Uzupełnienie
klasy Punkt w taki sposób, aby możliwe było korzystanie z dowolnych wartości x, można potraktować
jako ćwiczenie do samodzielnego wykonania.
b4ac40784ca6e05cededff5eda5a930f
b
136 Java. Praktyczny kurs
Tym razem deklarujemy, podobnie jak w poprzednim przypadku, zmienną x typu double
oraz przypisujemy jej wynik działania: modul * Math.sqrt(1 - sinusalfa * sinusalfa).
Wyjaśnienia wymaga zapewne konstrukcja Math.sqrt — to standardowa metoda oblicza-
jąca pierwiastek kwadratowy z przekazanego jej argumentu (czyli np. wykonanie instrukcji
Math.sqrt(4) da w wyniku 2). W naszym przypadku ten argument to 1 – sinusalfa *
2
sinusalfa, czyli 1 – sinusalfa , zgodnie z podanym wzorem na współrzędną x. Wyko-
nujemy mnożenie zamiast potęgowania, gdyż jest ono po prostu szybsze i wygodniejsze.
Pozostały jeszcze do napisania metody ustawX i ustawY. Pierwsza z nich będzie mieć
następującą postać:
public void ustawX(int wspX) {
int x = wspX;
int y = pobierzY();
Ponieważ zarówno parametr modul, jak i sinusalfa zależą od obu współrzędnych, musimy
je najpierw uzyskać. Współrzędna x jest oczywiście przekazywana jako argument, nato-
miast y uzyskamy, wywołując odpowiednio przygotowaną metodę pobierzY. Dalsza część
metody ustawX to wykonanie działań zgodnych z podanymi wzorami. Podobnie jak
w przypadku pobierzX, zamiast potęgowania wykonujemy zwykłe mnożenie x * x i y * y.
Metoda ustawY będzie miała prawie identyczną postać, z tą różnicą, że skoro będzie jej
przekazywana wartość współrzędnej y, to trzeba uzyskać jedynie wartość x, czyli począt-
kowe instrukcje będą wyglądały tak:
int x = pobierzX();
int y = wspY;
Kiedy złożymy wszystkie napisane do tej pory elementy w jedną całość, uzyskamy
klasę Punkt w postaci widocznej na listingu 3.38. Teraz po uruchomieniu programu
z listingu 3.37 będzie można się przekonać, że wynik jego działania z nową klasą
Punkt jest dokładnie taki sam jak wtedy, gdy korzystaliśmy z jej poprzedniej postaci.
Mimo całkowitej wymiany wnętrza klasy Punkt program działa tak, jakby nic się nie
zmieniło (rysunek 3.19).
Listing 3.38.
class Punkt {
private double sinusalfa;
private double modul;
public int pobierzY() {
double y = modul * sinusalfa;
return (int) y;
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 137
Rysunek 3.19.
Zmiana implementacji
klasy Punkt nie wpłynęła
na działanie programu
z listingu 3.37
Pakiety
Klasy w Javie są grupowane w jednostki nazywane pakietami. Pakiet to inaczej bi-
blioteka, zestaw powiązanych ze sobą tematycznie klas. Wraz z JDK programista
otrzymuje naprawdę pokaźną liczbę takich pakietów zawierających w sumie kilka (to
nie pomyłka) tysięcy klas. Aby więc sprawnie programować w Javie, trzeba zapoznać
się przynajmniej w podstawowym zakresie z jednostką biblioteczną, jaką jest pakiet.
Tworzenie pakietów
Do tworzenia pakietów służy słowo kluczowe package, po którym następuje nazwa pa-
kietu zakończona znakiem średnika. Schematycznie wygląda to więc następująco:
package nazwa_pakietu;
Ten zapis musi być umieszczony na początku pliku, przed nim nie mogą znajdować
się żadne inne instrukcje. Przed package mogą występować jedynie komentarze. Jeśli
zatem przykładowa klasa Punkt miałaby znajdować się w pakiecie o nazwie grafika,
plik powinien mieć następującą strukturę:
package grafika;
class Punkt {
/*
b4ac40784ca6e05cededff5eda5a930f
b
138 Java. Praktyczny kurs
Aby móc korzystać z obiektów takiej klasy Punkt w innych klasach (np. klasie Main),
trzeba jej nazwę poprzedzić nazwą pakietu (pisząc grafika.Punkt, o czym dalej) lub użyć
polecenia import w schematycznej postaci:
import nazwa_pakietu.nazwa_klasy;
class Main {
/*
…treść klasy Main…
*/
}
Natomiast aby zaimportować wszystkie klasy z pakietu grafika, należy użyć polecenia
import w postaci:
import grafika.*;
Jest ono często stosowane, nawet jeśli używa się tylko kilku klas.
Przyjmuje się, że nazwy pakietów piszemy w całości małymi literami, a jeśli pakiet
ma być udostępniony publicznie, należy poprzedzić go odwróconą nazwą domeny13.
To oczywiście nie jest obligatoryjne, ale pozwala na stworzenie, z dużym prawdopo-
dobieństwem, nazwy unikalnej w skali globu. Jeśli zatem mamy przykładową domenę
marcinlis.com i chcemy stworzyć pakiet o nazwie grafika, jego pełna nazwa powinna
brzmieć com.marcinlis.grafika. Z kolei wszystkie klasy tego pakietu będą musiały
być umieszczone w strukturze katalogów odpowiadających tej nazwie (rysunek 3.20).
13
Rzecz jasna, o ile posiadamy zarejestrowaną domenę internetową.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 139
Rysunek 3.20.
Struktura katalogów
dla pakietu o nazwie
com.marcinlis.grafika
Korzystanie z pakietów
Spróbujmy teraz wykonać poglądowy przykład pokazujący wykorzystanie dwóch klas
o takich samych nazwach, ale znajdujących się w różnych pakietach. Obie będą przecho-
wywały informacje o punktach, ale w przypadku jednej z nich będą to punkty na
płaszczyźnie, a drugiej — w przestrzeni trójwymiarowej. Pierwsza z nich zostanie
umieszczona w pakiecie o nazwie grafika2d, a druga — w pakiecie grafika3d. Obie
klasy są widoczne na listingu 3.39.
Listing 3.39.
package grafika2d;
package grafika3d;
Oczywiście klasę Punkt z pakietu grafika2d umieszczamy w pliku o nazwie Punkt w ka-
talogu grafika2d, a klasę Punkt z pakietu grafika3d — także w pliku Punkt, ale w katalo-
gu o nazwie grafika3d. Ścieżka dostępu do obu katalogów musi być określona w zmiennej
b4ac40784ca6e05cededff5eda5a930f
b
140 Java. Praktyczny kurs
Listing 3.40.
class Main {
public static void main (String args[]) {
grafika2d.Punkt punkt2d = new grafika2d.Punkt();
grafika3d.Punkt punkt3d = new grafika3d.Punkt();
punkt2d.x = 100;
punkt2d.y = 200;
punkt3d.x = 110;
punkt3d.y = 120;
punkt3d.z = 130;
Rysunek 3.21.
Wykorzystanie obiektów
dwóch różnych klas o tej
samej nazwie Punkt
14
Jeśli nie chce się modyfikować zmiennej środowiskowej CLASSPATH, można umieścić katalogi z klasami
pakietów w folderze, w którym znajduje się kod klas korzystających z tych pakietów. A zatem
w omawianym przypadku katalogi grafika2d i grafika3d można umieścić w folderze, w którym
znajduje się kod klasy Main.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 141
Klasy wewnętrzne zostaną omówione dopiero w rozdziale 5., teraz zajmiemy się jedynie
klasami pakietowymi i publicznymi. Otóż jeśli przed nazwą klasy nie ma modyfikatora
public, jest to klasa pakietowa, czyli dostępna jedynie dla innych klas z tego samego pa-
kietu. Jeśli natomiast przed nazwą klasy znajduje się modyfikator public, jest to klasa
publiczna, czyli dostępna dla wszystkich innych klas.
Klasy z listingu 3.39 zostały zadeklarowane właśnie jako publiczne, inaczej program
(dokładniej klasa Main) z listingu 3.40 nie miałby do nich dostępu i nie mógłby tworzyć
obiektów obu klas Punkt. Można się o tym przekonać, zmieniając ich dostęp na pakieto-
wy, czyli usuwając słowa public. Przy próbie kompilacji programu testowego pojawi się
jedynie seria komunikatów o błędach.
Trzeba jeszcze wyjaśnić jedną kwestię — otóż do tej pory trzymaliśmy się zasady, że
w jednym pliku o rozszerzeniu java można umieścić tylko jedną klasę. To oczywiście
nieprawda albo też nie cała prawda. Otóż ta zasada powinna brzmieć: w jednym pliku
java (czyli w jednej jednostce kompilacji) może znaleźć się tylko jedna klasa o dostępie
publicznym. Wynika z tego, że w takim pliku można umieścić dodatkowo klasy o dostę-
pie pakietowym. I rzeczywiście tak jest.
Nie należy zapomnieć, że tym razem, inaczej niż w większości dotychczas przedsta-
wionych przykładów, trzeba zapisać cały listing w jednym pliku o nazwie Punkt.java
— oczywiście w podkatalogu grafika, czyli podkatalogu o nazwie zgodnej z nazwą
pakietu.
b4ac40784ca6e05cededff5eda5a930f
b
142 Java. Praktyczny kurs
Listing 3.41.
package grafika;
class Pomocnicza {
/*
…pola i metody klasy Pomocnicza…
*/
}
Cały listing trzeba zapisać w jednym pliku o nazwie Punkt.java. Ponieważ obie klasy
należą do pakietu grafika, o czym informuje znajdująca się na początku deklaracja
package grafika;, plik Punkt.java powinien znajdować się w podkatalogu grafika.
Ścieżkę do tego katalogu należy zapisać w zmiennej środowiskowej CLASSPATH. Klasa
Punkt jest klasą publiczną, zawiera trzy pola: dwa typu int i jedno typu Pomocnicza.
Klasa Pomocnicza jest pakietowa i dostęp do niej jest możliwy wyłącznie dla klas z pakietu
grafika, czyli w tym przypadku jedynie dla klasy Punkt (w pakiecie grafika nie ma
bowiem żadnych innych klas).
W wyniku skompilowania pliku Punkt.java powstaną dwa pliki class: Punkt.class oraz
Pomocnicza.class. Do klasy Pomocnicza będzie się jednak można odwołać jedynie z wnę-
trza klasy Punkt. Testowa klasa Main, której kod jest widoczny na listingu 3.42, nie
będzie miała dostępu do klasy Pomocnicza, nie będzie więc mogła tworzyć jej obiektów.
Próba kompilacji15 tego kodu skończy się komunikatami o błędach widocznymi na ry-
sunku 3.22. Kiedy jednak usunięte zostaną odwołania do klasy Pomocnicza, kompilacja
przebiegnie bez problemów.
Listing 3.42.
import grafika.*;
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
Pomocnicza pomocnicza = new Pomocnicza();
}
}
15
W celu uproszczenia zadania można podkatalog grafika umieścić bezpośrednio w katalogu, w którym
znajduje się plik Main.java. Nie trzeba wtedy modyfikować zmiennej CLASSPATH.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 143
Rysunek 3.22.
Próba odwołania się
do klasy o dostępie
pakietowym kończy się
błędami kompilacji
Warto zatem podkreślić jeszcze raz: publiczna klasa Punkt znajdująca się w pakiecie
grafika ma pełny dostęp do pakietowej klasy Pomocnicza (obie znajdują się wręcz w tym
samym pliku), jednak klasa Main (spoza pakietu grafika) nie może odwoływać się do kla-
sy Pomocnicza. W klasie Main wolno jedynie tworzyć obiekty publicznej klasy Punkt.
Oczywiście dostęp do pakietu jest możliwy dzięki poleceniu import grafika.*.
Można zadać sobie pytanie, jakiego rodzaju w takim razie były klasy Punkt i Main, których
używaliśmy, począwszy od lekcji 13. W pliku Punkt.java umieszczany był kod klasy
Punkt w postaci:
class Punkt {
/*
…treść klasy Punkt…
*/
}
A zatem nie było ani modyfikatora public, ani też słowa package. Czyżby nie były one ani
publiczne, ani pakietowe? Oczywiście nie. Obie klasy w takiej postaci były klasami pa-
kietowymi. Wskazówką jest to, że znajdowały się w jednym katalogu. Kompilator tworzył
więc tymczasowy pakiet domyślny, niemający nazwy, do którego zaliczał obie klasy.
Dlatego też możliwa była kompilacja i wykonanie prostych przykładowych programów.
Ćwiczenie 17.1.
Napisz klasę Punkt3D dziedziczącą po klasie Punkt zaprezentowanej na listingu 3.35, taką,
że pole z będzie polem publicznym. Zastanów się, do których pól klasy 3D będzie można
się odwoływać z innych klas oraz czy taka konstrukcja będzie miała praktyczny sens.
b4ac40784ca6e05cededff5eda5a930f
b
144 Java. Praktyczny kurs
Ćwiczenie 17.2.
Napisz wersję klasy Punkt, w której pola x i y będą przechowywane w tablicy. Klasa
powinna współpracować z klasą Main z listingu 3.37. Zastanów się, jakie składowe klasy
będą niezbędne i jakich modyfikatorów dostępu należy użyć.
Ćwiczenie 17.3.
Utwórz pakiet o nazwie biblioteka, umieść w nim dwie przykładowe klasy: Nowela
i Powiesc. Napisz dodatkową publiczną klasę Main, która będzie się odwoływała do
obiektów tych klas. Pamiętaj o użyciu instrukcji import.
Ćwiczenie 17.4.
Przygotuj taką strukturę klas, aby w jednym programie można było używać trzech
wersji klasy Punkt: z listingu 3.34, z listingu 3.38 i z ćwiczenia 17.2 (wszystkie wersje
mają mieć nazwę Punkt; możesz zmienić typ klasy z pakietowej na publiczną lub od-
wrotnie). Napisz przykładowy program, w którym powstaną obiekty tych trzech róż-
nych klas.
Ćwiczenie 17.5.
Utwórz pakiet o nazwie mojpakiet. Umieść w nim klasę o dostępie publicznym i nazwie
MojaKlasa. W tej klasie zadeklaruj dwa pola typu int o nazwach liczba1 i liczba2.
Pierwsze z nich ma być polem publicznym, drugie polem pakietowym. Zastanów się,
czy będzie można w dowolnej klasie publicznej tworzyć obiekty klasy MojaKlasa, a jeśli
tak, do których pól tej klasy będzie można się swobodnie odwoływać.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 145
Listing 3.43.
public class A {
public void f() {
System.out.println("Metoda f() z klasy A");
}
}
Listing 3.44.
public class B extends A {
public void f(int liczba) {
System.out.println("Metoda f(int) z klasy B");
}
}
Listing 3.45.
public class Main {
public static void main (String args[]) {
A a = new A();
B b = new B();
b4ac40784ca6e05cededff5eda5a930f
b
146 Java. Praktyczny kurs
Rysunek 3.23.
Ilustracja mechanizmu
przeciążania metod
w klasach potomnych
Przesłanianie metod
Wiadomo już, że w klasach potomnych można przeciążać metody zdefiniowane w klasie
bazowej, jest to wręcz zgodne z logiką i intuicją. Byłoby to dziwne, gdyby zabrakło
takiej możliwości. Co się jednak stanie, kiedy w klasie potomnej ponownie zdefiniujemy
metodę o takiej samej nazwie i takich samych argumentach jak w klasie bazowej? Al-
bo inaczej: jakiego zachowania metod należałoby się spodziewać w przypadku klas
przedstawionych na listingu 3.46 (klasy należy zapisać w jednym katalogu w dwóch
plikach: A.java i B.java)?
Listing 3.46.
public class A {
public void f() {
System.out.println("Metoda f z klasy A");
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 147
nazw (dwukrotne zadeklarowanie metody f), jednak próba kompilacji wykaże, że kompi-
lator nie zgłasza żadnych błędów. Czemu konflikt nazw nie występuje? Otóż zasada
jest następująca: jeśli w klasie bazowej i pochodnej są metody o tej samej nazwie
i argumentach, metoda z klasy bazowej jest przesłaniana. Tę technikę nazywamy
przesłanianiem metod (przykrywaniem, nadpisywaniem16, ang. overriding).
W obiektach klasy bazowej będzie zatem obowiązywała metoda z klasy bazowej, a w obiek-
tach klasy pochodnej — metoda z klasy pochodnej. Sprawdźmy to na przykładzie klas
z listingu 3.46. Co pojawi się na ekranie po uruchomieniu klasy Main z listingu 3.47,
która korzysta z obiektów klas A i B? Oczywiście najpierw tekst Metoda f z klasy A,
a następnie Metoda f z klasy B. Skoro bowiem obiekt a jest typu A, to wywołanie a.f()
powoduje uruchomienie metody f z klasy A, czyli wyświetlenie tekstu Metoda f z klasy A.
Z kolei typem obiektu b jest B, zatem wywołanie b.f() powoduje uruchomienie me-
tody f z klasy B i wyświetlenie na ekranie tekstu Metoda f z klasy B (rysunek 3.24).
Listing 3.47.
public class Main {
public static void main (String args[]) {
A a = new A();
B b = new B();
a.f();
b.f();
}
}
Rysunek 3.24.
Efekt wywoływania
przesłoniętych metod
Może się w tym miejscu pojawić pytanie, czy jest w takim razie możliwe wywołanie
w klasie pochodnej przesłoniętej metody z klasy bazowej. Ten problem może się wy-
dawać zawiły. Spójrzmy na przykład klas z listingu 3.46 — chodzi o to, czy w obiek-
cie klasy B można wywołać metodę f pochodzącą z klasy A. Nie jest to zagadnienie
czysto teoretyczne, gdyż w praktyce programistycznej takie odwołania często uprasz-
czają kod i ułatwiają tworzenie spójnych hierarchii klas. Skoro tak, to takie odwołanie
oczywiście jest możliwe. Jak wiadomo z lekcji 16., jeśli trzeba było wywołać konstruktor
klasy bazowej, wystarczyło skorzystać ze słowa super. W tym przypadku jest dokładnie
tak samo. Odwołanie do przesłoniętej metody klasy bazowej uzyskujemy dzięki wy-
wołaniu w schematycznej postaci:
super.nazwa_metody(argumenty);
16
Akurat ten termin nie oddaje w pełni zasady działania omawianego mechanizmu.
b4ac40784ca6e05cededff5eda5a930f
b
148 Java. Praktyczny kurs
Listing 3.48.
public class A {
public void f() {
System.out.println("Klasa A");
}
}
Listing 3.49.
public class Main {
public static void main (String args[]) {
A a = new A();
B b = new B();
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 149
Rysunek 3.25.
Ilustracja działania
techniki przesłaniania
metod w klasach
potomnych
Przeanalizujmy zasadę działania tego zestawu klas i zastanówmy się, czemu rezultat
jest właśnie taki. Klasa A ma postać znaną nam z wcześniejszych przykładów. Została
w niej zdefiniowana metoda f, której jedynym zadaniem jest wyświetlenie nazwy klasy na
ekranie. Klasa B jest klasą pochodną od A, czyli dziedziczy po A. Zdefiniowano w niej
metodę f, która przesłania metodę f z klasy A. Jednakże w tej metodzie jest wywoływana,
za pomocą składni super, metoda przesłonięta. Dopiero po tym wywołaniu na ekranie
zostaje wyświetlona nazwa klasy B.
W klasie B występują jeszcze dodatkowo dwie inne metody, g i h. Pierwsza z nich wywo-
łuje w standardowy sposób metodę f (czyli będzie to metoda f z klasy B), druga ko-
rzysta ze składni super w postaci: super.f(), zatem wywołuje metodę f zdefiniowaną
w klasie A.
Trzecia klasa — Main — jest klasą testową. Tworzony jest w niej jeden obiekt o nazwie a
(typu A) i jeden o nazwie b (typu B). Następnie jest wywoływana metoda f obiektu klasy A,
zatem na ekranie pojawia się napis Klasa A. W kolejnym kroku jest wywoływana me-
toda f obiektu b. Ponieważ najpierw wywołuje ona metodę f klasy A, a dopiero potem wy-
świetla na ekranie nazwę klasy B, wynikiem jej działania są dwa teksty: Klasa A i Klasa B.
Krok trzeci to wywołanie metody g obiektu b, której jedynym zadaniem jest wywołanie
metody f z klasy B. Zatem zachowanie będzie takie samo jak w kroku drugim: na ekranie
pojawią się napisy Klasa A i Klasa B. W kroku ostatnim — czwartym — jest wywo-
ływana metoda h obiektu b. Jest w niej uruchamiana, przy użyciu składni super, me-
toda f z klasy A, zatem na ekranie pojawia się tekst Klasa A. Ostatecznie otrzymywany
jest wynik widoczny na rysunku 3.25.
Przesłanianie pól
Pola klas bazowych są przesłaniane w sposób analogiczny do wykorzystywanego w przy-
padku metod. Jeśli więc w klasie pochodnej zdefiniowane zostanie pole o takiej samej
nazwie jak w klasie bazowej, bezpośrednio dostępne będzie tylko pole klasy pochodnej.
Taka sytuacja została zobrazowana na listingu 3.50.
b4ac40784ca6e05cededff5eda5a930f
b
150 Java. Praktyczny kurs
Listing 3.50.
public class A {
public int liczba;
}
W klasie A zostało zdefiniowane pole o nazwie liczba i typie int. W klasie B, która dzie-
dziczy po A, ponownie zostało zadeklarowane pole o takiej samej nazwie i identycznym
typie. Trzeba sobie jednak dobrze uświadomić, że każdy obiekt klasy B będzie w takiej
sytuacji zawierał DWA pola o nazwie liczba — jedno pochodzące z klasy A, drugie z B.
Tym polom można z kolei przypisywać różne wartości.
Jeśli mamy obiekt takiej klasy B, to z dowolnej klasy zewnętrznej jesteśmy w stanie
dostać się jedynie do pola liczba zdefiniowanego w klasie B. Jednak już z wnętrza klasy B
za pomocą składni super możemy odwołać się również do drugiego pola liczba (po-
chodzącego z klasy A). Taka sytuacja została przedstawiona na listingu 3.51.
Listing 3.51.
public class A {
public int liczba;
}
Do klasy B zostały dopisane cztery metody operujące na obydwu polach liczba. Metody
ustawLiczbaB i pobierzLiczbaB operują na polu liczba w sposób standardowy (czyli
odwołują się do pola pochodzącego z klasy B). Pierwsza z nich pozwala na jego ustawia-
nie, druga zwraca jego aktualną wartość. Dwie pozostałe metody, czyli ustawLiczbaA
i pobierzLiczbaA, operują na polu liczba odziedziczonym po klasie A. Aby odwołać się
do tego pola, wykorzystują składnię super, czyli odwołanie wygląda następująco:
super.liczba
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 151
Możemy teraz napisać klasę testową Main, która udowodni, że obiekt klasy B napraw-
dę zawiera dwa pola o takiej samej nazwie. Taka klasa Main jest zaprezentowana na
listingu 3.52.
Listing 3.52.
public class Main {
public static void main (String args[]) {
B b = new B();
b.ustawLiczbaA(0);
b.ustawLiczbaB(1);
Rysunek 3.26.
Ilustracja przesłaniania
pól klasy bazowej
Warto tu zauważyć, że pole przesłaniające nie musi być tego samego typu co przesłaniane.
Tak więc może istnieć w klasie A pole o nazwie pole1 i typie int, a w dziedziczącej po
niej klasie B pole o nazwie pole1, ale typie np. char. Taka sytuacja została przedsta-
wiona na listingu 3.53. Podobnych konstrukcji należy jednak zdecydowanie unikać —
nie ma zwykle praktycznej potrzeby stosowania tego typu techniki, wprowadza ona
bowiem niepotrzebne zamieszanie w budowie klas.
Listing 3.53.
public class A {
protected int pole1;
}
b4ac40784ca6e05cededff5eda5a930f
b
152 Java. Praktyczny kurs
Składowe statyczne
Składowe statyczne to pola i metody klasy, które mogą istnieć, nawet jeśli nie istnieje
obiekt tej klasy. Takie metody lub pola są wspólne dla wszystkich obiektów danej
klasy. Składowe te są oznaczane słowem static. W dotychczasowych przykładach
wykorzystywana była jedna metoda tego typu — main, od której rozpoczyna się wykony-
wanie programu.
Metody statyczne
Metodę statyczną oznacza się słowem static, które zwyczajowo powinno znaleźć się
zaraz za specyfikatorem dostępu17, zatem schematycznie deklaracja metody statycznej
będzie wyglądała następująco:
specyfikator_dostępu static typ_zwracany nazwa_metody(argumenty) {
//treść metody
}
Przykładowa klasa z zadeklarowaną metodą statyczną może wyglądać tak jak na li-
stingu 3.54.
Listing 3.54.
public class A {
public static void f() {
System.out.println("Metoda f klasy A");
}
}
Ten sposób jest nam doskonale znany — dotychczas wszystkie metody były wywoływane
za pomocą tej techniki:
nazwa_obiektu.nazwa_metody(argumenty metody);
Ponieważ jednak metody statyczne istnieją nawet wtedy, kiedy nie ma żadnego obiektu
danej klasy, jest możliwe wywołanie w postaci:
nazwa_klasy.nazwa_metody(argumenty metody);
17
W rzeczywistości słowo kluczowe static może pojawić się również przed specyfikatorem dostępu,
ta kolejność nie jest bowiem istotna z perspektywy kompilatora. Przyjmuje się jednak, że — ze względu
na ujednolicenie notacji — o ile występuje specyfikator dostępu metody, słowo static powinno
znaleźć się za nim; przykładowo: public static void main, a nie static public void main.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 153
Na listingu 3.55 jest przedstawiona przykładowa klasa Main, która korzysta z takiego
wywołania. Uruchomienie tego kodu pozwoli się przekonać, że w przypadku metody
statycznej rzeczywiście nie trzeba tworzyć obiektu.
Listing 3.55.
public class Main {
public static void main (String args[]) {
A.f();
}
}
Dlatego też metoda main, od której rozpoczyna się wykonywanie kodu klasy, jest
metodą statyczną, może bowiem zostać wykonana, mimo że w trakcie uruchamiania apli-
kacji nie powstały jeszcze żadne obiekty.
Należy jednak zdawać sobie sprawę, że metoda statyczna jest umieszczana w specjalnie
zarezerwowanym do tego celu obszarze pamięci i jeśli powstaną obiekty danej klasy,
będzie ona dla nich wspólna. To znaczy, że dla każdego obiektu klasy nie tworzy się
kopii metody statycznej. Najłatwiej będzie jednak pokazać to na przykładzie pól sta-
tycznych.
Pola statyczne
Do pól oznaczonych jako statyczne można się odwoływać, podobnie jak w przypadku
metod statycznych, nawet jeśli nie istnieje żaden obiekt danej klasy. Pola takie deklaruje-
my, umieszczając przed typem słowo static. Schematycznie deklaracja taka wygląda
następująco:
static typ_pola nazwa_pola;
lub:
specyfikator_dostępu static typ_pola nazwa_pola;
Jeśli zatem w naszej przykładowej klasie A ma się pojawić pole statyczne o nazwie
liczba typu int o dostępie publicznym, taka klasa będzie miała postać widoczną na
listingu 3.5618.
Listing 3.56.
public class A {
public static int liczba;
}
18
Podobnie jak w przypadku metod statycznych, słowo static może się znaleźć przed specyfikatorem
dostępu, czyli przykładowo: static public int liczba. Jednak dla ujednolicenia notacji będziemy
konsekwentnie stosować formę zaprezentowaną w powyższym akapicie, czyli public static int liczba.
b4ac40784ca6e05cededff5eda5a930f
b
154 Java. Praktyczny kurs
Do pól statycznych odwołujemy się w sposób klasyczny, tak jak do innych pól klasy
— poprzedzając je nazwą obiektu (oczywiście jeśli wcześniej stworzymy dany obiekt),
czyli stosując konstrukcję:
nazwa_obiektu.nazwa_pola
Podobnie jak metody statyczne, również pola tego typu znajdują się w wyznaczonym
obszarze pamięci i są wspólne dla wszystkich obiektów danej klasy. Tak więc nieza-
leżnie od liczby obiektów danej klasy pole statyczne o danej nazwie będzie tylko jedno.
Przykład ilustrujący to zagadnienie jest widoczny na listingu 3.57 (korzystamy w nim
z klasy A z listingu 3.56).
Listing 3.57.
public class Main {
public static void main (String args[]) {
//krok 1
A.liczba = 100;
//krok 2
A obiekt1 = new A();
A obiekt2 = new A();
//krok 3
obiekt1.liczba = 200;
//krok 4
obiekt2.liczba = 300;
W pierwszym kroku przypisujemy polu statycznemu liczba z klasy A wartość 100. Stosu-
jemy odwołanie w postaci nazwa_klasy.nazwa_pola = wartość, czyli A.liczba = 100.
Można tu zauważyć, że nie powstał jeszcze żaden obiekt klasy A, tymczasem polu
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 155
Krok czwarty to już tylko formalność. Tym razem przypisujemy polu liczba wartość 300,
stosując odwołanie w postaci obiekt2.liczba = 300. Oczywiste jest, że i tym razem
efekt będzie analogiczny do uzyskanego w poprzednich przypadkach. Tak więc odczyta-
nie wartości zarówno za pomocą konstrukcji obiekt1.liczba, jak i obiekt2.liczba da
w wyniku wartość 300. Nic dziwnego, skoro istnieje tylko jedno pole liczba, po pro-
stu odwołujemy się do niego w różny sposób. Ostatecznie otrzymamy wynik widoczny
na rysunku 3.27.
Rysunek 3.27.
Różne typy odwołań
do pola statycznego
z klasy A
Ćwiczenie 18.1.
Napisz klasę Punkt3D dziedziczącą po klasie Punkt zaprezentowanej na listingu 3.3
(dostęp do klasy i do składowych zmień na publiczny). Zdefiniuj w niej pole typu int
o nazwie z oraz metodę wyswietlWspolrzedne przesłaniającą metodę wyswietlWspolrzedne
z klasy Punkt.
b4ac40784ca6e05cededff5eda5a930f
b
156 Java. Praktyczny kurs
Ćwiczenie 18.2.
Napisz klasę Opakowanie zawierającą pole typu int o nazwie liczba oraz klasę Nowe
Opakowanie dziedziczącą po Opakowanie i również zawierającą pole liczba. W klasie
NoweOpakowanie zdefiniuj metodę pobierzWartosc przyjmującą jeden argument typu
boolean. Jeśli wartością argumentu będzie true, metoda ta ma zwracać wartość pola
liczba zdefiniowanego w klasie NoweOpakowanie, a jeśli tą wartością będzie false, ma
ona zwrócić wartość pola liczba odziedziczonego po klasie Opakowanie.
Ćwiczenie 18.3.
Do klasy NoweOpakowanie z ćwiczenia 18.2 dopisz metodę ustawWartosc przyjmującą
dwa argumenty: pierwszy typu int, drugi typu boolean. Jeśli wartością drugiego argu-
mentu będzie true, wartość pierwszego przypisz polu liczba zdefiniowanemu w klasie
NoweOpakowanie, jeśli natomiast wartością drugiego argumentu będzie false, wartość
pierwszego przypisz polu liczba zdefiniowanemu w klasie Opakowanie.
Ćwiczenie 18.4.
Napisz klasę Dodawanie zawierającą metodę statyczną dodaj przyjmującą dwa argumenty
typu int. Metoda ta powinna zwrócić wartość będącą wynikiem dodawania obu ar-
gumentów.
Ćwiczenie 18.5.
Napisz klasę Przechowalnia zawierającą metodę statyczną o nazwie przechowaj przyjmu-
jącą jeden argument typu int. Klasa ta ma zapamiętywać argument przekazany meto-
dzie przechowaj w taki sposób, że każde wywołanie tej metody spowoduje zwrócenie
poprzednio zapisanej wartości i zapamiętanie aktualnie przekazanej.
Klasy finalne
Klasa finalna to taka, z której nie wolno wyprowadzać innych klas. Pozwala to pro-
gramiście tworzyć klasy, które nie będą miały klas pochodnych — ich postać będzie
po prostu z góry ustalona. Jeśli klasa ma stać się finalna, należy przed jej nazwą
umieścić słowo kluczowe final zgodnie ze schematem:
specyfikator_dostępu final class nazwa_klasy {
//pola i metody klasy
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 157
Tak więc przykładowa klasa finalna Ostateczna mogłaby wyglądać jak na listingu 3.58.
Listing 3.58.
public final class Ostateczna {
public int liczba;
public void wyswietl() {
System.out.println(liczba);
}
}
Podobnie jak w przypadku słowa kluczowego static (lekcja 18.), nie ma formalnego
znaczenia to, czy napiszemy public final class, czy final public class, niemniej dla
przejrzystości i ujednolicenia notacji będziemy konsekwentnie stosować pierwszy przed-
stawiony sposób zapisu.
Listing 3.59.
public class RozszerzonaOstateczna extends Ostateczna {
public int liczba2;
/*
…dalsze pola i metody klasy…
*/
}
Rysunek 3.28. Próba dziedziczenia po klasie finalnej kończy się błędem kompilacji
Pola finalne
Pole klasy oznaczone słowem final staje się finalne, a więc jego wartość jest stała i nie
można jej zmieniać. Słowo kluczowe final zwyczajowo umieszcza się przed nazwą
typu danego pola — pisze się zatem:
final typ_pola nazwa_pola
lub ogólniej:
specyfikator_dostępu [static] final typ_pola nazwa_pola
b4ac40784ca6e05cededff5eda5a930f
b
158 Java. Praktyczny kurs
Listing 3.60.
public final class Obliczenia {
public final int liczba1 = 100;
public int liczba2;
public void licz() {
//prawidłowo: odczyt pola liczba1, zapis pola liczba2
liczba2 = 2 * liczba1;
System.out.println(liczba1);
System.out.println(liczba2);
}
public static void main(String args[]) {
Obliczenia obliczenia = new Obliczenia();
obliczenia.licz();
}
}
Zostały tu zadeklarowane dwa pola, liczba1 i liczba2, oba publiczne o typie int.
Pierwsze jest również polem finalnym, przypisanej mu wartości nie wolno zmieniać.
W klasie znajduje się także dodatkowa metoda licz, która wykonuje działania, wyko-
rzystując wartości przypisane zadeklarowanym polom. Na początku zmiennej liczba2
przypisujemy wynik mnożenia 2 * liczba1. Jest to poprawna instrukcja, gdyż wolno
odczytywać wartość pola finalnego liczba1 oraz przypisywać wartości zwykłemu
polu liczba2. Identyczna sytuacja zachodzi w przypadku drugiego działania. Trzecia
instrukcja została ujęta w komentarz, gdyż jest nieprawidłowa i spowodowałaby błąd
kompilacji widoczny na rysunku 3.29. Występuje tu bowiem próba przypisania wyniku
działania liczba2 / 2 polu liczba1, które jest polem finalnym. Takiej operacji nie wolno
wykonywać, zatem po usunięciu znaków komentarza z tej instrukcji kompilator zdecydo-
wanie zaprotestuje. Do klasy Obliczenia została też dopisana metoda main (lekcja 14.),
w której tworzymy nowy obiekt typu Obliczenia i wywołujemy jego metodę licz.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 159
Gdyby pole to było finalne, tej referencji nie wolno byłoby zmieniać, jednak nic nie stało-
by na przeszkodzie, aby modyfikować pola obiektu, na który ta referencja wskazuje.
Czyli po wykonaniu instrukcji:
final Punkt punkt = new Punkt();
Listing 3.61.
public class Punkt {
public int x;
public int y;
}
b4ac40784ca6e05cededff5eda5a930f
b
160 Java. Praktyczny kurs
Wydaje się to logiczne, jednak wcale nie musi tak być. Otóż pola finalne można de-
klarować również bez ich inicjalizacji. Wartością takiego pola staje się wartość użyta
w pierwszym przypisaniu i dopiero od chwili tego przypisania nie wolno jej zmieniać.
Pole takie musi jednak zostać zainicjalizowane w konstruktorze danej klasy. Należy tylko
pamiętać, że po pierwszym przypisaniu wartości polu finalnemu nie wolno mu już
przypisywać innych wartości. Kilka przykładowych, poprawnych oraz niepoprawnych
inicjalizacji zostało pokazanych na listingu 3.62.
Listing 3.62.
public class Main {
public final int liczba1 = 100;
public final int liczba2;
public final Punkt punkt1 = new Punkt();
public final Punkt punkt2;
public Main() {
//prawidłowo, można modyfikować pola obiektu punkt1
punkt1.x = 100;
punkt1.y = 200;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 161
Metody finalne
Metoda oznaczona słowem final staje się finalna, co oznacza, że jej przesłonięcie
w klasie potomnej nie będzie możliwe. Słowo final umieszcza się przed typem wartości
zwracanej przez metodę, czyli schematycznie konstrukcja taka wygląda następująco:
final typ_zwracany nazwa_metody(argumenty)
lub ogólniej:
specyfikator_dostępu [static] final typ_zwracany nazwa_metody(argumenty).
Ponieważ w jednej klasie nie mogą znaleźć się dwie metody o tej samej nazwie i takich
samych argumentach, metody finalne będą się różniły od zwykłych tylko w przypad-
ku dziedziczenia. Dokładniej, zgodnie z tym, co zostało napisane powyżej, metod fi-
nalnych nie można przesłaniać w klasach potomnych. Zilustrowano to w przykładzie
widocznym na listingu 3.63.
Listing 3.63.
public class Bazowa {
public final void metoda() {
//kod metody
}
}
b4ac40784ca6e05cededff5eda5a930f
b
162 Java. Praktyczny kurs
Rysunek 3.30. Próba przesłonięcia metody finalnej kończy się błędem kompilacji
Argumenty finalne
Wiadomo już, że finalne mogą być klasy, pola oraz metody. Finalne mogą być rów-
nież argumenty metod. Argument finalny to taki, którego nie wolno zmieniać wewnątrz
metody. Aby uzyskać argument finalny, należy umieścić słowo final przed jego typem.
Schematycznie konstrukcja taka będzie wyglądała następująco:
specyfikator_dostępu [static][final] typ_zwracany nazwa_metody(final typ_argumentu
nazwa_argumentu)
Spójrzmy teraz na listing 3.64. Została na nim przedstawiona klasa z metodami ilu-
strującymi zachowanie argumentów finalnych.
Listing 3.64.
public class ArgumentyFinalne {
public void metoda1(final int liczba) {
//prawidłowo, można odczytywać wartość argumentu
int x = liczba;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3. Programowanie obiektowe. Część I 163
Ćwiczenie 19.1.
Napisz klasę zawierającą dwa pola statyczne i finalne, jedno typu int, drugie typu double.
Oba pola zainicjuj w trakcie deklaracji, przypisując im wartość 1.
Ćwiczenie 19.2.
Napisz klasę zawierającą jedno pole statyczne klasy Punkt. Użyj klasy Punkt z listingu 3.19.
Pole zainicjuj, wykorzystując konstruktor dwuargumentowy przyjmujący dwie wartości
typu int. Współrzędna x pola ma zostać zainicjowana wartością 100, a y wartością 200.
Ćwiczenie 19.3.
Napisz klasę finalną Punkt3D dziedziczącą po klasie Punkt z listingu 3.1. W klasie
Punkt3D zdefiniuj jedno pole typu int o nazwie z.
Ćwiczenie 19.4.
Zmodyfikuj klasę Punkt z listingu 3.1 tak, aby stała się klasą publiczną zawierającą dwa
pola finalne typu int o nazwach x i y. Z tej klasy wyprowadź klasę Punkt3D, w której
będzie zdefiniowane dodatkowe pole finalne o nazwie z i typie int. Czy takie klasy będą
miały praktyczne zastosowanie?
b4ac40784ca6e05cededff5eda5a930f
b
164 Java. Praktyczny kurs
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4.
Wyjątki
Praktycznie w każdym większym programie powstają jakieś błędy. Powodów jest bardzo
wiele — może to być skutek niefrasobliwości programisty, założenia, że wprowadzone
dane są zawsze poprawne, niedokładnej specyfikacji poszczególnych modułów aplikacji,
użycia niesprawdzonych bibliotek czy nawet zwykłego zapomnienia o zainicjowaniu
tylko jednej zmiennej. Na szczęście w Javie, tak jak w większości współczesnych języków
programowania, istnieje mechanizm tzw. wyjątków, który pozwala na przechwytywanie
błędów. Ta właśnie tematyka zostanie przedstawiona w trzech kolejnych lekcjach.
b4ac40784ca6e05cededff5eda5a930f
b
166 Java. Praktyczny kurs
Dużo więcej kłopotów mogłaby sprawić sytuacja, w której np. tablica byłaby deklaro-
wana w jednej klasie, a odwołanie do niej następowałoby w innej. Taka sytuacja została
przedstawiona na listingu 4.1.
Listing 4.1.
public class Tablica {
private int[] tablica = new int[10];
public int pobierzElement(int indeks) {
return tablica[indeks];
}
public void ustawElement(int indeks, int wartosc) {
tablica[indeks] = wartosc;
}
}
Widoczne są dwie klasy: Tablica oraz Main. W klasie Tablica zostało zadeklarowane pry-
watne pole typu tablicowego o nazwie tablica, któremu została przypisana 10-elemen-
towa tablica liczb całkowitych. Ponieważ to pole jest prywatne (lekcja 17.), dostęp do
niego mają jedynie inne składowe klasy Tablica. Dlatego też powstały dwie metody, po-
bierzElement oraz ustawElement, operujące na elementach tablicy. Metoda pobierzElement
zwraca wartość zapisaną w komórce o indeksie przekazanym jako argument, natomiast
ustawElement zapisuje wartość drugiego argumentu w komórce o indeksie wskazywanym
przez argument pierwszy.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4. Wyjątki 167
Jak poradzić sobie z takim problemem? Pierwszym nasuwającym się sposobem jest
sprawdzenie w metodach pobierzElement i ustawElement, czy przekazane argumenty nie
przekraczają dopuszczalnych wartości. Jeśli takie przekroczenie nastąpi, należy zasygnali-
zować błąd. To jednak prowokuje pytanie: w jaki sposób ten błąd sygnalizować? Jednym
z pomysłów jest zwracanie przez funkcję (metodę) wartości –1 w przypadku błędu
oraz wartości nieujemnej (najczęściej 0), jeśli błąd nie wystąpił. To rozwiązanie będzie
dobre w przypadku metody ustawElement, która będzie wtedy wyglądała następująco:
public int ustawElement(int indeks, int wartosc) {
if(indeks >= tablica.length || indeks < 0){
return –1;
}
else{
tablica[indeks] = wartosc;
return 0;
}
}
Wystarczyłoby teraz w klasie Main testować wartość zwróconą przez ustawElement, aby
sprawdzić, czy nie został przekroczony dopuszczalny indeks tablicy. Niestety tej techniki
nie można zastosować w przypadku metody pobierzElement — przecież zwraca ona
wartość zapisaną w jednej z komórek tablicy. Czyli –1 i 0 użyte przed chwilą do zasygna-
lizowania, czy operacja zakończyła się błędem, mogą być wartościami odczytanymi
z tablicy. Trzeba zatem wymyślić inną metodę. Może to być np. wykorzystanie dodatko-
wego pola sygnalizującego w klasie Tablica. Pole to byłoby typu boolean. Ustawione
na true oznaczałoby, że ostatnia operacja zakończyła się błędem, natomiast ustawione
na false — że zakończyła się sukcesem. Klasa Tablica miałaby wtedy postać jak na
listingu 4.2.
Listing 4.2.
public class Tablica {
private int[] tablica = new int[10];
public boolean wystapilBlad = false;
public int pobierzElement(int indeks) {
if(indeks >= tablica.length){
wystapilBlad = true;
return 0;
}
else{
wystapilBlad = false;
return tablica[indeks];
}
}
public void ustawElement(int indeks, int wartosc) {
if(indeks >= tablica.length){
wystapilBlad = true;
}
else{
tablica[indeks] = wartosc;
wystapilBlad = false;
}
}
}
b4ac40784ca6e05cededff5eda5a930f
b
168 Java. Praktyczny kurs
Do klasy zostało dodane pole typu boolean o nazwie wystapilBlad. Jego początkowa
wartość to false. W metodzie pobierzElement sprawdzamy najpierw, czy przekazany in-
deks nie przekracza maksymalnej dopuszczalnej wartości1. Jeśli tak, ustawiamy pole
wystapilBlad na true oraz zwracamy wartość 0. Oczywiście w tym przypadku zwróco-
na wartość nie ma żadnego praktycznego znaczenia (przecież wystąpił błąd), niemniej
coś musimy zwrócić. Użycie instrukcji return i zwrócenie wartości typu int jest ko-
nieczne, inaczej kompilator zgłosi błąd. Jeśli jednak argument przekazany metodzie
nie przekracza dopuszczalnego indeksu tablicy, pole wystapilBlad ustawiamy na false
oraz zwracamy wartość znajdującą się pod wskazanym indeksem.
Listing 4.3.
public class Main {
public static void main (String args[]) {
Tablica tablica = new Tablica();
tablica.ustawElement(5, 10);
int liczba = tablica.pobierzElement(10);
if (tablica.wystapilBlad){
System.out.println("Nieprawidłowy indeks tablicy...");
}
else{
System.out.println(liczba);
}
}
}
Podstawowe wykonywane operacje są takie same jak w przypadku klasy z listingu 4.1,
tyle że po pobraniu elementu sprawdzamy, czy operacja ta zakończyła się sukcesem,
i wyświetlamy odpowiedni komunikat na ekranie. Identyczne sprawdzenie można byłoby
wykonać również po wywołaniu metody ustawElement. Wykonanie kodu z listingu 4.3
spowoduje oczywiście wyświetlenie napisu Nieprawidłowy indeks tablicy... (ry-
sunek 4.2).
1
Nie jest natomiast badana dopuszczalna wartość minimalna. Będzie to ćwiczenie do samodzielnego
wykonania.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4. Wyjątki 169
Rysunek 4.2.
Efekt wykonania
programu z listingu 4.3
Wyjątki w Javie
Wyjątek (ang. exception) jest to byt programistyczny, który powstaje w sytuacji wystą-
pienia błędu. Z powstaniem wyjątku mieliśmy już do czynienia w rozdziale 2., w lek-
cji 11. Był to wyjątek spowodowany przekroczeniem dopuszczalnego zakresu tablicy.
Zobaczmy go raz jeszcze: na listingu 4.4 została zaprezentowana odpowiednio spre-
parowana klasa Main.
Listing 4.4.
public class Main {
public static void main (String args[]) {
int tab[] = new int[10];
tab[10] = 100;
}
}
Deklarujemy tablicę liczb typu int o nazwie tab oraz próbujemy przypisać elementowi
o indeksie 10 (czyli wykraczającym poza zakres tablicy) wartość 100. Jeśli skompilu-
jemy i uruchomimy taki program, na ekranie pojawi się obraz podobny do tego z rysun-
ku 4.1 (tym razem w komunikacie wskazany będzie tylko jeden plik źródłowy — Ma-
in.java). Został tu wygenerowany wyjątek o nazwie ArrayIndexOutOfBoundsException,
oznaczający, że indeks tablicy znajduje się poza dopuszczalnymi granicami.
2
Sama technika obsługi sytuacji wyjątkowych sięga jednak lat sześćdziesiątych ubiegłego stulecia
(czyli wieku XX).
b4ac40784ca6e05cededff5eda5a930f
b
170 Java. Praktyczny kurs
Listing 4.5.
public class Main {
public static void main (String args[]) {
int tab[] = new int[10];
try{
tab[10] = 100;
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Nieprawidłowy indeks tablicy!");
}
}
}
Jak widać, wszystko odbywa się tu zgodnie z wcześniejszym ogólnym opisem. W bloku
try znalazła się instrukcja tab[10] = 100, która — jak już wiadomo — spowoduje wyge-
nerowanie wyjątku. W nawiasie okrągłym występującym po instrukcji catch został wy-
mieniony rodzaj wyjątku, który będzie wygenerowany: ArrayIndexOutOfBoundsException,
oraz jego identyfikator: e. Identyfikator to nazwa3, która pozwala na wykonywanie
operacji związanych z wyjątkiem, tym jednak zajmiemy się w kolejnej lekcji. W bloku
po catch znajduje się instrukcja System.out.println wyświetlająca odpowiedni ko-
munikat na ekranie. Tym razem po uruchomieniu kodu pojawi się widok podobny do
zaprezentowanego na rysunku 4.2.
Blok try…catch nie musi jednak obejmować tylko jednej instrukcji ani też tylko instrukcji
mogących wygenerować wyjątek. Powróćmy na chwilę do listingu 2.39. Blok try
mógłby w tym wypadku objąć wszystkie trzy instrukcje, a klasa Main miałaby wtedy po-
stać jak na listingu 4.6.
Listing 4.6.
class Main {
public static void main (String args[]) {
try{
int tab[] = new int[10];
tab[10] = 5;
System.out.println("Dziesiąty element tablicy ma wartość: " + tab[10]);
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Nieprawidłowy indeks tablicy!");
3
Dokładniej, jest to nazwa zmiennej obiektowej (referencyjnej), co zostanie bliżej wyjaśnione w lekcji 21.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4. Wyjątki 171
}
}
}
Nie trzeba również obejmować blokiem try instrukcji bezpośrednio generujących wyją-
tek, tak jak miało to miejsce w dotychczas przedstawionych przykładach. Wyjątek
wygenerowany przez obiekt klasy Y może być bowiem przechwytywany w klasie X,
która korzysta z obiektów klasy Y. Sprawdźmy to na przykładzie klas z listingu 4.1.
Klasa Tablica pozostanie bez zmian, natomiast klasę Main zmodyfikujemy tak, aby
miała wygląd zaprezentowany na listingu 4.7.
Listing 4.7.
public class Main {
public static void main (String args[]) {
Tablica tablica = new Tablica();
try{
tablica.ustawElement(5, 10);
int liczba = tablica.pobierzElement(10);
System.out.println(liczba);
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Nieprawidłowy indeks tablicy!");
}
}
}
Spójrzmy: w bloku try zostają wykonane trzy instrukcje, z których jedna, int liczba =
tablica.pobierzElement(10), jest odpowiedzialna za wygenerowanie wyjątku. Ale wyją-
tek jest przecież generowany we wnętrzu metody pobierzElement klasy Tablica, a nie
w klasie Main! Zostanie on jednak przekazany klasie Main, jako że wywołuje ona me-
todę z klasy Tablica. Tym samym w klasie Main można zastosować blok try…catch.
Listing 4.8.
public class Wyjatki {
public void f() {
try{
g();
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Wyjątek: metoda f");
}
}
public void g() {
try{
h();
}
b4ac40784ca6e05cededff5eda5a930f
b
172 Java. Praktyczny kurs
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Wyjątek: metoda g");
}
}
public void h() {
int[] tab = new int[0];
try{
tab[0] = 1;
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Wyjątek: metoda h");
}
}
public static void main(String args[]) {
Wyjatki ex = new Wyjatki();
try{
ex.f();
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Wyjątek: metoda main");
}
}
}
Taką klasę da się skompilować bez żadnych problemów. Trzeba jednak zdawać sobie
sprawę z tego, jak taki kod będzie wykonywany. Pytanie brzmi: które bloki try zosta-
ną wykonane? Zasada jest następująca: zostanie wykonany blok znajdujący się najbli-
żej instrukcji powodującej wystąpienie wyjątku. Tak więc w przypadku przedstawionym
na listingu 4.8 będzie to jedynie blok obejmujący wywołanie metody tab[0] = 1;
w metodzie h. Jeśli jednak będziemy usuwać kolejne bloki try najpierw z metody h,
następnie g, f i ostatecznie z main, zobaczymy, że w rzeczywistości wykonywany jest
zawsze blok najbliższy miejsca wystąpienia błędu. Po usunięciu wszystkich instrukcji
try wyjątek nie zostanie obsłużony w klasie Wyjatki i obsłuży go maszyna wirtualna
Javy, co zaowocuje znanym nam już komunikatem na konsoli. Co jednak warte uwagi,
w tym wypadku zostanie wyświetlona cała hierarchia metod, w których propagowany
był wyjątek (rysunek 4.3).
Rysunek 4.3. Przy hierarchicznym wywołaniu metod po wystąpieniu wyjątku otrzymamy ich nazwy
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4. Wyjątki 173
Ćwiczenie 20.1.
Popraw kod klasy z listingu 4.2 tak, aby w metodach pobierzElement i ustawElement
sprawdzane było również to, czy przekazany indeks nie przekracza minimalnej dopusz-
czalnej wartości.
Ćwiczenie 20.2.
Zmień kod klasy Main z listingu 4.3 w taki sposób, by sprawdzane było również to,
czy wywołanie metody ustawElement zakończyło się sukcesem.
Ćwiczenie 20.3.
Zmień kod z listingu 4.2 tak, aby do wychwytywania błędów był wykorzystywany
mechanizm wyjątków, a nie instrukcja warunkowa if (przy czym zewnętrzna sygnalizacja
wystąpienia błędu nadal ma się odbywać poprzez pole wystapilBlad).
Ćwiczenie 20.4.
Napisz klasę Wyjatki, w której będzie się znajdowała metoda o nazwie a, wywoływana
z kolei przez metodę o nazwie b. W metodzie a wygeneruj wyjątek ArrayIndexOutOf
BoundsException. Napisz następnie klasę Main zawierającą metodę main, w której zosta-
nie utworzony obiekt klasy Wyjatki i zostaną wywołane metody a oraz b tego obiektu.
W metodzie main zastosuj bloki try…catch przechwytujące powstałe wyjątki.
Kompilacja takiego kodu przebiegnie bez problemu, jednak próba wykonania musi
skończyć się komunikatem o błędzie, widocznym na rysunku 4.4. Widać wyraźnie, że
tym razem został zgłoszony wyjątek ArithmeticException (wyjątek arytmetyczny).
b4ac40784ca6e05cededff5eda5a930f
b
174 Java. Praktyczny kurs
Dzięki wiedzy zdobytej w ramach lekcji 20. nikt nie powinien mieć żadnych problemów
z napisaniem kodu, który przechwyci taki wyjątek. Wykorzystajmy więc dobrze nam
znaną instrukcję try…catch w postaci:
try{
int liczba = 10 / 0;
}
catch(ArithmeticException e){
//instrukcje do wykonania, kiedy wystąpi wyjątek
}
Intuicja podpowiada zapewne, że rodzajów wyjątków może być bardzo, bardzo dużo.
I faktycznie tak jest, w klasach dostarczonych w JDK (w wersji SE) jest ich zdefinio-
wanych blisko 500. Aby sprawnie się nimi posługiwać, trzeba się dowiedzieć, czym tak
naprawdę są.
Listing 4.9.
public class Main {
public static void main (String args[]) {
try{
int liczba = 10 / 0;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4. Wyjątki 175
}
catch(ArithmeticException e){
System.out.println("Wystąpił wyjątek arytmetyczny...");
System.out.println("Komunikat systemowy:");
System.out.println(e.getMessage());
}
}
}
Rysunek 4.5.
Wyświetlenie
systemowego
komunikatu o błędzie
W pierwszej chwili może się to wydawać nieco dziwne, bo niby skąd instrukcja System.
out.println ma „wiedzieć”, co ma w takim wypadku wyświetlić. Jest to sytuacja
analogiczna do tej, z jaką mieliśmy do czynienia w przypadku typów prostych (por.
lekcja 5.). Skoro udawało się automatycznie przekształcać np. zmienną typu int na
ciąg znaków, uda się również przekształcić zmienną typu obiektowego. Tym problemem
zajmiemy się bliżej w rozdziale 5.
na:
System.out.println(e);
b4ac40784ca6e05cededff5eda5a930f
b
176 Java. Praktyczny kurs
Rysunek 4.6.
Pełniejszy komunikat
o typie wyjątku
Hierarchia wyjątków
Każdy wyjątek jest obiektem pewnej klasy. Klasy podlegają z kolei regułom dziedzicze-
nia, zgodnie z którymi powstaje ich hierarchia. Kiedy zatem pracuje się z wyjątkami,
trzeba tę kwestię wziąć pod uwagę. Wszystkie standardowe wyjątki, które można
przechwytywać w naszych aplikacjach za pomocą bloku try…catch, dziedziczą po
klasie Exception, dziedziczącej z kolei po Throwable oraz Object. Hierarchia klas dla
wyjątku ArithmeticException, który był wykorzystywany we wcześniejszych przy-
kładach, jest zaprezentowana na rysunku 4.74.
Rysunek 4.7.
Hierarchia klas
dla wyjątku
ArithmeticException
Wynika z tego kilka własności. Przede wszystkim, jeśli spodziewamy się, że dana
instrukcja może wygenerować wyjątek typu X, możemy zawsze przechwycić wyjątek
ogólniejszy, czyli taki, którego typem będzie jedna z klas nadrzędnych do X. Jest to
bardzo wygodna technika. Przykładowo po klasie RuntimeException dziedziczy bar-
dzo wiele klas wyjątków odpowiadających najróżniejszym błędom. Jedną z nich jest
ArithmeticException. Jeśli instrukcje, które są obejmowane blokiem try…catch, mogą
spowodować wiele różnych wyjątków, zamiast stosować wiele oddzielnych instrukcji
przechwytujących konkretne typy błędów, często lepiej jest użyć jednej przechwytu-
jącej wyjątek bardziej ogólny. Taka sytuacja została przedstawiona na listingu 4.10.
4
Kierunek strzałek na rysunku jest niezgodny z metodyką modelowania UML, za to bardziej zgodny
z intuicją.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4. Wyjątki 177
Listing 4.10.
public class Main {
public static void main (String args[]) {
try{
int liczba = 10 / 0;
}
catch(RuntimeException e){
System.out.println("Wystąpił wyjątek czasu wykonania...");
System.out.println("Komunikat systemowy:");
System.out.println(e);
}
}
}
Jest to znany nam już program, który generuje błąd polegający na próbie wykonania
niedozwolonego dzielenia przez zero. Tym razem jednak zamiast wyjątku klasy
ArithmeticException przechwytywany jest wyjątek klasy nadrzędnej — Runtime
Exception. Co więcej, nic nie stoi na przeszkodzie, aby przechwycić wyjątek jeszcze
bardziej ogólny, czyli klasy nadrzędnej do RuntimeException. Jak widać na rysunku 4.7,
byłaby to klasa Exception.
Przy tego typu konstrukcjach należy jednak pamiętać o hierarchii wyjątków, nie jest
bowiem obojętne, w jakiej kolejności będą one przechwytywane. Ogólna zasada jest taka,
że nie ma znaczenia kolejność, o ile wszystkie wyjątki są na jednym poziomie hierarchii.
Jeśli jednak przechwytuje się wyjątki z różnych poziomów, najpierw muszą to być te
bardziej szczegółowe, czyli stojące niżej w hierarchii, a dopiero po nich bardziej ogólne,
czyli stojące wyżej w hierarchii.
b4ac40784ca6e05cededff5eda5a930f
b
178 Java. Praktyczny kurs
Listing 4.11.
public class Main {
public static void main (String args[]) {
try{
int liczba = 10 / 0;
}
catch(RuntimeException e){
System.out.println(e);
}
catch(ArithmeticException e){
System.out.println(e);
}
}
}
Dzieje się tak dlatego, że (można powiedzieć) błąd bardziej ogólny zawiera już w so-
bie błąd bardziej szczegółowy. Jeśli zatem przechwyci się najpierw wyjątek Runtime-
Exception, to tak jakby przechwycić już wyjątki wszystkich klas dziedziczących po
RuntimeException. Dlatego kompilator protestuje.
Kiedy może się przydać rozwiązanie, w którym najpierw przechwytuje się wyjątek
szczegółowy, a dopiero potem ogólny? Otóż wtedy, kiedy chcemy w specyficzny sposób
zareagować na konkretny typ wyjątku, a wszystkie pozostałe wyjątki z danego poziomu
hierarchii obsłużyć w identyczny, standardowy sposób. Taki przykład jest przedstawiony
na listingu 4.12.
Listing 4.12.
public class Main {
public static void main (String args[]) {
Punkt punkt = null;
int liczba;
try{
liczba = 10 / 0;
punkt.x = liczba;
}
catch(ArithmeticException e){
System.out.println("Nieprawidłowa operacja arytmetyczna");
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4. Wyjątki 179
System.out.println(e);
}
catch(Exception e){
System.out.println("Błąd ogólny");
System.out.println(e);
}
}
}
Zostały zadeklarowane dwie zmienne: pierwsza typu Punkt o nazwie punkt oraz druga
typu int o nazwie liczba. Zmiennej punkt została przypisana wartość pusta null, nie
został zatem utworzony żaden obiekt klasy Punkt. W bloku try są wykonywane dwie
błędne instrukcje. Pierwsza z nich to znane z poprzednich przykładów dzielenie przez
zero. Druga instrukcja z bloku try to z kolei próba odwołania się do pola x nieistniejącego
obiektu klasy Punkt (przecież zmienna punkt zawiera wartość null). Ponieważ chcemy
w sposób niestandardowy zareagować na błąd arytmetyczny, najpierw przechwytujemy
błąd typu ArithmeticException i w przypadku kiedy wystąpi, wyświetlamy na ekranie
napis Nieprawidłowa operacja arytmetyczna. W drugim bloku catch przechwytujemy
wszystkie inne możliwe wyjątki, w tym także NullPointerException występujący podczas
próby wykonania operacji na zmiennej obiektowej, która zawiera wartość null.
Po uruchomieniu kodu z listingu 4.12 na ekranie pojawi się zgłoszenie tylko pierw-
szego błędu. Dzieje się tak dlatego, że po jego wystąpieniu blok try został przerwa-
ny, a sterowanie zostało przekazane blokowi catch. Jeśli więc w bloku try któraś z in-
strukcji spowoduje wygenerowanie wyjątku, dalsze instrukcje z tego bloku nie zostaną
wykonane. Nie będzie więc miała szansy zostać wykonana nieprawidłowa instrukcja
punkt.x = liczba;. Jeśli jednak usuniemy wcześniejsze dzielenie przez zero, przeko-
namy się, że i ten błąd zostanie przechwycony przez drugi blok catch, a na ekranie pojawi
się stosowny komunikat (rysunek 4.9).
Rysunek 4.9.
Odwołanie do pustej
zmiennej obiektowej
zostało wychwycone
przez drugi blok catch
b4ac40784ca6e05cededff5eda5a930f
b
180 Java. Praktyczny kurs
Listing 4.13.
public class Main {
public static void main (String args[]) {
Punkt punkt = null;
int liczba;
try{
liczba = 10 / 0;
punkt.x = liczba;
}
catch(ArithmeticException | NullPointerException e){
System.out.println("Błąd arytmetyczny lub niezainicjowana zmienna.");
System.out.println(e);
}
catch(Exception e){
System.out.println("Błąd ogólny");
System.out.println(e);
}
}
}
Zagnieżdżenie może być wielopoziomowe, czyli w już zagnieżdżonym bloku try można
umieścić kolejny taki blok. W praktyce tego rodzaju piętrowych konstrukcji zazwyczaj się
nie stosuje, bo zwykle nie ma takiej potrzeby. Z reguły nie korzysta się z bezpośred-
niego zagnieżdżenia więcej niż dwóch poziomów. Aby na praktycznym przykładzie
przyjrzeć się takiej dwupoziomowej konstrukcji, zmodyfikujmy kod z listingu 4.12.
Zamiast obejmować jednym blokiem try dwie instrukcje powodujące błąd, zastosu-
jemy zagnieżdżenie, tak jak jest to przedstawione na listingu 4.14.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4. Wyjątki 181
Listing 4.14.
public class Main {
public static void main (String args[]) {
Punkt punkt = null;
int liczba;
try{
try{
liczba = 10 / 0;
}
catch(ArithmeticException e){
System.out.println("Nieprawidłowa operacja arytmetyczna");
System.out.println("Przypisuję zmiennej liczba wartość 10");
liczba = 10;
}
punkt.x = liczba;
}
catch(Exception e){
System.out.println("Błąd ogólny");
System.out.println(e);
}
}
}
Podobnie jak w poprzednim przypadku, deklarujemy dwie zmienne: punkt typu Punkt
oraz liczba typu int. Zmiennej punkt przypisujemy wartość pustą null. W wewnętrznym
bloku try próbujemy wykonać nieprawidłowe dzielenie przez zero i przechwytujemy
wyjątek ArithmeticException. Jeśli on wystąpi, zmienna liczba otrzymuje domyślną
wartość równą 10, dzięki czemu można wykonać kolejną operację, czyli próbę przypi-
sania polu x obiektu wskazywanego przez punkt wartości zmiennej liczba. Rzecz jasna
przypisanie takie nie może zostać wykonane, gdyż zmienna punkt jest pusta, jest za-
tem generowany wyjątek NullPointerException, który zostaje przechwycony przez
zewnętrzny blok try. Widać więc, że zagnieżdżanie bloków try może być przydatne,
choć warto zauważyć, że identyczny efekt można osiągnąć, korzystając z niezagnież-
dżonej postaci instrukcji try…catch (ćwiczenie 21.3).
Ćwiczenie 21.1.
Popraw kod z listingu 4.11 tak, aby przechwytywanie wyjątków odbywało się w pra-
widłowej kolejności.
Ćwiczenie 21.2.
Zmodyfikuj kod z listingu 4.12 tak, by zgłoszone zostały oba typy błędów: Arithmetic
Exception oraz NullPointerException.
b4ac40784ca6e05cededff5eda5a930f
b
182 Java. Praktyczny kurs
Ćwiczenie 21.3.
Zmodyfikuj kod z listingu 4.14 w taki sposób, aby usunąć zagnieżdżenie bloków
try…catch, ale by nie zmieniać efektów działania programu.
Ćwiczenie 21.4.
Napisz kod klasy TablicaPunktow przechowującej do czterech obiektów typu Punkt oraz
testowy program ilustrujący różne sytuacje powstające przy korzystaniu z obiektu tej
klasy (zadbaj o przechwytywanie wyjątków). Klasa ma zawierać jedynie dwie meto-
dy: pierwszą — przyjmującą trzy argumenty typu int i ustawiającą współrzędne x i y
punktu przechowywanego pod zadanym indeksem tablicy, oraz drugą — zwracającą
referencję do punktu znajdującego się pod wskazanym indeksem. Program ma odpo-
wiednio reagować, gdy następuje próba użycia nieistniejącego indeksu tablicy, a także
wtedy, gdy następuje próba zmiany współrzędnych nieistniejącego punktu (punkt powi-
nien zostać wówczas utworzony).
Ćwiczenie 21.5.
Jeśli w rozwiązaniu ćwiczenia 21.4 użyłeś instrukcji if, rozwiąż je, nie stosując tej in-
strukcji. W przeciwnym przypadku rozwiąż zadanie, korzystając z instrukcji if. Do-
datkowo do klasy TablicaPunktow dopisz metodę usuwającą punkt znajdujący się pod
wskazanym indeksem tablicy.
Zgłaszanie wyjątku
Wiadomo już, że wyjątki są obiektami. Zgłoszenie5 własnego wyjątku będzie polegało na
utworzeniu nowego obiektu klasy opisującej wyjątek oraz użyciu instrukcji throw.
Dokładniej: za pomocą instrukcji new należy utworzyć nowy obiekt klasy, która pośrednio
lub bezpośrednio dziedziczy po klasie Throwable. W najbardziej ogólnym przypadku bę-
dzie to klasa Exception. Tak utworzony obiekt musi stać się parametrem instrukcji throw.
Jeśli zatem gdziekolwiek w pisanym kodzie ma być zgłoszony wyjątek ogólny, wy-
starczy napisać:
throw new Exception();
5
Stosuje się również termin „wyrzucenie wyjątku”, od ang. throw — rzucać, wyrzucać.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4. Wyjątki 183
Jeśli taki wyjątek zostanie obsłużony przez znajdującą się w danym bloku (danej me-
todzie) instrukcję try…catch, nie trzeba robić nic więcej. Jeśli jednak nie zostanie obsłużo-
ny (i nie jest wyjątkiem klasy RuntimeException lub pochodnej od niej), w specyfikacji
metody należy zaznaczyć, że może ona taki wyjątek zgłaszać (rzucać, wyrzucać). Ro-
bi się to za pomocą instrukcji throws, w ogólnej postaci:
specyfikator_dostępu [static] [final] typ_zwracany nazwa_metody(argumenty)
throws KlasaWyjątku1, KlasaWyjątku2, ... , KlasaWyjątkuN {
//treść metody
}
Zobaczmy, jak to wygląda w praktyce. Załóżmy, że mamy klasę Main, a w niej metodę
main. Jedynym zadaniem tej metody będzie zgłoszenie wyjątku typu Exception. Taka
klasa jest widoczna na listingu 4.15.
Listing 4.15.
public class Main {
public static void main (String args[]) throws Exception {
throw new Exception();
}
}
W deklaracji metody main poprzez użycie słów throws Exception zostało zaznaczone, że
może ona generować wyjątek klasy Exception. W ciele metody main została natomiast
wykorzystana instrukcja throw, która jako argument otrzymała nowy obiekt klasy
Exception. Po uruchomieniu opisanego programu na ekranie pojawi się widok zaprezen-
towany na rysunku 4.10. Jest to najlepszy dowód, że rzeczywiście udało się zgłosić
wyjątek.
Rysunek 4.10.
Zgłoszenie wyjątku
klasy Exception
Utworzenie obiektu wyjątku nie musi mieć miejsca bezpośrednio w instrukcji throw,
można go utworzyć wcześniej, przypisać zmiennej obiektowej (referencyjnej) i dopiero
tej zmiennej użyć jako parametru dla throw. Zamiast więc pisać:
throw new Exception();
b4ac40784ca6e05cededff5eda5a930f
b
184 Java. Praktyczny kurs
lub:
Exception exception = new Exception("komunikat");
throw exception;
Listing 4.16.
public class Dzielenie {
public double podziel(int liczba1, int liczba2) {
if(liczba2 == 0)
throw new ArithmeticException("Dzielenie przez zero: " + liczba1 + "/" +
liczba2);
return liczba1 / liczba2;
}
public static void main(String args[]) {
Dzielenie obj = new Dzielenie();
double wynik = obj.podziel(20, 10);
System.out.println("Wynik pierwszego dzielenia: " + wynik);
wynik = obj.podziel (20, 0);
System.out.println("Wynik drugiego dzielenia: " + wynik);
}
}
W klasie Dzielenie jest zdefiniowana metoda podziel, która przyjmuje dwa argumenty
typu int. Ma ona zwracać wynik dzielenia wartości przekazanej w argumencie liczba1
przez wartość przekazaną w argumencie liczba2. Jest zatem jasne, że liczba2 nie
może mieć wartości 0. Sprawdzamy to, wykorzystując instrukcję warunkową if. Jeśli
okaże się, że liczba2 ma wartość 0, za pomocą instrukcji throw zgłaszamy nowy wy-
jątek klasy ArithmeticException. W konstruktorze klasy przekazujemy komunikat in-
formujący o dzieleniu przez zero. Podajemy w nim wartości argumentów metody po-
dziel, tak by łatwo można było stwierdzić, jakie parametry spowodowały błąd.
W celu przetestowania działania metody podziel w klasie Dzielenie pojawiła się rów-
nież metoda main. Tworzymy w niej nowy obiekt klasy Dzielenie i odniesienie do nie-
go przypisujemy zmiennej o nazwie obj. Następnie dwukrotnie wywołujemy metodę
podziel, raz przekazując jej argumenty równe 20 i 10, drugi raz równe 20 i 0. Można
się spodziewać, że w drugim przypadku program zgłosi wyjątek ArithmeticException
ze zdefiniowanym przez nas komunikatem. Program zachowa się właśnie tak, co jest
widoczne na rysunku 4.11.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4. Wyjątki 185
Listing 4.17.
public class Main {
public static void main (String args[]) {
try{
int liczba = 10 / 0;
}
catch(ArithmeticException e){
System.out.println("Tu wyjątek został przechwycony.");
throw e;
}
}
}
W bloku try jest wykonywana niedozwolona instrukcja dzielenia przez zero. W bloku
catch najpierw wyświetlamy na ekranie informację o przechwyceniu wyjątku, a następnie
za pomocą instrukcji throw ponownie wyrzucamy (zgłaszamy) przechwycony już wyją-
tek. Ponieważ w programie nie ma innego bloku try…catch, który mógłby przechwycić
ten wyjątek, zostanie on obsłużony standardowo przez maszynę wirtualną. Dlatego też na
ekranie pojawi się widok zaprezentowany na rysunku 4.12.
b4ac40784ca6e05cededff5eda5a930f
b
186 Java. Praktyczny kurs
Listing 4.18.
public class Main {
public static void main (String args[]) {
//tu dowolne instrukcje
try{
//tu dowolne instrukcje
try{
int liczba = 10 / 0;
}
catch(ArithmeticException e){
System.out.println("Tu wyjątek został przechwycony pierwszy raz.");
throw e;
}
}
catch(ArithmeticException e){
System.out.println("Tu wyjątek został przechwycony drugi raz.");
throw e;
}
}
}
Mamy tu dwa zagnieżdżone bloki try. W bloku wewnętrznym zostaje wykonana niepra-
widłowa instrukcja dzielenia przez zero. Zostaje ona w tym bloku przechwycona, a na
ekranie wyświetlany jest komunikat o pierwszym przechwyceniu wyjątku. Następnie
wyjątek jest ponownie zgłaszany. W bloku zewnętrznym następuje drugie przechwy-
cenie, wyświetlenie drugiego komunikatu oraz kolejne zgłoszenie wyjątku. Ponieważ
nie istnieje trzeci blok try…catch, ostatecznie wyjątek jest obsługiwany przez maszynę
wirtualną. Dlatego po uruchomieniu przykładu wynik będzie taki, jak zaprezentowano
na rysunku 4.13.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4. Wyjątki 187
b4ac40784ca6e05cededff5eda5a930f
b
188 Java. Praktyczny kurs
Listing 4.20.
public class Main {
public static void main (String args[]) throws GeneralException {
throw new GeneralException();
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4. Wyjątki 189
Rysunek 4.15.
Zgłaszanie własnych
wyjątków
Listing 4.21.
public class DivideByZeroException extends ArithmeticException {
}
Możemy teraz zmodyfikować program z listingu 4.16 tak, aby po wykryciu dzielenia
przez zero był zgłaszany wyjątek nowego typu, czyli DivideByZeroException. Taka
klasa została przedstawiona na listingu 4.22.
Listing 4.22.
public class Dzielenie {
public double podziel(int liczba1, int liczba2) {
if(liczba2 == 0)
throw new DivideByZeroException();
return liczba1 / liczba2;
}
public static void main(String args[]) {
Dzielenie obj = new Dzielenie();
double wynik = obj.podziel(20, 10);
System.out.println("Wynik pierwszego dzielenia: " + wynik);
wynik = obj.podziel (20, 0);
System.out.println("Wynik drugiego dzielenia: " + wynik);
}
}
W stosunku do kodu z listingu 4.16 różnice nie są duże, ograniczają się do zmiany typu
zgłaszanego wyjątku w metodzie podziel. Zadaniem tej metody nadal jest zwrócenie
wyniku dzielenia argumentu liczba1 przez argument liczba2. W metodzie main dwukrotnie
b4ac40784ca6e05cededff5eda5a930f
b
190 Java. Praktyczny kurs
wywołujemy metodę podziel, pierwszy raz z prawidłowymi danymi i drugi raz z tymi,
które spowodują wygenerowanie wyjątku. Efekt działania przedstawionego kodu jest
widoczny na rysunku 4.16.
Rysunek 4.16.
Zgłoszenie wyjątku
DivideByZeroException
Zwróćmy jednak uwagę, że pierwotnie (listing 4.16) przy zgłaszaniu wyjątku argu-
mentem konstruktora był komunikat tekstowy (czyli wartość typu String). Tym razem nie
można było zastosować tego typu konstrukcji, gdyż klasa DivideByZeroException nie
ma konstruktora przyjmującego ciąg znaków jako argument, ale jedynie bezargumentowy
konstruktor domyślny. Aby zatem możliwe było przekazywanie własnych komunika-
tów, należy dopisać do klasy DivideByZeroException odpowiedni konstruktor (przyj-
mujący argument typu String). Będzie on miał postać widoczną na listingu 4.23.
Listing 4.23.
public class DivideByZeroException extends ArithmeticException {
public DivideByZeroException(String str) {
super(str);
}
}
Sekcja finally
Do bloku try można dołączyć sekcję finally, która będzie wykonywana zawsze, nie-
zależnie od tego, co będzie działo się w bloku try. Schemat takiej konstrukcji wygląda
następująco:
try{
//instrukcje mogące spowodować wyjątek
}
catch(TypWyjątku identyfikatorWyjątku){
//instrukcje sekcji catch
}
finally{
//instrukcje sekcji finally
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4. Wyjątki 191
Listing 4.24.
public class Dzielenie {
public double podziel(int liczba1, int liczba2) {
if(liczba2 == 0)
throw new DivideByZeroException("Dzielenie przez zero: " + liczba1 + "/" + liczba2);
return liczba1 / liczba2;
}
public static void main(String args[]) {
Dzielenie obj = new Dzielenie();
double wynik;
try{
wynik = obj.podziel(20, 10);
}
catch(DivideByZeroException e){
System.out.println("Przechwycenie wyjątku 1");
}
finally{
System.out.println("Sekcja finally 1");
}
try{
wynik = obj.podziel(20, 0);
}
catch(DivideByZeroException e){
System.out.println("Przechwycenie wyjątku 2");
}
finally{
System.out.println("Sekcja finally 2");
}
}
}
Jest to znana nam już klasa Dzielenie z metodą podziel wykonującą dzielenie prze-
kazanych jej argumentów. Tym razem metoda podziel pozostała bez zmian w stosun-
ku do wersji z listingu 4.22, czyli zgłasza błąd DivideByZeroException. Zmodyfiko-
wana została natomiast metoda main. Oba wywołania metody zostały ujęte w bloki
try…catch…finally. Pierwsze wywołanie nie powoduje powstania wyjątku, nie jest
więc wykonywany pierwszy blok catch, ale jest wykonywany pierwszy blok finally.
Tym samym na ekranie pojawia się napis Sekcja finally 1.
b4ac40784ca6e05cededff5eda5a930f
b
192 Java. Praktyczny kurs
Rysunek 4.17.
Blok finally jest
wykonywany niezależnie
od tego, czy pojawi się
wyjątek, czy nie
Sekcji finally można użyć również w przypadku instrukcji, które nie powodują wy-
generowania wyjątku. Stosuje się wtedy instrukcję try…finally w postaci:
try{
//instrukcje
}
finally{
//instrukcje
}
Działanie jest takie samo jak w przypadku bloku try…catch…finally, to znaczy kod
z bloku finally zostanie wykonany zawsze, niezależnie od tego, jakie instrukcje znajdą
się w bloku try. Przykładowo: nawet jeśli w bloku try znajdzie się instrukcja return
lub zostanie wygenerowany wyjątek, blok finally i tak zostanie wykonany. Zostało
to zobrazowane w przykładzie widocznym na listingu 4.25.
Listing 4.25.
public class Wyjatki {
public int f1() {
try{
return 0;
}
finally{
System.out.println("Sekcja finally f1");
}
}
public void f2() {
try{
int liczba = 10 / 0;
}
finally{
System.out.println("Sekcja finally f2");
}
}
public static void main(String args[]) {
Wyjatki wyjatki = new Wyjatki ();
wyjatki.f1();
wyjatki.f2();
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4. Wyjątki 193
rwanie wykonywania funkcji nie jest w stanie zapobiec wykonaniu instrukcji z bloku
finally. Konstrukcja metody f2 jest bardzo podobna. Różnica jest taka, że w bloku
try jest wykonywane dzielenie przez zero, czyli powstaje wyjątek. Wyjątek nie jest
jednak przechwytywany w tej metodzie, a zatem zostanie przekazany do metody main,
a następnie obsłużony przez maszynę wirtualną. Również w tym przypadku (mimo wyge-
nerowania wyjątku) zostanie wykonany blok finally. Zatem po skompilowaniu i uru-
chomieniu programu zobaczymy widok przedstawiony na rysunku 4.18.
Ćwiczenie 22.1.
Napisz klasę, w której zostaną zadeklarowane metody f i main. W metodzie f napisz do-
wolną instrukcję generującą wyjątek NullPointerException. W main wywołaj metodę f
i przechwyć wyjątek za pomocą bloku try…catch.
Ćwiczenie 22.2.
Zmodyfikuj kod z listingu 4.18 tak, aby generowany, przechwytywany i ponownie zgła-
szany był wyjątek ArrayIndexOutOfBoundsException.
Ćwiczenie 22.3.
Napisz klasę o takim układzie metod jak w przypadku klasy Wyjatki z listingu 4.19.
W najbardziej zagnieżdżonej metodzie h wygeneruj wyjątek ArithmeticException.
Przechwyć go w metodzie g i zgłoś wyjątek klasy nadrzędnej do ArithmeticException,
czyli RuntimeException. Wyjątek ten przechwyć w metodzie f i zgłoś wyjątek nadrzędny
do RuntimeException, czyli Exception. Ten ostatni wyjątek przechwyć w klasie main.
Ćwiczenie 22.4.
Napisz klasę wyjątku o nazwie NegativeValueException oraz klasę Main, która będzie
z niego korzystać. W klasie Main napisz metodę o nazwie f przyjmującą dwa argumenty
typu int. Metoda f powinna zwracać wartość będącą wynikiem odejmowania pierw-
szego argumentu od drugiego. Gdyby jednak wynik ten był ujemny, powinien zostać
zgłoszony wyjątek NegativeValueException. Dopisz metodę main, która przetestuje
działanie metody f.
b4ac40784ca6e05cededff5eda5a930f
b
194 Java. Praktyczny kurs
Ćwiczenie 22.5.
Na podstawie przykładu z listingu 4.1 napisz kod klasy Tablica, w której po wykryciu
nieprawidłowego indeksu (zarówno przy zapisie, jak i odczycie danych) będzie genero-
wany wyjątek nowej klasy o nazwie InvalidIndexException (lub podobnej). Przetestuj
działanie nowego mechanizmu klasy Tablica, zmieniając odpowiednio klasę Main.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.
Programowanie
obiektowe. Część II
W rozdziale 3. zostały omówione podstawy programowania obiektowego. Była w nim
mowa o podstawowych technikach i zasadach niezbędnych do sprawnego programowania
w języku Java. Materiał przedstawiony w tym rozdziale pozwoli na znaczne poszerzenie
zdobytej dotąd wiedzy. Treść tego rozdziału jest podzielona na trzy główne tematy:
polimorfizm, interfejsy oraz klasy wewnętrzne. Obejmują one zagadnienia bardziej
zaawansowane niż te, które zostały do tej pory przedstawione, niemniej nie można ich
pominąć. Każdy, kto myśli poważnie o programowaniu w Javie, powinien się z nimi
zapoznać. W szczególności dotyczy to kwestii polimorfizmu będącego jednym z filarów
programowania obiektowego, a także interfejsów, bez których nie da się sprawnie pra-
cować z klasami zawartymi w JDK, o czym będzie mowa później, w rozdziałach 6. i 8.
Polimorfizm
Lekcja 23. Konwersje typów i rzutowanie obiektów
Lekcja 23. jest poświęcona konwersji typów oraz rzutowaniu obiektów. Będzie się można
dzięki niej dowiedzieć, czy da się przypisać zmiennej typu int wartość double oraz
w jaki sposób są wykonywane domyślne zmiany typów i jak je kontrolować. Zostanie opi-
sana także technika pozwalająca na przypisanie do zmiennej referencyjnej o określonym
typie obiektu klasy nadrzędnej lub potomnej w stosunku do tego typu. Znajdzie się tu
również wyjaśnienie, jak to się dzieje, że jako argumentu instrukcji System.out.println
można użyć dowolnego typu obiektowego.
b4ac40784ca6e05cededff5eda5a930f
b
196 Java. Praktyczny kurs
Okazało się wtedy, że wynik takiego dzielenia zostanie zaokrąglony w dół, czyli
zmiennej liczba zostanie w tym przypadku przypisana wartość 4. Czas wyjaśnić to
dokładniej. Otóż w takiej sytuacji zostanie wykonana automatyczna konwersja typów
danych. Wynik, którym jest liczba zmiennoprzecinkowa typu double (9/2 = 4,5), zo-
stanie skonwertowany na typ int. W efekcie tej konwersji zostanie utracona część
ułamkowa i dlatego właśnie zmienna liczba otrzyma wartość 4. A zatem najważniej-
sza operacja to konwersja typów danych, a zaokrąglenie jest jedynie efektem ubocznym
tej konwersji.
Przykładowo: aby dokonać jawnej konwersji wyniku dzielenia 9 / 2, który jest war-
tością typu double, na typ int, należy zastosować instrukcję:
int liczba = (int) 9 / 2;
Można też dokonać jawnej konwersji zmiennej typu double na typ int, np.:
double liczba1 = 10.5;
int liczba2 = (int) liczba1;
Zwróćmy uwagę, że przy tego typu przypisaniu jawna konwersja jest niezbędna — jeśli
nie zostanie dokonana, kompilator nie dopuści do kompilacji programu. Aby to spraw-
dzić, wystarczy użyć programu przedstawionego na listingu 5.1. Widać na nim przy-
kładową klasę Main, w której wartość zmiennej typu double jest przypisywana zmiennej
typu int. Kompilator wygeneruje w takim przypadku błąd widoczny na rysunku 5.1.
Listing 5.1.
public class Main {
public static void main (String args[]) {
double liczba1 = 0;
int liczba2 = liczba1;
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 197
Mówiąc ogólnie, jeśli zmiennej, która reprezentuje węższy zakres wartości (np. int),
przypisuje się zmienną mogącą przedstawiać szerszy zakres wartości (np. double), trzeba
zawsze dokonać jawnej konwersji typu (oczywiście o ile taka konwersja jest możliwa).
Należy zdawać sobie sprawę, że w sytuacji odwrotnej konwersja typów również wy-
stępuje, choć najczęściej jest po prostu niezauważalna. Jeśli spróbujemy przypisać
zmiennej typu double (reprezentującej szerszy zakres wartości) wartość całkowitą lub
wartość zmiennej typu int (reprezentującej węższy zakres wartości), konwersja również
zostanie wykonana, tyle że będzie w pełni automatyczna. Można więc spokojnie wykonać
instrukcje:
int liczba1 = 10;
double liczba2 = liczba1;
Wiadomo, że obiekt klasy potomnej zawiera w sobie wszystkie pola i metody klasy
bazowej. Można więc powiedzieć, że zawiera już w sobie obiekt tej klasy. W związku
z tym nie ma żadnych przeciwwskazań, aby w miejscu, gdzie powinien się znaleźć obiekt
klasy bazowej, umieścić obiekt klasy potomnej. Weźmy dla przykładu dobrze nam
znane z rozdziału 3. klasy Punkt i Punkt3D w postaci zaprezentowanej na listingu 5.2.
Listing 5.2.
public class Punkt {
public int x;
public int y;
}
b4ac40784ca6e05cededff5eda5a930f
b
198 Java. Praktyczny kurs
public int z;
}
Jeśli zadeklarujemy teraz zmienną typu Punkt, to będzie można przypisać jej nowy
obiekt klasy Punkt3D (dokładniej: odniesienie do nowego obiektu klasy Punkt3D). Po-
prawne zatem będą instrukcje:
Punkt punkt;
punkt = new Punkt3D();
Oczywiście odwołując się teraz w standardowy sposób do metod i pól obiektu wskazy-
wanego przez zmienną punkt, można uzyskać dostęp jedynie do metod zdefiniowa-
nych w klasie Punkt. W związku z tym niepoprawne będzie np. odwołanie:
punkt.z = 10;
Zostało to zobrazowane w kodzie z listingu 5.3. Próba jego kompilacji zakończy się
błędem widocznym na rysunku 5.2.
Listing 5.3.
public class Main {
public static void main (String args[]) {
Punkt punkt;
punkt = new Punkt3D();
punkt.z = 10;
}
}
Nie powinno to dziwić. Dla kompilatora typem zmiennej punkt jest Punkt (potocznie
mówimy, że „zmienna jest klasy Punkt”), a w klasie Punkt nie ma pola z, dlatego też
wyświetlane są komunikaty o błędzie.
W rzeczywistości instrukcja:
punkt = new Punkt3D();
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 199
Przypomina to konwersje typów prostych, jednak znaczenie tego zapisu jest nieco inne.
Zostało tu wykonane rzutowanie wskazania do obiektu klasy Punkt3D na klasę Punkt.
Jest to informacja dla kompilatora: traktuj zmienną punkt wskazującą obiekt klasy
Punkt3D tak, jakby wskazywała obiekt klasy Punkt, a w uproszczeniu: traktuj obiekt klasy
Punkt3D tak, jak gdyby był on klasy Punkt. Obiekt Punkt3D nie zmienia się jednak ani
nie traci żadnych informacji, jest po prostu inaczej traktowany.
Jest to też doskonały dowód, że dokonuje się tu rzutowania typów, a nie konwersji.
Warto przypomnieć, że w przypadku typów prostych konwersja typu bardziej ogólnego na
bardziej szczegółowy powodowała utratę części informacji. Przykładowo wartość 4,5 ty-
pu double po konwersji na typ int zmieniała się na 4. W takiej sytuacji część ułamkowa
zostanie bezpowrotnie utracona i nawet powtórna konwersja na typ double nie przywróci
poprzedniej wartości. Zobrazowano to w przykładzie widocznym na listingu 5.4.
Listing 5.4.
public class Main {
public static void main (String args[]) {
double x = 4.5;
int y = (int) x;
double z = (double) y;
System.out.println("x = " + x);
System.out.println("y = " + y);
System.out.println("z = " + z);
}
}
Deklarujemy zmienną x typu double i przypisujemy jej wartość 4,5. Następnie deklaruje-
my zmienną y typu int oraz przypisujemy jej wartość konwersji zmiennej x na typ int
(int y = (int) x;). W kroku trzecim deklarujemy zmienną z typu double, której
przypisujemy wynik konwersji zmiennej y na typ double. Na zakończenie wyświetlamy
wartości wszystkich trzech zmiennych na ekranie (rysunek 5.3). Widać wyraźnie, że
nie da się odzyskać informacji utraconej przez konwersję typu double na typ int.
Rysunek 5.3.
Utrata informacji
w wyniku konwersji
typów prostych
b4ac40784ca6e05cededff5eda5a930f
b
200 Java. Praktyczny kurs
Listing 5.5.
public class Main {
public static void main (String args[]) {
Punkt3D punkt3D1 = new Punkt3D();
punkt3D1.x = 10;
punkt3D1.y = 20;
punkt3D1.z = 30;
System.out.println("punkt3D1");
System.out.println("x = " + punkt3D1.x);
System.out.println("y = " + punkt3D1.y);
System.out.println("z = " + punkt3D1.z);
System.out.println("");
System.out.println("punkt");
System.out.println("x = " + punkt.x);
System.out.println("y = " + punkt.y);
System.out.println("");
System.out.println("punkt3D2");
System.out.println("x = " + punkt3D2.x);
System.out.println("y = " + punkt3D2.y);
System.out.println("z = " + punkt3D2.z);
}
}
Jest już zatem jasne, że rzutowanie nie zmienia stanu obiektu. W przykładzie utwo-
rzony został tylko JEDEN obiekt klasy Punkt3D. Żadna z operacji nie zmieniła typu
tego obiektu, nie odbyły się żadne konwersje, które mogłyby doprowadzić do utraty
części danych. Powstały natomiast trzy zmienne, przez które spoglądaliśmy na obiekt.
Zmienne punkt3D1 i punkt3D2 traktowały go jako obiekt klasy Punkt3D, a zmienna punkt
jako obiekt klasy Punkt. Schematycznie przedstawiono tę sytuację na rysunku 5.5.
Można zatem powiedzieć, że rzutowanie pozwala spojrzeć na dany obiekt z innej,
szerszej lub węższej, perspektywy.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 201
Rysunek 5.4.
Ilustracja rzutowania
typów obiektowych
Rysunek 5.5.
Schematyczne
przedstawienie
zmiennych
i obiektu
z listingu 5.5
Przykład z listingu 5.5 pokazał również, że rzutowanie obiektów jest możliwe w obie
strony, to znaczy obiekt klasy bazowej można rzutować na obiekt klasy potomnej, a obiekt
klasy potomnej na obiekt klasy bazowej. Przypadek drugi jest oczywisty, wiadomo
już, że obiekt klasy potomnej zawiera w sobie obiekt klasy bazowej. Sytuacja odwrotna
jest jednak bardziej skomplikowana. Jak np. potraktować poniższy fragment kodu?
Punkt3D punkt3D = (Punkt3D) new Punkt();
punkt3D.z = 10;
b4ac40784ca6e05cededff5eda5a930f
b
202 Java. Praktyczny kurs
Zatem klasa Object jest praklasą, z której wywodzą się wszystkie inne klasy w Javie.
Oznacza to, że każda klasa dziedziczy wszystkie metody i pola klasy Object. Jedną
z takich metod jest toString. Często programista nie zdaje sobie nawet sprawy, że jest ona
wykonywana. Przypomnijmy sobie np. sposób, w jaki wyświetlany był systemowy
komunikat o typie wyjątku w lekcjach z rozdziału 4. Stosowaliśmy m.in. następującą
konstrukcję:
try{
//instrukcje mogące spowodować wyjątek ArithmeticException
}
catch(ArithmeticException e){
System.out.println(e);
}
Zastanawialiśmy się wtedy, jak to się dzieje, że jako argument metody println1 można
przekazać obiekt klasy wyjątku, w powyższym przypadku obiekt klasy Arithmetic
Exception. Po sprawdzeniu w dokumentacji JDK okazuje się, że istnieje wiele prze-
ciążonych wersji metody println, żadna jednak nie przyjmuje argumentu typu
ArithmeticException. Nie istnieje też domyślna konwersja żadnego typu obiektowego
na typ String.
Oczywiście metoda toString zdefiniowana w klasie Object nie jest w stanie dostarczyć
komunikatu o typie zgłoszonego wyjątku ArithmeticException. Jednak komunikaty z klas
potomnych są uzyskiwane dzięki przesłanianiu metody toString w tych klasach. Jak
to będzie wyglądało w konkretnym przykładzie, widać na listingu 5.6.
Listing 5.6.
public class MojaKlasa {
public String toString() {
return "Jestem obiektem klasy MojaKlasa";
}
public static void main(String args[]) {
MojaKlasa mk = new MojaKlasa();
System.out.println(mk);
}
}
1
Po wyjaśnieniach z rozdziału 3. łatwo się domyślić, że instrukcja System.out.println to nic innego
niż wywołanie metody println obiektu out zdefiniowanego w klasie System.
2
Formalnie rzecz biorąc, w postaci obiektu klasy String.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 203
Rysunek 5.6.
Efekt działania
przeciążonej metody
toString z klasy
MojaKlasa
Warto jednak zauważyć, że w tej chwili jest wykorzystywana inna wersja metody
println. W poprzednim wypadku została zastosowana wersja przyjmująca jako ar-
gument obiekt klasy Object, a tym razem ta metoda przyjmuje obiekt klasy String.
Można byłoby więc tę instrukcję rozbić na dwie następujące linie:
String komunikat = mk.toString();
System.out.println(komunikat);
Listing 5.7.
public class MojaKlasa {
public static void main(String args[]) {
MojaKlasa mk = new MojaKlasa();
System.out.println(mk);
}
}
Rysunek 5.7.
Efekt wywołania
domyślnej metody
toString pochodzącej
z klasy Object
Ćwiczenie 23.1.
Popraw kod z listingu 5.1 tak, aby jego kompilacja była możliwa.
b4ac40784ca6e05cededff5eda5a930f
b
204 Java. Praktyczny kurs
Ćwiczenie 23.2.
Napisz klasy Bazowa i Pochodna (dziedziczącą po Bazowa) oraz testową klasę Main. W klasie
Main utwórz obiekt klasy Pochodna i przypisz go zmiennej typu Bazowa o nazwie bazowa.
Następnie zadeklaruj dodatkową zmienną typu Pochodna i przypisz jej obiekt wskazywany
przez zmienną bazowa.
Ćwiczenie 23.3.
Zmodyfikuj kod z listingu 5.6 tak, aby metoda toString z klasy MojaKlasa wyświetlała
również komunikat zwracany przez klasę nadrzędną.
Ćwiczenie 23.4.
Zmodyfikuj kod klasy Punkt z listingu 3.1 z rozdziału 3. tak, by po użyciu obiektu tej
klasy jako argumentu metody println na ekranie były wyświetlane współrzędne punktu.
Dodatkowo spraw, aby ta klasa pakietowa stała się publiczna, a także by oba jej pola
były publiczne.
Ćwiczenie 23.5.
Napisz klasę Main, która przetestuje działanie metody toString klasy Punkt powstałej
w ćwiczeniu 23.4.
Taką konstrukcję można zapisać również przy użyciu dodatkowej zmiennej klasy Punkt:
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 205
Rysunek 5.8.
Zmienna punkt klasy
Punkt3D wskazuje
na obiekt klasy Punkt
Czy można wykonać instrukcję punkt3D.z = 10? Odpowiedź oczywiście musi brzmieć:
nie! Obiekt klasy Punkt nie ma pola o nazwie z, nie ma zatem możliwości takiego przypi-
sania. Czy uda się w związku z tym skompilować kod widoczny na listingu 5.8?
Odpowiedź brzmi… tak.
Listing 5.8.
public class Main {
public static void main(String args[]) {
Punkt punkt = new Punkt();
Punkt3D punkt3D = (Punkt3D) punkt;
punkt3D.z = 10;
}
}
b4ac40784ca6e05cededff5eda5a930f
b
206 Java. Praktyczny kurs
Rysunek 5.9. Próba odwołania się do nieistniejącego w klasie Punkt pola o nazwie z
Spróbujmy teraz odwołać się do jednego z istniejących w klasie Punkt pól x lub y,
czyli wykonać instrukcje:
Punkt punkt = new Punkt();
Punkt3D punkt3D = (Punkt3D) punkt;
punkt3D.x = 10;
Czy tym razem program zadziała? Otóż spotka nas tu niespodzianka: reakcja będzie
identyczna z tą, jaką widzieliśmy w poprzednim przypadku, zostanie wygenerowany
wyjątek ClassCastException (rysunek 5.9). Stanie się tak, mimo że obiekt, na który wska-
zuje zmienna punkt3D, zawiera pole x. To jednak nie ma znaczenia, gdyż podstawowym
problemem jest to, że sam obiekt jest innej klasy niż Punkt3D. Maszyna wirtualna w trakcie
wykonania programu dokonuje sprawdzenia zgodności klas. Wykonywanie operacji na
obiekcie okazuje się możliwe tylko wtedy, kiedy zmienna wskazująca na ten obiekt
jest klasy tego obiektu lub klasy nadrzędnej do klasy tego obiektu — nigdy odwrotnie. Je-
żeli opisywana zgodność nie następuje, w trakcie wykonania zostanie wygenerowany
błąd ClassCastException, tak jak miało to miejsce w ostatnich przykładach. Jest on gene-
rowany już przy próbie wykonania instrukcji:
Punkt3D punkt3D = (Punkt3D) punkt;
nic więc dziwnego, że nie sposób wykonać żadnej instrukcji modyfikującej pola obiektu,
niezależnie od tego, czy są one w nim zawarte, czy też nie.
Załóżmy, że zostały utworzone trzy klasy: MojaKlasa oraz dziedziczące po niej Moja
DrugaKlasa i MojaTrzeciaKlasa. We wszystkich zostanie zdefiniowana metoda toString,
która będzie wyświetlała nazwę danej klasy. Całość przyjmie postać widoczną na li-
stingu 5.9.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 207
Listing 5.9.
public class MojaKlasa {
public String toString() {
return "MojaKlasa";
}
}
Listing 5.10.
public class Main {
public static void main(String args[]) {
MojaKlasa mk1 = new MojaDrugaKlasa ();
MojaKlasa mk2 = new MojaTrzeciaKlasa ();
System.out.println(mk1.toString());
System.out.println(mk2.toString());
}
}
Tworzymy tu dwie zmienne typu (klasy) MojaKlasa. Pierwszej z nich przypisujemy re-
ferencję do nowego obiektu klasy MojaDrugaKlasa, drugiej — do obiektu klasy Moja
TrzeciaKlasa. Można tego dokonać, jako że obie klasy dziedziczą po MojaKlasa. Jest tu
wykorzystywane rzutowanie domyślne. Formalnie można by ten kod zapisać również jako:
MojaKlasa mk1 = (MojaKlasa) new MojaDrugaKlasa ();
MojaKlasa mk2 = (MojaKlasa) new MojaTrzeciaKlasa ();
b4ac40784ca6e05cededff5eda5a930f
b
208 Java. Praktyczny kurs
Rysunek 5.10.
Wynik polimorficznego
wywołania metody
toString z listingów 5.9
i 5.10
Wykonajmy jeszcze jeden przykład. Przy założeniu, że mamy klasę Shape, która jest
nadrzędna dla innych klas opisujących figury geometryczne, wyprowadzimy z niej
klasy Circle, Rectangle oraz Triangle. W każdej z nich umieścimy metodę draw. Choć
w praktyce metoda ta rysowałaby zapewne daną figurę na ekranie, ograniczmy się jedy-
nie do wyświetlenia nazwy. Taki zestaw klas jest widoczny na listingu 5.11.
Listing 5.11.
public class Shape {
public void draw() {
System.out.println("Shape");
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 209
Załóżmy teraz, że do napisania jest klasa Main, w której znajdzie się metoda drawShape
rysująca figury. To znaczy metoda taka miałaby przyjmować argument będący obiektem
danej klasy i wywoływać jego metodę draw. Bez wywołań polimorficznych niezbędne
byłoby napisanie wielu wersji przeciążonej metody drawShape. Jedna przyjmowałaby
argumenty klasy Triangle, inna Rectangle, jeszcze inna Circle. Co więcej, utworzenie
nowej klasy dziedziczącej po Shape wymagałoby dopisania kolejnej przeciążonej metody
drawShape w klasie Main. Tymczasem polimorfizm pozwala napisać tylko jedną meto-
dę drawShape w klasie Main i będzie ona pasować do każdej kolejnej klasy wyprowadzonej
z Shape. Zostało to zobrazowane w programie widocznym na listingu 5.12.
Listing 5.12.
public class Main {
public void drawShape(Shape shape) {
shape.draw();
}
public static void main(String args[]) {
Main main = new Main();
main.drawShape(circle);
main.drawShape(triangle);
main.drawShape(rectangle);
}
}
W klasie Main istnieje tylko jedna metoda drawShape, która jako argument przyjmuje
obiekt typu Shape. Będzie więc mogła również otrzymać jako argument obiekt dowolnej
klasy dziedziczącej po Shape. W samej metodzie następuje wywołanie metody draw
obiektu będącego argumentem. Będzie to oczywiście wywołanie polimorficzne, zatem
zostanie wywołana metoda wynikająca z rzeczywistej klasy obiektu (Triangle, Circle
lub Rectangle), a nie ta pochodząca z klasy Shape.
Dalsza część programu potwierdza takie właśnie zachowanie kodu. W metodzie main two-
rzymy bowiem obiekty klas Circle, Triangle oraz Rectangle i przekazujemy je jako
argumenty metodzie drawShape klasy Main. Wynik działania, zgodny z naszymi oczeki-
waniami, jest widoczny na rysunku 5.11.
Warto się teraz zastanowić, co by się stało, gdyby w jednej z klas dziedziczących po
Shape nie było zdefiniowanej metody draw. Jaki będzie wynik działania programu z li-
stingu 5.12, jeśli np. klasa Triangle z listingu 5.11 przyjmie pokazaną poniżej postać?
public class Triangle extends Shape {
}
b4ac40784ca6e05cededff5eda5a930f
b
210 Java. Praktyczny kurs
Rysunek 5.11.
Polimorficzne
wywołania metody
drawShape
Efekt jest widoczny na rysunku 5.12. Zamiast napisu Triangle pojawił się napis Shape. To
nie powinno dziwić, bo metoda draw z klasy Triangle przesłaniała metodę draw zdefi-
niowaną w klasie Shape. Kiedy z definicji klasy Triangle usunięta została metoda draw,
doszło do odsłonięcia metody draw odziedziczonej po klasie nadrzędnej, czyli Shape.
Została zatem wykonana metoda draw obiektu klasy Triangle, tak jak w poprzednim
przypadku, tyle że była to metoda odziedziczona po klasie bazowej i stąd też na ekranie
pojawił się napis Shape.
Rysunek 5.12.
Z klasy Triangle została
usunięta metoda draw
Metody prywatne
Przy wykorzystywaniu polimorficznych wywołań metod należy pamiętać, że mają
one miejsce, kiedy metoda X z klasy bazowej jest przesłaniana przez metodę X z klasy
potomnej. Problem może wystąpić w sytuacji, kiedy w klasie bazowej zostanie zdefi-
niowana metoda prywatna. Taka metoda nie może być bowiem przesłonięta. Jest to
oczywiste, bo przecież metody prywatne (lekcja 17.) są dostępne wyłącznie dla klasy,
w której zostały zdefiniowane. Łatwo tu jednak wpaść w pewną pułapkę. Otóż to, że
dana metoda została zadeklarowana w klasie bazowej jako prywatna, nie oznacza wcale,
że nie można zdefiniować metody o takiej samej nazwie i argumentach w klasie po-
chodnej. Choć brzmi to może paradoksalnie, taka możliwość naprawdę istnieje, ale nie
zaleca się stosowania tego typu konstrukcji, gdyż zaciemnia to sposób działania kodu.
Trzeba natomiast wiedzieć, co mogłoby się dziać w tego typu sytuacji, gdyby się jed-
nak przytrafiła. Co będzie się działo w sytuacji przedstawionej na listingu 5.13?
Listing 5.13.
public class A {
private void f() {
System.out.println("Metoda f klasy A");
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 211
Tego typu przykład był prezentowany już w rozdziale 3. Jednak w powyższym przypadku
tak naprawdę w klasie B została zdefiniowana zupełnie nowa metoda f, która nie ma nic
wspólnego z metodą f klasy A.
Jaki napis pojawi się na ekranie? Na podstawie tego, co już zostało powiedziane o wywo-
łaniach polimorficznych, wydawać by się mogło, że będzie to tekst Metoda f klasy B.
Skoro bowiem rzeczywistym typem obiektu jest B, a zostanie to sprawdzone w trakcie
wykonywania programu, wydaje się oczywiste, że powinna zostać wykonana metoda f
klasy B. Tymczasem uruchomienie klasy A da wynik widoczny na rysunku 5.13. Jak wi-
dać, została wykonana metoda f klasy A.
Rysunek 5.13.
Niespodziewane
zachowanie klasy
z metodą prywatną
b4ac40784ca6e05cededff5eda5a930f
b
212 Java. Praktyczny kurs
Co się stało? Otóż gdyby obie metody f były publiczne, czyli metoda f z klasy B przesła-
niałaby metodę f z klasy A, zachowanie programu okazałoby się zgodne z wcześniej-
szymi oczekiwaniami. Wywołanie polimorficzne spowodowałoby, że choć zmienna a
jest typu A, zostałaby wykonana metoda f klasy B, jako że sam obiekt jest typu B. Tym-
czasem ponieważ metoda f z klasy A jest prywatna, a tym samym finalna, maszyna
wirtualna nie ma potrzeby sprawdzania, czy istnieje jej wersja w klasie B (bo taka
wersja nie może istnieć), czyli zachowuje się tak, jakby metody f w klasie B nie było.
Aby uniknąć tego typu problemów, najlepiej stosować się do zasady, że w klasach po-
tomnych nie używa się metod o takich samych nazwach jak nazwy metod prywatnych
w klasie bazowej. Nie ma praktycznej potrzeby stosowania tego typu konstrukcji, a ich
unikanie pozwoli również ustrzec się niepotrzebnych problemów i niejasności w kon-
strukcji kodu programów.
Ćwiczenie 24.1.
Popraw kod z listingu 5.8 tak, aby po wystąpieniu wyjątku nie następowało niekon-
trolowane zakończenie działania programu. Zastosuj właściwy blok try…catch.
Ćwiczenie 24.2.
Zmodyfikuj kod z listingu 5.10 tak, by metoda toString nie była jawnie wywoływana.
Zachowanie programu ma pozostać bez zmian.
Ćwiczenie 24.3.
Zmodyfikuj kod klas z listingu 5.11 tak, aby wywołanie metody draw powodowało wy-
świetlenie odpowiedniej figury geometrycznej zbudowanej z dowolnych znaków semi-
graficznych.
Ćwiczenie 24.4.
Popraw kod klas z listingów 5.13 tak, by wykonanie kodu metody main z listingu 5.14
powodowało wywołanie metody f z klasy B.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 213
Klasy abstrakcyjne
W poprzedniej lekcji był wykorzystywany zestaw klas opisujących figury geometryczne,
które dziedziczyły po wspólnej klasie bazowej Shape. Klasy te zostały przedstawione
na listingu 5.11. W tego typu przypadkach klasa bazowa często jest tylko atrapą, która
w rzeczywistości nie wykonuje żadnych zadań, służy jedynie do zdefiniowania zestawu
metod, jakimi będą posługiwały się klasy potomne, oraz udostępnienia udogodnień, jakie
niesie możliwość korzystania z wywołań polimorficznych (jak w przykładach z lekcji 24.).
W takich sytuacjach często nie ma potrzeby lub jest wręcz niewskazane, aby były tworzo-
ne obiekty klasy bazowej. W przypadku zwykłych klas nie można jednak nikomu za-
bronić tworzenia ich instancji3, taką możliwość dają natomiast klasy abstrakcyjne.
Klasa abstrakcyjna to taka klasa, która została zadeklarowana z użyciem słowa kluczowe-
go abstract. Przy czym klasa, w której przynajmniej jedna metoda jest abstrakcyjna
(oznaczona słowem kluczowym abstract), MUSI być zadeklarowana jako abstrakcyjna4.
Schemat takiej konstrukcji wygląda następująco:
[public] abstract class nazwa_klasy {
[specyfikator_dostępu] abstract typ_zwracany nazwa_metody(argumenty);
}
Metoda abstrakcyjna ma jedynie definicję, nie może zawierać żadnego kodu (nie wolno
użyć nawet pustego nawiasu klamrowego; deklarację należy zakończyć średnikiem).
Przykładowo metoda draw z klasy Shape z zestawu klas z listingu 5.11 mogłaby być z po-
wodzeniem metodą abstrakcyjną. Zgodnie z powyższym opisem klasa Shape w takim wy-
padku również musiałaby być abstrakcyjna. Cała konstrukcja miałaby następującą postać:
public abstract class Shape {
public abstract void draw();
}
Po takiej deklaracji nie będzie można tworzyć obiektów klasy Shape. Próba wykonania
przykładowej instrukcji:
Shape shape = new Shape();
spowoduje błąd kompilacji zaprezentowany na rysunku 5.14 (nie ma w niej bowiem de-
finicji metody draw).
3
Można by co prawda zastosować prywatny konstruktor, ale to nie oznacza, że w ogóle nie da się utworzyć
obiektu danej klasy.
4
Nie wyklucza to oczywiście istnienia klas abstrakcyjnych, w których żadna z metod nie jest abstrakcyjna.
b4ac40784ca6e05cededff5eda5a930f
b
214 Java. Praktyczny kurs
Rysunek 5.14. Klasa pochodna musi zawierać metody zadeklarowane jako abstrakcyjne w klasie bazowej
Można mieć zatem pewność, że jeśli klasa bazowa zawiera metodę abstrakcyjną, to
każda klasa potomna również ją zawiera. Da się więc bezpiecznie stosować wywołania
polimorficzne, takie jak omówione w poprzedniej lekcji.
W tym miejscu może paść pytanie, czemu klasa, która zawiera metodę abstrakcyjną, rów-
nież musi być zadeklarowana jako abstrakcyjna. Odpowiedź jest prosta: gdyby w zwykłej
klasie została zadeklarowana metoda abstrakcyjna, a następnie utworzony zostałby
obiekt tej klasy, nie byłoby przecież możliwe wywołanie tej metody, ponieważ nie miała-
by ona kodu wykonywalnego. Musiałoby się to skończyć błędem w trakcie wykonania
aplikacji.
Należy jednak pamiętać, że zgodnie z definicją podaną powyżej klasa musi być zade-
klarowana jako abstrakcyjna, jeżeli co najmniej jedna jej metoda jest abstrakcyjna. Wy-
nika z tego, że nie ma żadnych przeciwwskazań, aby pozostałe metody nie były abs-
trakcyjne. Taka sytuacja została zilustrowana na listingu 5.15.
Listing 5.15.
public abstract class Shape {
public abstract void draw();
public String toString() {
return "metoda toString z klasy Shape";
}
}
Zostały tu zdefiniowane trzy klasy: klasa abstrakcyjna Shape oraz klasy od niej pochodne
Rectangle i Triangle. W klasie Shape są dwie metody: publiczna metoda abstrakcyjna
draw oraz zwykła metoda toString. Klasa Rectangle zawiera definicje metod draw
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 215
oraz toString, natomiast Triangle jedynie definicję metody draw. Jak już wiadomo, taka
sytuacja jest możliwa, gdyż metoda toString klasy Shape nie jest abstrakcyjna, klasy
pochodne nie muszą zatem zawierać jej definicji. Aby się przekonać, jak działa taki ze-
staw klas i metod, napiszmy dodatkowo testową klasę Main. Została ona zaprezentowana
na listingu 5.16.
Listing 5.16.
public class Main {
public void drawShape(Shape shape) {
shape.draw();
}
public static void main(String args[]) {
Main main = new Main();
Klasa Main zawiera metodę drawShape przyjmującą jako argument obiekt klasy Shape.
Metodzie tej będą przekazywane obiekty klas Rectangle i Triangle. Jest to konstrukcja
wywołania polimorficznego analogiczna do zaprezentowanej na listingu 5.12 w lekcji 24.
W metodzie main, od której rozpoczyna się wykonywanie kodu, tworzone są obiekty
klas Main, Rectangle i Triangle, a następnie zostają wywołane metody drawShape oraz
toString. Wszystkie te konstrukcje były już prezentowane wcześniej, nie wymagają
więc dokładniejszego tłumaczenia.
Efekt działania klasy Main jest widoczny na rysunku 5.15. W pierwszej sekcji dzięki
wywołaniom polimorficznym zostały wykonane metody drawShape klas Triangle oraz
Rectangle. Metody te musiały być w tych klasach zdefiniowane, gdyż klasy te dziedziczą
po Shape, w której metoda drawShape została zadeklarowana jako abstrakcyjna. W sekcji
drugiej zostały wywołane metody toString obiektów triangle oraz rectangle. Po-
nieważ jednak w klasie Triangle nie była zdefiniowana przeciążona metoda toString,
została wywołana metoda toString odziedziczona po klasie Shape. Jest to zachowanie jak
najbardziej prawidłowe i zgodne z oczekiwaniami. Warto zatem zapamiętać, że w klasie
abstrakcyjnej mogą znaleźć się również metody, które nie są abstrakcyjne.
b4ac40784ca6e05cededff5eda5a930f
b
216 Java. Praktyczny kurs
Rysunek 5.15.
Wynik działania klas
przedstawionych na
listingach 5.15 i 5.16
Wywołania konstruktorów
Wywołania konstruktorów nie są polimorficzne, ważne jednak, aby wiedzieć, jaka jest
kolejność ich wywoływania, szczególnie w odniesieniu do hierarchii klas. Otóż w klasie
potomnej zawsze musi zostać wywołany konstruktor klasy bazowej. Powinno to być
oczywiste, jako że obiekt klasy potomnej zawiera w sobie obiekt klasy bazowej (ale
może nie mieć dostępu do niektórych jego składowych!). Skoro tak, zawsze najpierw
powinien zostać wykonany konstruktor klasy bazowej, a dopiero potem klasy potomnej.
Programista ma wtedy pewność, że obiekt klasy bazowej został prawidłowo zainicjo-
wany. Sytuacja taka jest zilustrowana na listingu 5.17.
Listing 5.17.
public class A {
public A() {
System.out.println("Konstruktor klasy A");
}
}
Klasa A zawiera jeden publiczny konstruktor, którego zadaniem jest wyświetlenie na ekra-
nie napisu oznajmiającego, z jakiej klasy pochodzi. Klasa B, dziedzicząca po klasie A,
również zawiera jeden publiczny konstruktor, który wyświetla na ekranie informację,
że pochodzi z klasy B. Konstruktory te są domyślne, jako że nie przyjmują żadnych
argumentów. W klasie B została dodatkowo zdefiniowana metoda main, w której jest
tworzony jeden obiekt klasy B. Zatem zgodnie z tym, co zostało napisane powyżej, w ta-
kiej sytuacji najpierw zostanie wywołany konstruktor klasy bazowej A, a dopiero po nim
konstruktor klasy potomnej B, co jest widoczne na rysunku 5.16.
Obowiązuje zatem zasada, że jeśli w konstruktorze klasy potomnej nie będzie jawnie wy-
wołany żaden konstruktor klasy bazowej, automatycznie zostanie wywołany domyślny
konstruktor klasy bazowej (konstruktorem domyślnym jest konstruktor bezargumentowy).
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 217
Rysunek 5.16.
W klasie potomnej
zawsze jest wywoływany
konstruktor klasy
bazowej
Co by się jednak stało, gdyby w klasie A z listingu 5.17 nie umieścić żadnego kon-
struktora, czyli gdyby miała ona postać zaprezentowaną poniżej?
public class A {
}
Obie klasy udałoby się skompilować, a na ekranie pojawiłby się jedynie napis Kon-
struktor klasy B. I choć wydawać by się mogło, że w takim razie nie został wywołany
żaden konstruktor klasy A, w rzeczywistości tak nie jest. Konstruktor musi zostać wy-
wołany, nawet jeśli nie zostanie umieszczony jawnie w ciele klasy. W takiej sytuacji
Java dodaje własny pusty konstruktor domyślny, który rzecz jasna jest wywoływany.
Z zupełnie inną sytuacją mamy do czynienia, kiedy w klasie bazowej nie zostanie umiesz-
czony konstruktor domyślny (czyli taki, który nie przyjmuje żadnych argumentów),
ale znajdzie się w niej dowolny inny konstruktor. Sytuacja tego typu została zapre-
zentowana na listingu 5.18.
Listing 5.18.
public class A {
public A(int arg) {
System.out.println("Konstruktor klasy A");
}
}
Klasa B nie zmieniła się w stosunku do wersji zaprezentowanej na listingu 5.17, zmody-
fikowana została natomiast treść klasy A. Nie ma już konstruktora domyślnego, bezar-
gumentowego, pojawił się natomiast konstruktor przyjmujący jeden argument typu
int. Jak już wiadomo, taka konstrukcja jest niepoprawna, gdyż konstruktor klasy B
będzie próbował wywołać domyślny konstruktor klasy A, którego po prostu nie ma (pusty
konstruktor domyślny zostałby dodany przez kompilator tylko wtedy, gdyby w klasie A
nie było żadnego innego konstruktora). Już przy próbie kompilacji zostanie zatem za-
sygnalizowany błąd, który został zaprezentowany na rysunku 5.17.
b4ac40784ca6e05cededff5eda5a930f
b
218 Java. Praktyczny kurs
Kod z listingu 5.18 można poprawić na dwa sposoby: dopisując konstruktor domyślny
do klasy A lub też jawnie wywołując istniejący w niej konstruktor. W tym drugim przy-
padku należałoby zastosować następującą konstrukcję:
public B() {
super(0);
System.out.println("Konstruktor klasy B");
}
Listing 5.19.
public class A {
public A() {
System.out.println("Konstruktor klasy A");
}
}
public class C {
A obiektA;
B obiektB;
public C() {
System.out.println("Konstruktor klasy C");
obiektA = new A();
obiektB = new B();
}
public static void main(String args[]) {
C obiektC = new C();
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 219
Klasy A i B mają tu postać taką jak na listingu 5.17, z tą różnicą, że z B została usunięta
metoda main. W klasie C zostały zadeklarowane dwa pola o nazwach obiektA i obiektB.
Pierwsze z nich jest typu A, drugie typu B. Oba są inicjowane w konstruktorze klasy C,
wcześniej jednak konstruktor ten przedstawia się, czyli wyświetla na ekranie informację,
z jakiej klasy pochodzi. W metodzie main z kolei zostaje utworzony nowy obiekt klasy
C. Jaka zatem będzie kolejność wykonania konstruktorów w tym przykładzie?
Skoro tworzony jest obiekt klasy C, najpierw zostanie wywołany konstruktor tej klasy,
a tym samym w pierwszej kolejności pojawi się na ekranie napis Konstruktor klasy C.
W tym konstruktorze jest tworzony obiekt klasy A, zatem wywołany zostanie konstruk-
tor tej klasy i zostanie wyświetlony napis Konstruktor klasy A. W kolejnym kroku po-
wstaje obiekt klasy B. Klasa B dziedziczy jednak po A, więc w tym kroku zostaną wywoła-
ne dwa konstruktory, najpierw z klasy bazowej A, a następnie z potomnej B. W związku
z tym na ekranie pojawi się ciąg napisów widoczny na rysunku 5.18.
Rysunek 5.18.
Złożone wywołania
konstruktorów
Listing 5.20.
public class A {
public A() {
//System.out.println("Konstruktor klasy A");
f();
}
public void f() {
//System.out.println("A:f");
}
}
b4ac40784ca6e05cededff5eda5a930f
b
220 Java. Praktyczny kurs
}
public static void main(String args[]) {
B b = new B(1);
b.f();
}
}
Na pierwszy rzut oka ten kod wygląda całkiem poprawnie. Klasa A zawiera publiczną
metodę f, która nie robi nic, oraz wywołujący ją konstruktor domyślny. W komentarze
zostały ujęte instrukcje System.out.println, które później pozwolą dokładniej prześledzić
sposób działania programu.
Warto prześledzić kolejne etapy działania programu. W metodzie main klasy B jest
tworzony obiekt tej klasy. Powoduje to oczywiście wywołanie konstruktora klasy B,
ale uwaga: zgodnie z informacjami podanymi już w tej lekcji, ponieważ klasa B dziedziczy
po A, przed wykonaniem jej konstruktora jest wywoływany konstruktor domyślny klasy A.
Spójrzmy więc, co się dzieje w konstruktorze klasy A: jest tam wywoływana metoda f.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 221
Skoro jednak sam obiekt jest typu B, wywołanie to będzie POLIMORFICZNE, a zatem
zostanie wywołana metoda f z klasy B! Oznacza to, że fragment kodu:
double wynik = 1 / dzielnik;
System.out.println("1 / dzielnik to: " + wynik);
Ćwiczenie 25.1.
Napisz klasę zawierającą abstrakcyjną metodę toString. Wyprowadź z tej klasy klasę
potomną.
Ćwiczenie 25.2.
Napisz klasę First zawierającą abstrakcyjną metodę f, wyprowadź z tej klasy klasę
potomną Second zawierającą abstrakcyjną metodę g. Z klasy Second wyprowadź klasę
Third. Pamiętaj o zdefiniowaniu wszystkich niezbędnych metod.
Ćwiczenie 25.3.
Zmodyfikuj kod z listingu 5.19 tak, aby obiekty obiektA i obiektB były inicjowane nie
w konstruktorze klasy C, ale w momencie ich deklaracji. Zaobserwuj kolejność wyko-
nywania konstruktorów.
b4ac40784ca6e05cededff5eda5a930f
b
222 Java. Praktyczny kurs
Ćwiczenie 25.4.
Zmodyfikuj kod z listingu 5.19 tak, by klasa C dziedziczyła po B. Jaka będzie kolejność
wykonania konstruktorów?
Interfejsy
Lekcja 26. Tworzenie interfejsów
W lekcji 25. zostały omówione klasy abstrakcyjne, czyli takie, w których znajdowały
się metody niemające implementacji. Implementacja tych metod musiała się odbywać
w klasach potomnych (o ile klasy potomne nie były również zadeklarowane jako abs-
trakcyjne). W tej lekcji zostaną przedstawione interfejsy, które można traktować (w wer-
sjach Javy do 7 włącznie) jako klasy czysto abstrakcyjne, niemające żadnej implementacji.
Czym są interfejsy?
W Javie do wersji 8 interfejs to klasa czysto abstrakcyjna, czyli taka, w której wszystkie
metody są traktowane jako abstrakcyjne. W wersji 8 wprowadzono jednak (ze wzglę-
du na inne zmiany dotyczące języka) możliwość definiowania tzw. metod domyśl-
nych (o czym pod koniec lekcji)5. Interfejs deklaruje się za pomocą słowa kluczowe-
go interface. Może on być publiczny, o ile jest zdefiniowany w pliku o takiej samej
nazwie jak nazwa interfejsu, lub pakietowy. W tym drugim przypadku jest dostępny
jedynie dla klas wchodzących w skład danego pakietu. Schematyczna konstrukcja in-
terfejsu wygląda więc następująco:
[public] interface nazwa_interfejsu {
typ_zwracany nazwa_metody1(argumenty);
typ_zwracany nazwa_metody2(argumenty);
/*…dalsze metody interfejsu…*/
typ_zwracany nazwa_metodyN(argumenty);
}
Listing 5.21.
public interface Drawable {
public void draw();
}
Tak zdefiniowany interfejs może być implementowany przez dowolną klasę. Jeśli
mówimy, że dana klasa implementuje interfejs, oznacza to, że zawiera ona definicje
wszystkich metod zadeklarowanych w interfejsie. Jeśli choć jedna metoda zostanie
5
W interfejsie mogą się również znaleźć metody statyczne oraz tzw. typy zagnieżdżone (ang. nested types).
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 223
Próba kompilacji takiej klasy skończy się… błędem kompilacji. Jest to widoczne na
rysunku 5.21.
Listing 5.22.
public class MojaKlasa implements Drawable {
public void draw() {
System.out.println("MojaKlasa");
}
}
Widać już więc, że interfejs określa po prostu, jakie metody muszą znaleźć się w klasie,
która go implementuje (można powiedzieć, że interfejs to rodzaj kontraktu, który mu-
si być spełniony przez klasę). Wróćmy teraz do przykładu z figurami z lekcji 24. i 25.
(klasy Triangle, Rectangle, Circle). Wymuszaliśmy w tych klasach deklarację metody
draw. Odbywało się to poprzez umieszczenie w klasie nadrzędnej Shape abstrakcyjnej
metody draw. Można to jednak zrobić w inny sposób — wykorzystując interfejs Drawable.
6
Lub musi być klasą abstrakcyjną.
b4ac40784ca6e05cededff5eda5a930f
b
224 Java. Praktyczny kurs
Wystarczy, jeśli każda z klas będzie ten interfejs implementowała, tak jak jest to
przedstawione na listingu 5.23. W takiej sytuacji również występuje konieczność de-
klaracji metody draw w każdej z przedstawionych klas.
Listing 5.23.
public class Circle implements Drawable {
public void draw() {
System.out.println("metoda draw z klasy Circle");
}
}
Listing 5.24.
public class Shape {
public void draw() {
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 225
Zwróćmy jednak uwagę na pewien mankament takiego rozwiązania. Otóż klasy Rectangle,
Triangle i Circle, czyli pochodne od Shape, implementują interfejs Drawable, ale sama
klasa Shape już nie. Oznacza to, że w klasach potomnych wymuszana jest implemen-
tacja metody draw, a w klasie bazowej nie. A więc nawet jeśli nie zapomnimy o napisa-
niu pustej metody draw w klasie Shape i kompilacja uda się bez problemu, nie będziemy
mogli skorzystać z udogodnień, jakie dają wywołania polimorficzne. Z kolei jeżeli nie
uwzględnimy metody draw w klasie Shape, kompilacja nie będzie możliwa. Łatwo się
o tym przekonać, redukując klasę Shape do postaci:
public class Shape {
}
oraz próbując skompilować klasę Main przedstawioną na listingu 5.25 (lub tę z listingu
5.12; obie mają prawie identyczną postać). Spowoduje to błąd widoczny na rysunku 5.22.
To nie powinno dziwić, przecież nie istnieje teraz w klasie Shape metoda draw, a pró-
bujemy ją wywołać w metodzie drawShape w klasie Main. Kompilator musi zatem za-
protestować.
Listing 5.25.
public class Main {
public void drawShape(Shape shape) {
shape.draw();
}
public static void main(String args[]) {
Main main = new Main();
Rysunek 5.22.
Próba wywołania
nieistniejącej metody
w klasie Shape
b4ac40784ca6e05cededff5eda5a930f
b
226 Java. Praktyczny kurs
Jakie jest rozwiązanie tego problemu? Otóż metoda bazowa również powinna imple-
mentować interfejs Drawable, czyli klasa Shape powinna mieć postać:
public class Shape implements Drawable {
public void draw(){};
}
Warto rozważyć jeszcze jeden problem. Jeśli jest wiele klas potomnych i każda z nich
ma implementować dany interfejs, łatwo o pominięcie słowa implements w jednej z nich,
a tym samym spowodowanie powstania błędu. Jak temu zapobiec? Czy nie dałoby się
zmusić klas potomnych do zaimplementowania interfejsu z klasy bazowej? Okazuje
się, że można to zrobić, pod warunkiem jednak, że klasa bazowa będzie klasą abstrak-
cyjną. W takiej sytuacji klasa ta nie będzie musiała implementować metod zawartych
w interfejsie, ale będą je musiały implementować wszystkie nieabstrakcyjne klasy po-
chodne. Taka sytuacja została przedstawiona na listingu 5.26.
Listing 5.26.
public abstract class Shape implements Drawable {
}
W tej chwili próba usunięcia metody draw z którejkolwiek z klas: Triangle, Rectangle,
Circle, mimo iż bezpośrednio nie implementują one interfejsu Drawable, skończyłaby
się błędem kompilacji.
Pola interfejsów
Interfejsy, oprócz deklaracji metod, mogą również zawierać pola. Pola interfejsu są zaw-
sze publiczne, statyczne oraz finalne, czyli trzeba im przypisać wartości już w momen-
cie ich deklaracji. Można stosować typy zarówno proste, jak i złożone. Pola interfejsów
są najczęściej wykorzystywane do tworzenia wyliczeń. Deklaracja pola interfejsu nie
różni się od deklaracji pola klasy, schematycznie wygląda tak:
[public] interface nazwa_interfejsu {
typ_pola1 nazwa_pola1 = wartość_pola1;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 227
Warto podkreślić jeszcze raz: pole interfejsu musi być zainicjowane już w momencie
deklaracji, inaczej kompilator zgłosi błąd. Przykładowy interfejs zawierający trzy pola
typu prostego jest przedstawiony na listingu 5.27. Zwróćmy uwagę na konwencję za-
pisu: ponieważ pola interfejsów są zawsze statyczne i finalne, przyjmuje się, że ich na-
zwy są pisane wielkimi literami, a poszczególne człony nazwy są oddzielane znakiem
podkreślenia.
Listing 5.27.
public interface NowyInterfejs {
int POLE_TYPU_INT = 100;
double POLE_TYPU_DOUBLE = 1.0;
char POLE_TYPU_CHAR = 'a';
}
Nic nie stoi również na przeszkodzie, aby interfejs zawierał pola typu obiektowego,
ważne jest tylko, żeby były one inicjowane również w momencie deklaracji, dokładnie
tak, jak ma to miejsce w przypadku pól typów prostych. Przykład takiego interfejsu
został zaprezentowany na listingu 5.28.
Listing 5.28.
public interface NowyInterfejs {
Rectangle POLE_TYPU_RECTANGLE = new Rectangle();
Triangle POLE_TYPU_TRIANGLE = new Triangle();
Circle POLE_TYPU_CICRLE = new Circle();
}
Metody domyślne
W Java 8 wprowadzono możliwość definiowania w interfejsach tzw. metod domyślnych
(ang. default methods). Taka metoda może mieć definicję (implementację) i ta defini-
cja zostanie odziedziczona przez wszystkie klasy implementujące dany interfejs. Do
jej utworzenia należy użyć słowa kluczowego default. Nie wyklucza to jednak istnienia
w interfejsie typowych metod abstrakcyjnych (które były omawiane do tej pory). Schemat
takiej konstrukcji jest następujący:
[public] interface nazwa_interfejsu {
typ_zwracany nazwa_metody1(argumenty);
typ_zwracany nazwa_metody2(argumenty);
/*…dalsze metody abstrakcyjne interfejsu…*/
typ_zwracany nazwa_metody_domyślnej1(argumenty){
/*treść metody domyślnej 1*/
}
typ_zwracany nazwa_metody_domyślnej2(argumenty){
/*treść metody domyślnej 2*/
}
/*…dalsze metody domyślne interfejsu…*/
}
b4ac40784ca6e05cededff5eda5a930f
b
228 Java. Praktyczny kurs
Załóżmy, że w trakcie prac nad interfejsem Drawable (z listingu 5.21) wystąpiła koniecz-
ność jego unowocześnienia i dodania metody draw3D. Jej zadaniem ma być np. wyświetla-
nie danego kształtu z symulacją trójwymiarowości. Jeżeli do interfejsu zostanie dodana
zwyczajna deklaracja metody (w sposób dostępny w Javie do wersji 7; będzie to więc
metoda abstrakcyjna), przestaną działać wszystkie dotychczas napisane klasy (nie będą
bowiem implementowały metody draw3D). W związku z tym trzeba będzie albo doko-
nać poprawek we wszystkich klasach implementujących interfejs, albo też utworzyć
zupełnie nowy interfejs (np. o nazwie Drawable3D). Jednak dzięki dostępnym w Java 8
i w nowszych wersjach metodom domyślnym da się wprowadzić poprawki do istniejące-
go interfejsu Drawable w taki sposób, aby „stare” klasy (Shape, Rectangle itp.) mogły
działać bez problemów. Kod tego interfejsu mógłby wyglądać tak jak na listingu 5.29.
Listing 5.29.
public interface Drawable {
public void draw();
default public void draw3D(){
System.out.println("Domyślny kształt 3D");
}
}
Listing 5.30.
public class Triangle extends Shape {
public void draw() {
System.out.println("metoda draw klasy Triangle");
}
public void draw3D() {
System.out.println("metoda draw3D klasy Triangle");
}
}
Listing 5.31.
public class Main {
public void drawShape(Shape shape) {
shape.draw();
}
public void drawShape3D(Shape shape) {
shape.draw3D();
}
public static void main(String args[]) {
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 229
Klasa Triangle (listing 5.30) dziedziczy po Shape (listing 5.26), która z kolei jest abs-
trakcyjna i implementuje interfejs Drawable. W klasie znajdują się dwie metody draw
oraz draw3D — ich zadaniem jest wyświetlenie komunikatu z nazwą danej metody.
Klasa Main z listingu 5.31 zawiera program pozwalający na przetestowanie różnych
właściwości najnowszej wersji interfejsu Drawable (z listingu 5.29). Analogicznie do wer-
sji z listingu 5.25 mamy w niej metodę drawShape wywołującą metodę draw obiektu
przekazanego jako argument oraz dodatkowo metodę drawShape3D, która wywołuje
metodę draw3D.
W metodzie main, od której zacznie się wykonywanie programu, tworzony jest obiekt
klasy Main oraz obiekty klas Triangle, Rectangle i Circle. Następnie dwukrotnie
wywoływana jest metoda drawShape3D obiektu wskazywanego przez zmienną main.
Przy pierwszym wywołaniu jako argument przekazywany jest obiekt klasy Triangle,
a przy drugim — obiekt klasy Rectangle. W ostatnim wierszu wywoływana jest natomiast
metoda drawShape i jest jej przekazywany obiekt klasy Circle. Oczywiście każde z tych
wywołań będzie polimorficzne (lekcja 24.).
Jaki zatem będzie efekt działania aplikacji i jakie napisy pojawią się po jej uruchomieniu?
Widać to na rysunku 5.23. Wszystkie klasy oznaczające figury dziedziczą po klasie ba-
zowej Shape i w związku z tym implementują najnowszą wersję interfejsu Drawable
(listing 5.29). Interfejs ten zawiera domyślną metodę draw3D, ale klasa Triangle w wersji
z listingu 5.30 ma własną wersję tej metody. Dlatego w pierwszym wywołaniu draw
Shape3D zostanie użyta metoda draw3D pochodząca z klasy Triangle.
Rysunek 5.23.
Efekt użycia metody
domyślnej interfejsu
W drugim wywołaniu drawShape3D jest już inaczej. Użyta została stara wersja klasy
Rectangle (z listingu 5.26), która nic „nie wie” o istnieniu metody draw3D. Dlatego też
zostanie zastosowana domyślna metoda pochodząca z interfejsu Drawable, a na ekranie
pojawi się napis „Jestem kształtem 3D”. Ostatnie wywołanie dotyczy obiektu klasy
b4ac40784ca6e05cededff5eda5a930f
b
230 Java. Praktyczny kurs
Circle oraz metody draw. Ono nie powinno budzić wątpliwości. Nowa wersja interfejsu
nie zmienia zachowania starych metod, dlatego efekt będzie taki jak w przykładzie z li-
stingu 5.25.
Ćwiczenie 26.1.
Napisz kod interfejsu o nazwie Rysowanie, w którym zostaną zadeklarowane metody
rysuj2D i rysuj3D. Następnie napisz klasę Figura, która będzie implementowała ten
interfejs.
Ćwiczenie 26.2.
Zmodyfikuj kod z listingu 5.24 tak, aby klasa Shape implementowała interfejs Drawable,
a klasa Rectangle go nie implementowała.
Ćwiczenie 26.3.
Napisz przykładowy interfejs zawierający dwa pola, jedno typu int, drugie typu double,
oraz implementującą go klasę wyświetlającą wartości tych pól.
Ćwiczenie 26.4.
Napisz przykładowy interfejs zawierający dwa pola, jedno typu int, drugie typu double,
oraz klasę wyświetlającą wartości tych pól. Klasa ta nie może implementować tego
interfejsu.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 231
Listing 5.32.
public interface PierwszyInterfejs {
public void f();
}
Po co jednak klasa miałaby implementować wiele interfejsów? Czy nie lepiej byłoby po
prostu napisać jeden? Okazuje się, że nie, traci się bowiem wtedy uniwersalność kodu.
Dla przykładu załóżmy, że napisane zostały klasy Telewizor i Radio oraz dwa interfejsy
WydajeDzwiek oraz WyswietlaObraz. Taka sytuacja jest zobrazowana na listingu 5.33.
Listing 5.33.
public interface WydajeDzwiek {
public void graj();
}
b4ac40784ca6e05cededff5eda5a930f
b
232 Java. Praktyczny kurs
Jak wiadomo, telewizor zarówno wyświetla obrazy, jak i wydaje dźwięk, implementuje
zatem oba interfejsy: WyswietlaObraz i WydajeDzwiek, a więc zawiera również metody
graj i wyswietl. Radio nie wyświetla obrazów, w związku z czym implementuje je-
dynie interfejs WydajeDzwiek, a klasa Radio zawiera wyłącznie metodę graj. Jak widać,
dzięki temu, że jedna klasa może implementować wiele interfejsów, mogą one być bar-
dziej uniwersalne. Gdyby taka możliwość nie istniała, dla telewizora musiałby po-
wstać interfejs np. o nazwie WyswietlaObrazIWydajeDzwiek, w którym zostałyby zade-
klarowane metody graj i wyswietl, co ograniczałoby jego zastosowania. Może to być
jednak ćwiczenie do samodzielnego wykonania.
Konflikty nazw
Kiedy jedna klasa implementuje wiele interfejsów, mogą powstać budzące wątpliwości
sytuacje konfliktu nazw, które niestety czasami prowadzą do powstawania błędów (na
szczęście zwykle wykrywanych w trakcie kompilacji). Załóżmy, że mamy dwa interfejsy
przedstawione na listingu 5.34.
Listing 5.34.
public interface PierwszyInterfejs {
public void f();
}
Są one w pełni poprawne, kod ten nie budzi żadnych wątpliwości. Co się natomiast
stanie, kiedy przykładowa klasa MojaKlasa będzie miała implementować oba te inter-
fejsy? Z kodu pierwszego interfejsu wynika, że klasa MojaKlasa powinna mieć zdefi-
niowaną metodę o nazwie f, z kodu drugiego wynika dokładnie to samo. Czy zatem
klasa ta ma zawierać dwie metody f? Oczywiście nie, nie byłoby to przecież możliwe.
Skoro jednak deklaracje metod f w obu interfejsach są takie same, oznacza to, że klasa
implementująca oba interfejsy musi zawierać JEDNĄ metodę f o deklaracji public
void f(). Zatem przykładowa klasa MojaKlasa będzie miała postać przedstawioną na
listingu 5.35.
Listing 5.35.
public class MojaKlasa implements PierwszyInterfejs, DrugiInterfejs {
public void f() {
System.out.println("f");
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 233
Nieco inna, ale również jednoznaczna sytuacja nastąpi, kiedy w dwóch interfejsach
będą metody o takiej samej nazwie, ale innych argumentach. Wtedy również będzie
można zaimplementować je w jednej klasie. Takie dwa przykładowe interfejsy zostały
przedstawione na listingu 5.36.
Listing 5.36.
public interface PierwszyInterfejs {
public void f(int argument);
}
Listing 5.37.
public class MojaKlasa implements PierwszyInterfejs, DrugiInterfejs {
public void f() {
System.out.println("f");
}
public void f(int argument) {
System.out.println("f:argument = " + argument);
}
public int f(double argument) {
System.out.println("f:argument = " + argument);
return (int) argument;
}
}
Co się jednak stanie, kiedy interfejsy przyjmą postać widoczną na listingu 5.38? Czy
jakakolwiek klasa może je jednocześnie implementować?
Listing 5.38.
public interface PierwszyInterfejs {
b4ac40784ca6e05cededff5eda5a930f
b
234 Java. Praktyczny kurs
Odpowiedź na zadane powyżej pytanie brzmi: nie. Co by się bowiem stało, gdyby utwo-
rzona wcześniej przykładowa klasa MojaKlasa miała implementować oba interfejsy
przedstawione na listingu 5.38? Musiałaby mieć postać z listingu 5.39.
Listing 5.39.
public class MojaKlasa implements PierwszyInterfejs, DrugiInterfejs {
public void f() {
//System.out.println("f");
}
public int f() {
//System.out.println("f");
}
}
Taka klasa jest w sposób oczywisty nieprawidłowa. W jednej klasie nie mogą istnieć
dwie metody o takiej samej nazwie różniące się jedynie typem zwracanego wyniku.
Próba kompilacji takiego kodu spowoduje powstanie serii błędów widocznych na ry-
sunku 5.24.
Podobna sytuacja jest przedstawiona na listingu 5.40, choć na pierwszy rzut oka kod może
wydawać się poprawny. Jeśli bowiem wziąć pod uwagę kod klasy MojaDrugaKlasa oraz
interfejsu PierwszyInterfejs, wygląda na to, że nie zawiera on błędu. Nie widać prze-
ciwwskazań do implementacji metody f przyjmującej argument typu int i niezwraca-
jącej wyniku.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 235
Listing 5.40.
public interface MojInterfejs {
public void f(int argument);
}
Kompilacja przedstawionego kodu jednak się nie uda i to niezależnie od tego, czy kod
metody będzie ujęty w komentarz (tak jak jest to widoczne na listingu), czy nie. Wszystko
staje się jasne po przyjrzeniu się klasie bazowej dla MojaDrugaKlasa, czyli klasie Moja
Klasa. Znajduje się w niej metoda o nazwie f przyjmująca jeden argument typu int
i zwracająca wartość typu int. Ponieważ klasa MojaDrugaKlasa dziedziczy po MojaKlasa,
zawiera również tę metodę, a tym samym nie może implementować interfejsu Pierwszy
Interfejs.
A zatem gdy aktywny jest komentarz w metodzie f klasy MojaDrugaKlasa, kod jest nie-
prawidłowy, ponieważ ze względu na implementowanie interfejsu MojInterfejs w klasie
musi istnieć metoda o deklaracji void f(int). Z kolei gdy taka metoda zostanie dodana
(a komentarz usunięty), kompilacja się nie uda, bo metoda o deklaracji void f(int) nie
może być użyta ze względu na odziedziczoną po klasie bazowej (MojaKlasa) metodę
o deklaracji int f(int).
Dziedziczenie interfejsów
Interfejsy można budować, stosując mechanizm dziedziczenia. Odbywa się to podobnie
jak w przypadku klas. Należy wykorzystać słowo extends. Interfejs potomny będzie za-
wierał wszystkie swoje metody oraz wszystkie metody z interfejsu bazowego. Schemat
zastosowania słowa extends w przypadku dziedziczenia interfejsów wygląda następująco:
[public] interface interfejs_potomny extends interfejs_bazowy {
//deklaracje pól i metod
}
Listing 5.41.
public interface I1 {
public void f();
}
b4ac40784ca6e05cededff5eda5a930f
b
236 Java. Praktyczny kurs
Jak widać, interfejs I2 dziedziczy po interfejsie I1, zatem zawiera własną metodę g
oraz odziedziczoną f. Przykładowa klasa, która będzie implementowała interfejs I2, bę-
dzie w związku z tym musiała zawierać zarówno metodę f, jak i g, bo inaczej nie uda się
skompilować jej kodu. Taka przykładowa klasa jest widoczna na listingu 5.42.
Listing 5.42.
public class MojaKlasa implements I2 {
public void f() {
//treść metody f
}
public void g() {
//treść metody g
}
}
Interfejs, inaczej niż zwykła klasa, może dziedziczyć nie tylko po jednym, ale też po wielu
interfejsach. Interfejsy bazowe należy w takiej sytuacji umieścić po słowie extends
i oddzielić ich nazwy przecinkami. Schematycznie taka konstrukcja wygląda następująco:
[public] interface nazwa_interfejsu extends interfejs1,
interfejs2, ... , interfejsN {
//metody i pola interfejsu
}
Listing 5.43.
public interface I1 {
public void f();
}
public interface I2 {
public void g();
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 237
Listing 5.44.
public class MojaKlasa implements I3 {
public void f() {//wymuszona przez I1
//treść metody f
}
public void g() { //wymuszona przez I2
//treść metody g
}
public void h() {//wymuszona przez I3
//treść metody g
}
}
Listing 5.45.
public interface I1 {
public void f();
}
Błąd pojawi się również w sytuacji przedstawionej na listingu 5.46 — tym razem doty-
czy trzech interfejsów. Interfejs I1 zawiera bezargumentową metodę f o typie zwraca-
nym int, I2 zawiera również bezargumentową metodę f, ale o typie zwracanym double.
Trzeci interfejs o nazwie I3 dziedziczy zarówno po I1, jak i po I2 (dodatkowo zawiera
własną metodę o nazwie h). Oczywiście czegoś takiego nie wolno zrobić, gdyż to
prowadzi do konfliktu nazw. Próba kompilacji interfejsu I3 spowoduje pojawienie się
komunikatu o błędzie widocznego na rysunku 5.25.
Listing 5.46.
public interface I1 {
public int f();
}
public interface I2 {
public double f();
}
b4ac40784ca6e05cededff5eda5a930f
b
238 Java. Praktyczny kurs
Ćwiczenie 27.1.
Dopisz do interfejsu PierwszyInterfejs z listingu 5.32 bezargumentową metodę g o typie
zwracanym void, a do interfejsu DrugiInterfejs bezargumentową metodę f o takim
samym typie. Jak powinna w takiej sytuacji wyglądać treść klasy MojaKlasa?
Ćwiczenie 27.2.
Zmień kod interfejsów z listingu 5.34 tak, aby metoda f interfejsu PierwszyInterfejs
przyjmowała argument typu int, a metoda f interfejsu DrugiInterfejs argument typu
double. Jakich modyfikacji będzie wymagać klasa MojaKlasa z listingu 5.35, by mogła
współpracować z tak zmienionymi interfejsami?
Ćwiczenie 27.3.
Zmień kod z listingu 5.33 w taki sposób, aby klasa Telewizor implementowała tylko
jeden interfejs o nazwie WyswietlaObrazIWydajeDzwiek.
Ćwiczenie 27.4.
Wykorzystując mechanizm dziedziczenia, połącz interfejsy WyswietlaObraz i Wydaje
Dzwiek z listingu 5.33, tak żeby powstał jeden interfejs o nazwie WyswietlaObrazIWydaje
Dzwiek.
Klasy wewnętrzne
Lekcja 28. Klasa w klasie
Przy omawianiu pakietów i rodzajów klas w rozdziale 3. pojawiło się pojęcie klas
wewnętrznych. Temat nie został wtedy rozwinięty, teraz nadszedł czas na bliższe zapo-
znanie się z owym nieprzedstawionym jeszcze rodzajem klas. Lekcja 28. jest poświę-
cona właśnie wprowadzeniu w tę nową tematykę. Zostanie w niej pokazane, w jaki
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 239
sposób tworzy się klasy wewnętrzne oraz jakie mają one właściwości, a także jak dostać
się do ich składowych i jakie relacje zachodzą między klasą wewnętrzną a zewnętrzną.
Aby utworzyć klasę o nazwie Outside, która będzie zawierała w sobie klasę Inside, moż-
na zastosować kod zaprezentowany na listingu 5.47. Należy go zapisać w pliku o na-
zwie Outside.java i skompilować ten plik tak jak każdy inny plik z kodem źródłowym
w Javie. Po kompilacji powstaną dwa pliki z rozszerzeniem class: Outside.class zawiera-
jący kod klasy Outside oraz Outside$Inside.class zawierający kod klasy Inside.
Listing 5.47.
public class Outside {
class Inside {
}
}
W klasie zewnętrznej można bez problemów oraz bez żadnych dodatkowych zabiegów
programistycznych korzystać z obiektów klasy wewnętrznej. Można je tworzyć, a także
bezpośrednio odwoływać się do zdefiniowanych w nich pól i metod. Przykład odwołań
do obiektu klasy wewnętrznej jest widoczny na listingu 5.48.
Listing 5.48.
public class Outside {
class Inside {
public int liczba = 100;
public void f() {
System.out.println("Inside:f liczba = " + liczba);
}
}
7
Dokładniej rzecz ujmując: taka, która została zdefiniowana we wnętrzu innej klasy i nie jest klasą statyczną.
Ściśle zgodnie z terminologią należałoby mówić o klasach zagnieżdżonych (ang. nested classes), które
dzielą się na statyczne klasy zagnieżdżone (ang. static nested classes) i klasy wewnętrzne (ang. inner
classes).
b4ac40784ca6e05cededff5eda5a930f
b
240 Java. Praktyczny kurs
Klasa wewnętrzna o nazwie Inside zawiera jedno publiczne pole typu int o nazwie liczba,
któremu już podczas deklaracji jest przypisywana wartość 100, oraz jedną publiczną
metodę o nazwie f, której zadaniem jest wyświetlenie wartości zapisanej w polu
liczba. Klasa zewnętrzna Outside zawiera publiczną metodę o nazwie g, w której jest
tworzony nowy obiekt klasy Inside. Następnie wywoływana jest metoda f tego obiektu,
a dalej polu liczba jest przypisywana wartość 200 i ponownie jest wywoływana metoda f.
Oprócz metody g w klasie Outside znajduje się metoda main, od której zaczyna się wyko-
nywanie kodu programu. W tej metodzie jest tworzony nowy obiekt klasy Outside
i wywoływana jest jego metoda g. Odbywa się to w jednej linii programu. Niestosowana
do tej pory przez nas konstrukcja:
new Outside().g();
oznacza: utwórz obiekt klasy Outside, a następnie wywołaj jego metodę o nazwie g
(odwołanie do obiektu nie jest tym samym nigdzie zapamiętywane). Efekt działania tego
fragmentu kodu jest taki sam jak rezultat wykonania dwóch instrukcji:
Outside out = new Outside();
out.g();
Rysunek 5.26.
Odwołania do pól
i metod klasy
wewnętrznej
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 241
Listing 5.49.
public class Outside {
SecondInside secondIns = new SecondInside();
class FirstInside {
public int liczba = 100;
public void f() {
System.out.println("FirstInside:f liczba = " + liczba);
}
}
class SecondInside {
public double liczba = 1.0;
public void f() {
System.out.println("SecondInside:f liczba = " + liczba);
}
}
public void g() {
FirstInside firstIns = new FirstInside();
firstIns.f();
firstIns.liczba = 200;
firstIns.f();
secondIns.f();
secondIns.liczba = 2.5;
secondIns.f();
}
public static void main(String args[]) {
new Outside().g();
}
}
Klasa zewnętrzna, Outside, zawiera pole typu SecondInside o nazwie secondIns, do któ-
rego już w trakcie deklaracji jest przypisywany nowy obiekt klasy SecondInside. Do dys-
pozycji są również dwie metody: g oraz main. W g tworzymy zmienną typu FirstInside
o nazwie firstIns i przypisujemy jej odniesienie do nowego obiektu klasy FirstInside,
następnie wywołujemy metodę f tego obiektu, przypisujemy polu liczba wartość 200
oraz ponownie wywołujemy metodę f. W kolejnym kroku wywołujemy metodę f obiektu
wskazywanego przez secondIns, przypisujemy polu liczba tego obiektu wartość 2.5
oraz ponownie wywołujemy jego metodę f. Nie musimy tworzyć obiektu secondIns,
gdyż czynność ta została wykonana w trakcie deklaracji pola secondIns. Ostatecznie
po uruchomieniu takiej aplikacji na ekranie pojawi się widok zaprezentowany na ry-
sunku 5.27.
b4ac40784ca6e05cededff5eda5a930f
b
242 Java. Praktyczny kurs
Rysunek 5.27.
Wykorzystanie dwóch
klas wewnętrznych
Listing 5.50.
public class Outside {
class Inside {
private int liczba = 100;
public void f() {
System.out.println("Inside:f liczba = " + liczba);
}
}
public void g() {
Inside ins = new Inside();
ins.f();
ins.liczba = 200;
ins.f();
}
public static void main(String args[]) {
new Outside().g();
}
}
Zaskoczeniem może być, że przykład ten kompiluje się bez problemów, choć pole
liczba w klasie Inside jest polem prywatnym! Otóż okazuje się, że klasa zewnętrzna
ma pełny dostęp do składowych klasy wewnętrznej. Nie ma więc przeciwwskazań,
aby w klasie Outside odwołać się do pola liczba z klasy Inside. Ta zasada dotyczy
jednak tylko i wyłącznie klasy zewnętrznej. Pozostałe klasy będą w pełni respektowały
modyfikatory dostępu. Przykład ilustrujący to zagadnienie został zaprezentowany na
listingu 5.51.
Listing 5.51.
public class Outside {
public Inside ins = new Inside();
class Inside {
private int liczba = 100;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 243
W drugim pliku, o nazwie Main.java, należy umieścić kod klasy Main. Zawiera ona
metodę main, od której rozpocznie się wykonywanie kodu. W metodzie tej tworzymy no-
wy obiekt klasy Outside, następnie odwołujemy się do jego pola ins, czyli do obiektu
klasy Inside. Wywołujemy metodę f, po czym przypisujemy polu liczba wartość 100
i ponownie wywołujemy metodę f. Próba kompilacji spowoduje wystąpienie błędu
widocznego na rysunku 5.28. Skoro bowiem pole liczba z klasy Inside jest polem
prywatnym, można się do niego odwoływać jedynie z wnętrza klasy Inside lub klasy
zewnętrznej dla Inside (czyli Outside). Problem zniknie, jeśli pole liczba będzie polem
publicznym lub też dopiszemy metody publiczne operujące na nim.
b4ac40784ca6e05cededff5eda5a930f
b
244 Java. Praktyczny kurs
wewnętrzna, ang. local inner class)8. Ta ostatnia sytuacja jest rzadziej spotykana, nie-
mniej taka możliwość istnieje i warto wiedzieć, jak w praktyce traktować taki frag-
ment kodu. Jak zatem umieścić klasę wewnętrzną w treści metody klasy zewnętrznej?
Taka sytuacja została zaprezentowana na listingu 5.52.
Listing 5.52.
public class Outside {
public void f() {
class Inside {
/*…pola klasy Inside…*/
public void g() {
/*…instrukcje metody g…*/
}
/*…dalsze metody klasy Inside…*/
}
/*…dalsze instrukcje metody f…*/
}
/*…dalsze metody klasy Outside…*/
}
Są tu dwie klasy: Outside i Inside. Outside jest klasą zewnętrzną i zawiera bezargu-
mentową metodę o nazwie f. W tej metodzie została zdefiniowana klasa wewnętrzna
o nazwie Inside zawierająca bezargumentową metodę g. Trzeba zdawać sobie jednak
sprawę, że w takiej sytuacji widoczność klasy Inside została ograniczona wyłącznie
do metody f klasy Outside. Poza tą metodą jest ona nieznana i nie można się do niej
odwoływać. Zostało to zilustrowane w przykładzie z listingu 5.53.
Listing 5.53.
public class Outside {
//źle, w tym miejscu nie jest znana klasa Inside
//Inside ins1 = new Inside();
public void f() {
//źle, w tym miejscu nie jest znana klasa Inside
//Inside ins2 = new Inside();
class Inside {
public void g() {
}
}
//dobrze, klasa Inside została zdefiniowana
Inside ins3 = new Inside();
}
//źle, w tym miejscu nie jest znana klasa Inside
//Inside ins4 = new Inside();
}
W komentarze zostały ujęte instrukcje, których wykonanie skończy się błędem kom-
pilacji. Nie można utworzyć obiektów ins1, ins2 i ins4, gdyż w miejscach ich deklaracji
nie jest widoczna klasa Inside. Jedyne miejsce, w którym można się do niej bezpo-
8
W rzeczywistości klasa wewnętrzna może się znaleźć w dowolnym bloku wyróżnionym za pomocą
znaków nawiasu klamrowego, a więc również w pętli czy w instrukcji warunkowej.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 245
średnio odwoływać, znajduje się w metodzie f, za jej (klasy) definicją. Próby odwołań poza
tym zasięgiem nieuchronnie doprowadzą do powstania błędów kompilacji.
Listing 5.54.
public class Outside {
private int liczba = 100;
class Inside {
private void g(int argument) {
liczba = argument;
}
}
private void f(int argument) {
Inside ins = new Inside();
System.out.println("liczba = " + liczba);
ins.g(argument);
System.out.println("liczba = " + liczba);
}
public static void main(String args[]) {
new Outside().f(200);
}
}
Klasa zewnętrzna Outside zawiera prywatne pole o nazwie liczba, któremu w trakcie
deklaracji jest przypisywana wartość 100. W tej klasie jest również zdefiniowana klasa
wewnętrzna Inside, która zawiera prywatną metodę g przyjmującą jeden argument typu
int. Wartość tego argumentu jest przypisywana, uwaga… prywatnemu polu liczba
w klasie Outside. Jak widać, nie trzeba stosować żadnych specjalnych konstrukcji
programistycznych, dostęp do pola klasy zewnętrznej jest bezpośredni.
Klasa Outside zawiera też prywatną metodę f przyjmującą argument typu int. W tej
klasie tworzony jest obiekt klasy Inside, który jest przypisywany zmiennej ins. Następ-
nie na ekranie jest wyświetlana bieżąca wartość pola liczba oraz wywoływana jest
metoda g obiektu ins. Metodzie tej zostaje przekazany argument otrzymany przez
metodę f. Na zakończenie ponownie wyświetlana jest wartość pola liczba.
W metodzie main, od której rozpocznie się wykonywanie kodu, jest tworzony nowy obiekt
klasy Outside i wywoływana jest jego metoda f. Metodzie tej jest przekazywana wartość
200. Tym samym wartość ta zostaje ostatecznie przypisana (w metodzie g klasy Inside)
polu liczba. Na ekranie pojawi się więc widok zaprezentowany na rysunku 5.29.
b4ac40784ca6e05cededff5eda5a930f
b
246 Java. Praktyczny kurs
Rysunek 5.29.
Ilustracja odwołania
z klasy wewnętrznej
do prywatnego pola
klasy zewnętrznej
Ćwiczenie 28.1.
Napisz przykładową klasę A, w której znajdzie się wewnętrzna klasa B, a w tej z kolei
zostanie umieszczona wewnętrzna klasa C. Skompiluj otrzymany kod, zobacz, jak będą
wyglądały nazwy plików wynikowych.
Ćwiczenie 28.2.
Zmodyfikuj kod klas z listingu 5.51 tak, aby pole liczba klasy Inside pozostało polem
prywatnym, ale by była możliwa jego modyfikacja przez metody innych klas. Nie do-
konuj zmian w klasie Outside.
Ćwiczenie 28.3.
Zmodyfikuj kod klas z listingu 5.51 tak, aby pole liczba klasy Inside pozostało polem
prywatnym, ale by była możliwa jego modyfikacja. Nie dokonuj zmian w klasie Inside.
Ćwiczenie 28.4.
Umieść definicję klasy wewnętrznej w metodzie klasy zewnętrznej, w bloku instrukcji if.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 247
Co jednak zrobić, aby móc operować na obiekcie klasy wewnętrznej poza klasą ze-
wnętrzną? Przykładowo: co zrobić w sytuacji, kiedy klasą zewnętrzną jest Outside,
wewnętrzną Inside, a chcemy mieć dostęp do obiektów klasy Inside z zupełnie nie-
zależnej klasy Main?
Możliwe są dwa rozwiązania tego problemu. Otóż w klasie zewnętrznej można umieścić
metodę tworzącą i zwracającą obiekty klasy wewnętrznej lub też, za pomocą specjalnej
składni, bezpośrednio powołać do życia obiekt klasy wewnętrznej. Zacznijmy od pierw-
szego sposobu. Na listingu 5.55 są widoczne przykładowe klasy Inside i Outside.
Listing 5.55.
public class Outside {
class Inside {
public void g() {
System.out.println("Inside:g()");
}
}
public Inside getInside() {
return new Inside();
}
}
Listing 5.56.
public class Main {
public static void main(String args[]) {
Outside out = new Outside();
out.getInside().g();
}
}
Tworzymy nowy obiekt klasy Outside i przypisujemy go zmiennej o nazwie out. Następ-
nie wywołujemy metodę getInside tego obiektu, która zwraca odniesienie do obiektu
klasy Inside, oraz wywołujemy metodę g tego obiektu. Po skompilowaniu i urucho-
mieniu klasy Main pojawi się widok zaprezentowany na rysunku 5.30. W klasie Main
udało się więc wykonać bezpośrednią operację na obiekcie typu Inside.
Rysunek 5.30.
Wywołanie metody klasy
wewnętrznej z klasy
Main
b4ac40784ca6e05cededff5eda5a930f
b
248 Java. Praktyczny kurs
Przedstawiony sposób dostępu do obiektu klasy wewnętrznej jest jak najbardziej po-
prawny, ale ma jedną wadę. Otóż obiekt klasy Inside można wykorzystać tylko raz, tylko
do jednokrotnego wywołania metody g, gdyż nie jest przechowywana referencja do
niego. Tego typu wywołania spotyka się w praktyce, jeśli jednak ma być zachowany
dostęp do obiektu zwróconego przez metodę getInside, trzeba postąpić inaczej. Należy
oczywiście zadeklarować zmienną, w której zostanie zapisana referencja.
Pytanie brzmi: jakiego typu będzie ta zmienna? Odpowiedź, która nasuwa się w pierw-
szej chwili, brzmi: zmienna powinna być typu Inside. Jeśli jednak spróbujemy w klasie
Main dokonać przykładowej deklaracji w postaci:
Inside ins;
nie osiągniemy zamierzonego celu. Kompilator nie zna osobnej klasy o nazwie Inside
i zgłosi błąd. Klasa wewnętrzna nie może istnieć samodzielnie, bez zewnętrznej. Jest to
również odzwierciedlone w deklaracji zmiennych klas wewnętrznych. Deklaracja taka
powinna wyglądać następująco:
klasa_zewnętrzna.klasa_wewnętrzna nazwa_zmiennej;
Czyli prawidłowa deklaracja zmiennej klasy Inside w klasie Main powinna mieć postać:
Outside.Inside ins;
Przykładowa klasa Main wykorzystująca taką deklarację jest widoczna na listingu 5.57.
Listing 5.57.
public class Main {
public static void main(String args[]) {
Outside out = new Outside();
Outside.Inside ins;
ins = out.getInside();
ins.g();
ins.g();
}
}
Tworzymy nowy obiekt klasy Outside i przypisujemy go zmiennej out. Następnie dekla-
rujemy zmienną klasy Inside, wykorzystując przedstawioną przed chwilą konstrukcję.
Tę zmienną inicjujemy, wywołując metodę getInside z klasy Outside, która zwraca
obiekt klasy Inside. W ten sposób zachowujemy referencję do obiektu, którą można dalej
dowolnie wykorzystywać. W tym przypadku dwukrotnie wywoływana jest metoda g.
Listing 5.58.
public class Outside {
class Inside {
public void g() {
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 249
System.out.println("Inside:g()");
}
}
}
Przede wszystkim trzeba sobie uświadomić, że obiekt klasy wewnętrznej nie może
istnieć, o ile nie istnieje również obiekt klasy zewnętrznej10. Zawsze więc najpierw
tworzymy obiekt klasy zewnętrznej i — dopiero korzystając z niego — możemy po-
wołać do życia obiekt klasy wewnętrznej. Powinniśmy zastosować w takiej sytuacji
ciąg instrukcji o następującej postaci:
klasa_zewnętrzna obiekt_zew = new klasa_zewnętrzna();
klasa_zewnętrzna.klasa_wewnętrzna obiekt_wew = obiekt_zew.new klasa_wewnętrzna();
Przy założeniu, że klasy Outside i Inside są takie jak na listingu 5.58, instrukcje te
wyglądałyby następująco:
Outside out = new Outside();
Outside.Inside ins = out.new Inside();
Po ich wykonaniu zmienna ins będzie wskazywała na nowy obiekt klasy wewnętrznej
Inside, na którym będzie można wykonywać wszelkie dozwolone operacje, np. wywoły-
wać metody, tak jak zostało to przedstawione na listingu 5.59.
Listing 5.59.
public class Main {
public static void main(String args[]) {
Outside out = new Outside();
Outside.Inside ins = out.new Inside();
ins.g();
}
}
10
Istnieje jednak specyficzny typ klas wewnętrznych, dla którego ta zasada nie obowiązuje. Zostanie on
omówiony w lekcji 30.
b4ac40784ca6e05cededff5eda5a930f
b
250 Java. Praktyczny kurs
Listing 5.60.
package pakiet_klas;
import pakiet_klas.*;
Klasa Outside jest klasą publiczną, która została tym razem umieszczona w pakiecie
o nazwie pakiet_klas. W pliku z klasą Main umieszczona została zatem dyrektywa import,
dzięki której będzie można odwoływać się do klasy Outside. Nie ma więc żadnego
problemu z utworzeniem obiektu klasy Outside, tak jak dzieje się to w pierwszej instrukcji
metody main w klasie Main. Zwróćmy jednak uwagę, że klasa Inside, która jest we-
wnętrzna dla Outside, to klasa pakietowa, w związku z tym nie ma możliwości od-
woływania się do niej z klasy Main. Dlatego też kolejna instrukcja metody main (two-
rząca obiekt klasy Inside) spowoduje powstanie błędów kompilacji.
Gdyby zatem istniała konieczność odwołania się w klasie Main do klasy Inside, czyli uzy-
skania dostępu do klasy wewnętrznej z klasy niezależnej, klasa wewnętrzna musiałaby
mieć dostęp publiczny. W takiej sytuacji klasa Inside powinna mieć postać:
public class Inside {
public void g() {
System.out.println("Inside:g()");
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 251
pakietowe,
publiczne,
prywatne,
chronione
i należy je traktować tak jak składowe wymienionych typów. Zatem aby całkowicie za-
bronić dostępu do klasy wewnętrznej (a jest to stosunkowo często spotykana sytuacja),
należy zrobić z niej klasę prywatną, tak jak zostało to przedstawione w przykładzie na
listingu 5.61.
Listing 5.61.
public class Outside {
private class Inside {
public void g() {
System.out.println("Inside:g()");
}
}
public Inside getInside() {
return new Inside();
}
}
Klasa wewnętrzna Inside jest teraz klasą prywatną. Oznacza to, że nie będzie miała do
niej dostępu żadna inna klasa oprócz Outside. W klasie Outside została natomiast zdefi-
niowana metoda getInside zwracająca nowy obiekt klasy Inside. Co w takiej sytuacji
dzieje się w klasie Main? Otóż można oczywiście utworzyć obiekt klasy Outside,
gdyż jest ona klasą publiczną, wszelkie próby odwołania do klasy Inside skończą się
natomiast niepowodzeniem. Nie uda się ani utworzyć obiektu Inside, ani pobrać re-
ferencji do niego przy użyciu metody getInside z jednoczesnym wywołaniem metody g.
Istnieje natomiast możliwość wywołania samej metody getInside. Ponieważ jednak
na tak zwróconej referencji nie można wykonać żadnej operacji, nie ma to praktycznego
znaczenia11.
11
Chyba że konstruktor klasy Inside wykonywałby jakieś specjalne operacje, co jednak wydaje się
sytuacją czysto hipotetyczną.
b4ac40784ca6e05cededff5eda5a930f
b
252 Java. Praktyczny kurs
Warto zwrócić uwagę na jeszcze jedną ciekawą kwestię. Otóż metoda g z klasy Inside
jest publiczna, teoretycznie dostępna dla wszystkich innych klas. Ponieważ jednak sama
klasa Inside jest klasą wewnętrzną prywatną, metody g i tak nie można wywołać poza
klasami Inside i Outside.
Listing 5.62.
public class Outside {
public class Inside {
public void g() {
System.out.println("Inside:g()");
}
}
public Inside getInside() {
return new Inside();
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 253
Przed chwilą padło stwierdzenie, że klasę potomną od klasy zewnętrznej można trakto-
wać jako klasę zewnętrzną dla klasy wewnętrznej. Brzmi to wyjątkowo zawile, ale
chodzi o to, że klasa ExtendedOutside z listingu 5.62 jest traktowana jako zewnętrzna
dla klasy Inside. Rzeczywiście tak jest, trzeba jednak wziąć w tym momencie pod
uwagę reguły dziedziczenia. Jeśli bowiem klasa Inside będzie klasą pakietową, pu-
bliczną lub chronioną, bez problemu będzie można odwoływać się do niej w klasie
ExtendedOutside. Gdyby jednak Inside była klasą prywatną, takie odwołania okaza-
łyby się niestety niemożliwe. Zilustrowano to w przykładzie z listingu 5.63.
Listing 5.63.
public class Outside {
private class Inside {
}
}
Klasa Inside jest w tym przykładzie klasą prywatną, czyli można ją traktować jako pry-
watną składową klasy Outside. W związku z tym klasa ExtendedOutside nie ma do niej
dostępu. Jeśli usuniemy komentarz z linii:
//Inside ins = new Inside();
przekonamy się o tym naocznie, gdyż kompilator zgłosi serię błędów.
b4ac40784ca6e05cededff5eda5a930f
b
254 Java. Praktyczny kurs
Ćwiczenie 29.1.
Napisz klasę o nazwie A zawierającą wewnętrzną klasę o nazwie B. W klasie A umieść
metodę zwracającą nowy obiekt klasy B.
Ćwiczenie 29.2.
Utwórz pakiet nowy_pakiet, w którym znajdą się dwie klasy: A, zewnętrzna o dostępie
publicznym, i B, wewnętrzna o dostępie pakietowym. Klasa A powinna zawierać jedno
prywatne pole klasy B o nazwie obiektB inicjowane w trakcie deklaracji, a B powinna
zawierać pole liczba typu int. W klasie A dopisz metody pozwalające na zapis i odczyt
pola liczba obiektu obiektB.
Ćwiczenie 29.3.
Napisz publiczną klasę o nazwie A zawierającą wewnętrzną klasę B o dostępie chronio-
nym. Z klasy A wyprowadź klasę pochodną ExtendedA zawierającą pole klasy B.
Ćwiczenie 29.4.
Napisz klasę Outside zawierającą klasę wewnętrzną Inside o dostępie publicznym.
Z Outside wyprowadź klasę ExtendedOutside zawierającą publiczną klasę wewnętrzną
o nazwie ExtendedInside.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 255
Powstanie wtedy obiekt typu nazwa_klasy, do którego będzie można odwoływać się
przez zmienną obiekt. Trzeba jednak pamiętać, że przez tę zmienną możliwe będzie
odwoływanie się jedynie do metod, które zostały wymuszone w deklaracji klasy poprzez
implementację interfejsu (lekcja 26.). Zostało to zilustrowane w przykładzie widocz-
nym na listingu 5.64.
Listing 5.64.
public interface PierwszyInterfejs {
public int getLiczba();
public void setLiczba(int liczba);
}
b4ac40784ca6e05cededff5eda5a930f
b
256 Java. Praktyczny kurs
Wykonywanie kodu rozpoczyna się od metody main. Jej kod zaczyna się od zadeklarowa-
nia zmiennej referencyjnej obj typu MojaKlasa i przypisania jej nowo utworzonego
obiektu klasy MojaKlasa. Następnie wywołujemy metodę setLiczba tego obiektu,
ustawiając wewnętrzne pole liczba na wartość 1. W kolejnym kroku za pomocą metody
getLiczba pobieramy wartość tego pola i przypisujemy ją zmiennej temp1, po czym
wywołujemy metodę setAndGet, przekazując w argumencie wartość 2. Wartość zwróconą
przez tę metodę przypisujemy zmiennej temp2.
Nie musimy jawnie umieszczać nazwy interfejsu, na który rzutujemy, przed nazwą
zmiennej (co zostało zaznaczone przez ujęcie jej w nawias klamrowy). Kompilator
„wie”, na który typ ma nastąpić rzutowanie. Ze względu na przejrzystość kodu można
jednak stosować rzutowanie jawne. W praktyce, przy założeniu, że dysponujemy zesta-
wem klas takim jak przedstawiony na listingu 5.64, możemy wykonać instrukcje dla
rzutowania jawnego:
MojaKlasa obj = new MojaKlasa();
PierwszyInterfejs pi = (PierwszyInterfejs) obj;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 257
Klasy anonimowe
Czy klasa może nie mieć nazwy? Wydaje się, że to niemożliwe, okazuje się jednak, że
Java dopuszcza tego typu sytuacje. Takie klasy nazywamy anonimowymi i są one często
stosowane w praktyce programistycznej. Można je wykorzystywać dzięki możliwości
rzutowania na typy klasowe bądź interfejsowe12. Budowę klasy anonimowej najlepiej
wyjaśnić na konkretnym przykładzie — został on zaprezentowany na listingu 5.65.
Listing 5.65.
public interface PierwszyInterfejs {
public int getLiczba();
public void setLiczba(int liczba);
}
Powstała więc klasa, która nie ma nazwy. Odniesienie do obiektu tej klasy zostało
przypisane zmiennej pi. Dostęp do obiektu jest możliwy jedynie poprzez interfejs
PierwszyInterfejs. Konsekwencją tego stanu rzeczy jest brak bezpośredniego dostępu do
jakichkolwiek składowych tej klasy innych niż zdefiniowane w interfejsie. Nie ma więc
12
Co prawda interfejs także jest klasą, ale można wprowadzić takie rozróżnienie.
b4ac40784ca6e05cededff5eda5a930f
b
258 Java. Praktyczny kurs
innej możliwości manipulacji wartością pola liczba jak tylko przez wykorzystanie
metod setLiczba oraz getLiczba. Nic nie da nawet zmiana modyfikatora dostępu pola
liczba z private na public.
Należy zwrócić uwagę na średnik znajdujący się za definicją klasy anonimowej (formal-
nie kończący instrukcję przypisania). Jest on niezbędny do prawidłowej kompilacji
kodu.
Jeśli chcemy, aby klasa anonimowa została wyprowadzona z konkretnej klasy bazowej,
musimy po operatorze new zamiast nazwy interfejsu użyć nazwy klasy. Taka sytuacja
została przedstawiona na listingu 5.66.
Listing 5.66.
public class MojaKlasa {
public void f() {
System.out.println("MojaKlasa:f()");
}
public static void main(String args[]) {
MojaKlasa obj1 = new MojaKlasa() {
public void f() {
System.out.println("Anonim:f()");
}
};
MojaKlasa obj2 = new MojaKlasa();
obj1.f();
obj2.f();
}
}
W tej chwili klasa anonimowa jest klasą pochodną od MojaKlasa, czyli dziedziczy po
niej. Sama konstrukcja tworząca klasę anonimową jest analogiczna do przedstawionej
w poprzednim przykładzie. Tym razem jednak możliwy jest dostęp do składowych tej
klasy! Prześledźmy to nieco dokładniej.
W klasie MojaKlasa znajduje się metoda f, której zadaniem jest wyświetlenie nazwy
klasy, w jakiej została zdefiniowana. Podobnie jest w klasie anonimowej. W metodzie
main tworzymy nowy obiekt klasy anonimowej i przypisujemy go (wykonywane jest
tu niejawne rzutowanie w górę) zmiennej obj1. Następnie tworzymy nowy obiekt klasy
MojaKlasa i przypisujemy go zmiennej obj2. W kolejnym kroku wywołujemy metody
f obiektów wskazywanych przez obj1 i obj2.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 259
Co zostanie wyświetlone na ekranie? Zmienna obj1 jest typu MojaKlasa, ale obiekt przez
nią wskazywany jest obiektem klasy anonimowej dziedziczącej po MojaKlasa. Po przy-
pomnieniu sobie materiału z lekcji 23. i 24. stanie się jasne, że wywołanie metody f
będzie wywołaniem polimorficznym, czyli zostanie wykonana metoda f z klasy anoni-
mowej. W drugim przypadku zmienna obj2 jest typu MojaKlasa i obiekt przez nią wska-
zywany jest również tego typu, zostanie więc oczywiście wykonana metoda f klasy
MojaKlasa. Ostatecznie na ekranie pojawi się widok zaprezentowany na rysunku 5.32.
Rysunek 5.32.
Polimorficzne
wywołanie metody
klasy anonimowej
Listing 5.67.
public class MojaKlasa {
private int liczba;
public MojaKlasa(int liczba) {
this.liczba = liczba;
}
/*
pola i metody klasy bazowej
*/
public static void main(String args[]) {
MojaKlasa obj1 = new MojaKlasa(1) {
/*
pola i metody klasy anonimowej
*/
};
/*
dalsze instrukcje metody main…
*/
}
}
b4ac40784ca6e05cededff5eda5a930f
b
260 Java. Praktyczny kurs
Klasy statyczne
Dzięki informacjom z lekcji 29. wiadomo, że obiekt klasy wewnętrznej nie może istnieć
bez obiektu klasy zewnętrznej. Jednym z powodów jest to, że klasa wewnętrzna prze-
chowuje odwołanie do zawierającego ją obiektu klasy zewnętrznej i dzięki temu ma
dostęp do jego składowych (por. lekcja 28.). Okazuje się jednak, że istnieje pewien
specjalny typ klas wewnętrznych, których obiekty mogą istnieć nawet wtedy, kiedy nie
istnieje obiekt klasy zewnętrznej. Są to wewnętrzne klasy statyczne nazywane klasami
zagnieżdżonymi (lub statycznymi klasami zagnieżdżonymi). Przykład takiej klasy
jest widoczny na listingu 5.68.
Listing 5.68.
public class Zewnetrzna {
public static class Wewnetrzna {
public void f() {
System.out.println("Wewnetrzna:f()");
}
}
public static void main(String args[]) {
Wewnetrzna wewnetrzna = new Wewnetrzna();
wewnetrzna.f();
}
}
Konsekwencją tego stanu (statyczności klasy wewnętrznej) jest brak powiązania klasy
wewnętrznej z zewnętrzną. Nie można zatem odwoływać się do pól i metod klasy we-
wnętrznej z klasy zewnętrznej, nie jest też możliwa sytuacja odwrotna. Odwołanie może
nastąpić wyłącznie poprzez obiekt, tak jak zostało to pokazane w przykładzie z listingu
5.68 (wywołanie metody f). Inne próby odwołań skończą się błędami kompilacji. Prze-
konamy się o tym, jeśli spróbujemy skompilować kod z listingu 5.69. Zamiast spodzie-
wanych plików wynikowych pojawi się seria komunikatów o błędach (rysunek 5.33).
Listing 5.69.
public class Zewnetrzna {
public int zewLiczba;
public static class Wewnetrzna {
public int wewLiczba;
public void f() {
//nieprawidłowo, nie ma dostępu do pola
//zewLiczba klasy Zewnetrzna
zewLiczba = 100;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 261
wewLiczba = 100;
System.out.println("Wewnetrzna:f()");
}
}
public void g() {
zewLiczba = 100;
b4ac40784ca6e05cededff5eda5a930f
b
262 Java. Praktyczny kurs
Listing 5.70.
public class Przyprostokatne{
private double p1;
private double p2;
Przyprostokatne(double p1, double p2){
setPrzyprostokatne(p1, p2);
}
public void setPrzyprostokatne(double p1, double p2){
this.p1 = p1;
this.p2 = p2;
}
public double przeciwprostokatna(){
return Math.sqrt(p1 * p1 + p2 * p2);
}
}
Pola p1 i p2 są prywatne, a ich wartości mogą być zmieniane dzięki metodzie set
Przyprostokatne. W klasie jest tylko jeden dwuargumentowy konstruktor, w którym
ta metoda jest wywoływana. Dzięki temu tworzenie obiektów klasy Przyprostokatne
będzie możliwe wyłącznie po podaniu długości obu przyprostokątnych — nie da się
zatem zapomnieć o podaniu jednej z wartości. W metodzie przeciwprostokatna za
pomocą powszechnie znanego wzoru (pierwiastek z sumy kwadratów przyprostokąt-
nych daje przeciwprostokątną) wyliczana jest długość przeciwprostokątnej. Pierwiastek
kwadratowy uzyskiwany jest dzięki wywołaniu statycznej metody sqrt z klasy Math
(tak jak miało to miejsce w przykładach z rozdziału 2.), a zamiast podnoszenia do
drugiej potęgi zostało użyte mnożenie.
Listing 5.71.
public class Main {
public static void main(String args[]) {
Przyprostokatne p = new Przyprostokatne(3, 4);
System.out.println(p.przeciwprostokatna());
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 263
A zatem metoda przeciwprostokatna ma być napisana tak, aby przy jej wywoływaniu
dało się przekazać jej kod wykonywalny operujący na wartościach pól p1 i p2. Same
wartości pól mają jednak pozostać niezmienione — nie chcemy zmieniać parametrów
trójkąta, chcemy jedynie dowiedzieć się, co będzie się teoretycznie działo przy różnych
zmianach długości boków. Jak jednak przekazać kod wykonywalny, czyli w rzeczy-
wistości treść innej metody? Nie ma możliwości, by zrobić to bezpośrednio. Moglibyśmy
jednak przekazać obiekt zawierający metodę wykonującą żądane przez nas obliczenia.
Utwórzmy więc przeciążoną wersję metody przeciwprostokatna przyjmującą argument
będący obiektem typu Przeksztalcenia, który zawiera metodę o nazwie przeksztalc.
Ta metoda będzie przyjmowała jeden argument typu double i zwracała jego wartość
po pewnym przekształceniu. Metoda przeciwprostokatna przyjmie wtedy postać
przedstawioną na listingu 5.72.
Listing 5.72.
public double przeciwprostokatna(Przeksztalcenia prz){
double p1 = prz.przeksztalc(this.p1);
double p2 = prz.przeksztalc(this.p2);
return Math.sqrt(p1 * p1 + p2 * p2);
}
Listing 5.73.
public interface Przeksztalcenia{
public double przeksztalc(double arg);
}
b4ac40784ca6e05cededff5eda5a930f
b
264 Java. Praktyczny kurs
Taki interfejs, który zawiera deklarację tylko jednej metody abstrakcyjnej, nazywamy
interfejsem funkcjonalnym (ang. functional interface). Powinno już być teraz widać,
że praktyczne wykorzystanie klasy Przyprostokatne i metody przeciwprostokatna będzie
wymagało użycia obiektu DOWOLNEJ klasy implementującej interfejs Przeksztalcenia
(taka klasa mogłaby też implementować dowolnie wiele innych interfejsów). Może ona
być publiczna, pakietowa, wewnętrzna. Zatem przykładowy program mógłby wyglądać
tak jak na listingu 5.74.
Listing 5.74.
class Zwiekszaj implements Przeksztalcenia{
public double przeksztalc(double arg){
return arg + 5;
}
}
Przy takim zapisie pojawia się jednak pewna niedogodność. Kod miał być przecież
możliwie uniwersalny, tak aby przed obliczeniem przeciwprostokątnej można było
łatwo zmieniać w różny sposób długości przyprostokątnych. Tymczasem użycie typo-
wych konstrukcji wymaga dla każdego rodzaju obliczeń pisania osobnych klas (np. klasa
zmniejszająca, mnożąca, dzieląca, dodająca itd.). To na pewno nie byłoby wygodne.
Od czego jednak są klasy anonimowe. Przecież klasa jest tak naprawdę jedynie opa-
kowaniem dla metody przeksztalc. W większość przypadków nie będzie więc potrzeby
jej nazywania i tworzenia osobnych konstrukcji. Nic nie stoi na przeszkodzie, aby me-
todzie przeciwprostokatna przekazywać obiekty klasy anonimowej implementującej
interfejs Przeksztalcenia. Wyglądałoby to wtedy tak jak na listingu 5.75.
Listing 5.75.
public class Main {
public static void main(String args[]) {
Przyprostokatne p = new Przyprostokatne(3, 4);
System.out.println(p.przeciwprostokatna(
new Przeksztalcenia(){
public double przeksztalc(double arg){
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 265
return arg - 1;
}
})
);
}
}
Przedstawiony zapis może wydawać się niezbyt czytelny, ale to wyłącznie kwestia przy-
zwyczajenia. To rodzaj wywołania kaskadowego (charakterystycznego dla stylu pro-
gramowania funkcyjnego), w którym unika się tworzenia zbędnych zmiennych pomoc-
niczych (dodatkowo można byłoby pozbyć się zmiennej p, co może być dobrym
ćwiczeniem do samodzielnego wykonania). Metoda println otrzymała w postaci argu-
mentu wartość zwróconą przez wywołanie metody przeciwprostokatna obiektu wska-
zywanego przez zmienną p.
Listing 5.76.
public class Main {
public static void main(String args[]) {
Przyprostokatne p = new Przyprostokatne(3, 4);
b4ac40784ca6e05cededff5eda5a930f
b
266 Java. Praktyczny kurs
wyrażeń lambda (ang. lambda expressions). Takie wyrażenie można przedstawić w sche-
matycznej postaci:
(typ1 arg1, typ2 arg2, … , typN argN) -> {treść metody}
Stąd wniosek, że cały program korzystający z tej uproszczonej składni miałby postać
przedstawioną na listingu 5.77.
Listing 5.77.
public class Main {
public static void main(String args[]) {
Przyprostokatne p = new Przyprostokatne(3, 4);
System.out.println(p.przeciwprostokatna(
(double arg) -> {
return arg - 1;
}
);
}
}
Ćwiczenie 30.1.
Do klasy MojaKlasa z listingu 5.64 dopisz metodę toString. Użyj jej do wyświetlania
wartości pola liczba po każdej wykonanej operacji. Czy będzie można skorzystać z tej
metody w przypadku obiektu pi?
Ćwiczenie 30.2.
Napisz klasę, która będzie zawierała metodę getI1 zwracającą nowy obiekt anonimowej
klasy implementującej interfejs o nazwie I1.
Ćwiczenie 30.3.
Napisz klasę anonimową, która będzie klasą pochodną od klasy niemającej konstruktora
domyślnego. Klasa bazowa powinna natomiast zawierać dwa konstruktory: jeden przyj-
mujący argument typu int oraz drugi przyjmujący argument typu double. Utwórz dwa
obiekty klasy anonimowej, korzystając z obu konstruktorów.
Ćwiczenie 30.4.
Napisz statyczną klasę zagnieżdżoną zawierającą metodę statyczną o nazwie f wyświe-
tlającą dowolny napis na ekranie. Wywołaj tę metodę, nie tworząc żadnego obiektu.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5. Programowanie obiektowe. Część II 267
Ćwiczenie 30.5.
Napisz taką wersję programu z listingu 5.75, aby nie została w niej użyta żadna zmienna.
Dodatkowo wykonaj to ćwiczenie, stosując wyrażenie lambda.
Ćwiczenie 30.6.
Przygotuj taką wersję klasy Przyprostokatne, aby można było osobno decydować o prze-
kształceniu wykonywanym na każdej z przyprostokątnych (np. pierwszą zwiększyć, a drugą
zmniejszyć). Czy trzeba będzie wprowadzać zmiany do interfejsu Przeksztalcenia?
Napisz program testujący nowe możliwości.
b4ac40784ca6e05cededff5eda5a930f
b
268 Java. Praktyczny kurs
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.
System wejścia-wyjścia
Do pisania aplikacji w Javie niezbędna jest znajomość przynajmniej podstaw systemu
wejścia-wyjścia. Tej tematyce jest poświęcony ten rozdział. W Javie operacje wej-
ścia-wyjścia są wykonywane za pomocą strumieni. Strumień to abstrakcyjny ciąg da-
nych, który działa — mówiąc w uproszczeniu — w taki sposób, że dane wprowadzone
na jednym jego końcu pojawiają się na drugim końcu. Strumienie dzielą się na wejściowe
i wyjściowe. Strumienie wyjściowe mają początek w aplikacji i koniec w innym urzą-
dzeniu, np. na ekranie czy w pliku, umożliwiają zatem wyprowadzanie danych z pro-
gramu. Przykładowo standardowy strumień wyjściowy jest reprezentowany przez obiekt
System.out, a wykonanie metody println tego obiektu powoduje, że dane domyślnie zo-
stają wysłane na ekran. Strumienie wejściowe działają odwrotnie: ich początek znajduje
się poza aplikacją (może to być np. klawiatura albo plik dyskowy), a koniec w aplikacji,
umożliwiają zatem wprowadzanie danych. Co więcej, strumienie umożliwiają też ko-
munikację między obiektami w obrębie jednej aplikacji. W tym rozdziale zostanie
jednak omówiona tylko komunikacja aplikacji ze światem zewnętrznym.
b4ac40784ca6e05cededff5eda5a930f
b
270 Java. Praktyczny kurs
Listing 6.1.
import java.io.*;
1
Oczywiście reprezentowanie zakresu od 0 do 255 byłoby możliwe również za pomocą typów short i long.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 271
Pierwszym krokiem jest zaimportowanie pakietu java.io (lekcja 17.), znajduje się w nim
bowiem definicja wyjątku IOException, który musimy przechwycić. W metodzie main
za pomocą metody System.out.println wyświetlamy tekst z prośbą o wprowadzenie
dowolnego znaku z klawiatury, a następnie wykonujemy instrukcję:
char c = (char) System.in.read();
Wywołujemy zatem metodę read obiektu in zawartego w klasie System2. Obiekt ten jest
klasy InputStream (typem obiektu jest InputStream) i reprezentuje standardowy stru-
mień wejściowy. W typowym przypadku jest on powiązany z klawiaturą, więc z tego
strumienia będą odczytywane dane wprowadzane przez użytkownika z klawiatury.
Metoda read zwraca wartość typu int, dokonujemy zatem konwersji na typ char, tak
aby wyświetlić na ekranie wprowadzony znak, i przypisujemy go zmiennej c. Następnie
wyświetlamy zawartość tej zmiennej za pomocą instrukcji:
System.out.println(c);
Aby wyświetlić kod znaku, należy ponownie dokonać konwersji. Zmienną c typu char
konwertujemy na typ int, tak aby metoda println potraktowała ją jako liczbę, a nie jako
znak. Ponieważ metoda read może spowodować powstanie wyjątku IOException,
wszystkie instrukcje zostały ujęte w blok try, który zapobiegnie niekontrolowanemu
zakończeniu programu (por. lekcje z rozdziału 4.). Przykładowy efekt działania programu
z listingu 6.1 jest widoczny na rysunku 6.1.
Rysunek 6.1.
Przykładowy efekt
działania programu
z listingu 6.1
Trzeba zwrócić uwagę, że w ten sposób tak naprawdę odczytywany jest ze strumienia
nie jeden znak, ale jeden bajt. A te pojęcia nie są tożsame. Jeden znak może zawierać
w zależności od standardu kodowania od jednego do kilku bajtów.
Wczytywanie tekstu
Wiadomo już, jak odczytać jeden bajt, co jednak zrobić, aby wprowadzić całą linię tekstu?
Przecież taka sytuacja jest o wiele częstsza. Można oczywiście odczytywać pojedyn-
cze znaki w pętli tak długo, aż zostanie osiągnięty znak końca linii, oraz połączyć je
w obiekt typu String. Najlepiej byłoby wręcz wyprowadzić z klasy InputStream klasę
pochodną, która zawierałaby metodę np. o nazwie readLine, wykonującą taką czynność.
Trzeba byłoby przy tym pamiętać o odpowiedniej obsłudze standardów kodowania
znaków. Na szczęście w JDK został zdefiniowany cały zestaw klas operujących na
strumieniach wejściowych. Zamiast więc powtarzać coś, co zostało już zrobione, najlepiej
po prostu z nich skorzystać.
2
Obiekt in jest polem finalnym i statycznym klasy System.
b4ac40784ca6e05cededff5eda5a930f
b
272 Java. Praktyczny kurs
Listing 6.2.
import java.io.*;
Znaczenie będzie takie samo, jednak w drugim przypadku zostanie utworzona zupełnie
niepotrzebnie dodatkowa zmienna isr typu InputStreamReader. To, który ze sposobów
jest czytelniejszy i zostanie zastosowany, zależy jednak od indywidualnych preferencji
programisty. Z reguły stosuje się sposób pierwszy.
Kiedy obiekt klasy BufferedReader jest już utworzony, można wykorzystać metodę
readLine, która zwróci w postaci obiektu typu String (klasa String reprezentuje ciągi
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 273
znaków) linię tekstu wprowadzoną przez użytkownika. Linia tekstu jest rozumiana
jako ciąg znaków wprowadzony aż do naciśnięcia klawisza Enter (dokładniej: aż do
osiągnięcia znaku końca wiersza w strumieniu). Wynika z tego, że instrukcja:
String line = brIn.readLine();
utworzy zmienną line typu String i przypisze jej obiekt (referencję do obiektu) zwrócony
przez metodę readLine obiektu brIn. Działanie programu będzie zatem następujące:
po uruchomieniu zostanie wyświetlona prośba o wprowadzenie linii tekstu, następnie
linia ta zostanie odczytana i przypisana zmiennej line, a potem zawartość tej zmiennej
zostanie wyświetlona na ekranie. Przykład wykonania tego programu pokazano na ry-
sunku 6.2.
Rysunek 6.2.
Wczytanie linii tekstu
za pomocą obiektu
klasy BufferedReader
Skoro wiadomo już, jak wczytać wiersz tekstu z klawiatury, spróbujmy napisać pro-
gram, którego zadaniem będzie wczytywanie kolejnych linii ze standardowego wej-
ścia tak długo, aż zostanie odczytany ciąg znaków quit. Zadanie wydaje się banalne,
wystarczy przecież do kodu z listingu 6.2 dodać pętlę while, która będzie sprawdzała, czy
zmienna line zawiera napis quit. W pierwszym odruchu napiszemy zapewne pętlę,
która będzie wyglądać następująco (przy założeniu, że wcześniej został prawidłowo
zainicjowany obiekt brIn):
String line = "";
while(line != "quit"){
line = brIn.readLine();
System.out.println("Wprowadzona linia to: " + line);
}
System.out.println("Koniec wczytywania danych.");
Wszystko to wygląda bardzo poprawnie, ma tylko jedną wadę — nie zadziała zgodnie
z założeniami. Aby się o tym przekonać, wystarczy uruchomić program z listingu 6.3,
następnie wpisać kilka linii tekstu, a po nich ciąg znaków quit. Teoretycznie program po-
winien zakończyć działanie, tymczasem działa nadal, co jest widoczne na rysunku 6.3.
b4ac40784ca6e05cededff5eda5a930f
b
274 Java. Praktyczny kurs
Rysunek 6.3.
Błąd w pętli while
uniemożliwia
zakończenie programu
Listing 6.3.
import java.io.*;
Co się dzieje, gdzie jest błąd? Podejrzenie powinno najpierw paść na warunek zakoń-
czenia pętli while. On rzeczywiście jest nieprawidłowy, choć wydaje się, że instrukcja
ta jest poprawna. Zwróćmy jednak uwagę na to, co tak naprawdę jest w tym warunku
porównywane. Miał być porównany ciąg znaków zapisany w zmiennej line z ciągiem
znaków quit. Tymczasem po umieszczeniu w kodzie sekwencji "quit" powstał nie-
nazwany obiekt klasy String faktycznie zawierający ciąg znaków quit, a w miejsce
napisu "quit" została wstawiona referencja do tego obiektu. Jak wiadomo z wcześniej-
szych lekcji (lekcja 13.), zmienna line jest także referencją do obiektu klasy String.
Instrukcja porównania line != "quit" porównuje zatem ze sobą dwie REFERENCJE,
które w tym przypadku muszą być różne. W związku z tym nie dochodzi tu do porów-
nania zawartości obiektów. Dlatego program nie może działać poprawnie i wpada w nie-
skończoną pętlę (aby zakończyć jego działanie, trzeba wcisnąć na klawiaturze kombi-
nację klawiszy Ctrl+C).
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 275
Warto pamiętać o tym podczas tworzenia własnych klas, by w razie potrzeby dopisać
własną wersję tej metody.
Jak zastosować tę metodę w praktyce? Otóż zwraca ona wartość true, kiedy obiekty
są takie same (czyli ich zawartość jest taka sama), lub false, kiedy obiekty są różne.
W przypadku obiektów typu String wartość true zostanie zwrócona, jeśli ciąg zna-
ków zapisany w jednym z nich jest taki sam jak ciąg znaków zapisany w drugim. Jeżeli
np. istnieje zmienna zadeklarowana jako:
String napis1 = "test";
to wynikiem operacji:
napis1.equals("test");
Powróćmy teraz do pętli while. Jako że znane są nam już właściwości metody equals,
w pierwszej chwili na pewno na myśl przyjdzie nam warunek:
while(!line.equals("quit")){
/*instrukcje pętli while*/
}
b4ac40784ca6e05cededff5eda5a930f
b
276 Java. Praktyczny kurs
a potem w warunku pętli while następuje próba wywołania metody equals obiektu
wskazywanego przez line. Tymczasem line ma wartość null i nie wskazuje na żaden
obiekt. W takiej sytuacji musi zostać wygenerowany wyjątek NullPointerException.
Jak temu zapobiec? Możliwości są dwie: albo dodamy w pętli warunek sprawdzający,
czy line jest równe null, a jeśli tak, to przerwiemy pętlę np. instrukcją break, albo też
zmodyfikujemy sam warunek pętli. Ta druga możliwość jest lepsza, gdyż nie powoduje
wykonywania dodatkowego kodu, a jednocześnie otrzymujemy ciekawą konstrukcję.
Poprawiony warunek powinien wyglądać następująco:
while(!"quit".equals(line)){
/*instrukcje pętli while*/
}
Początkowo tego typu zapis budzi zdziwienie: jak można wywoływać jakąkolwiek
metodę na rzecz ciągu znaków? Wystarczy jednak sobie przypomnieć stwierdzenie,
które pojawiło się kilka akapitów wyżej, otóż literał3 "quit" w rzeczywistości powoduje
powstanie obiektu klasy String i podstawianie w jego (literału) miejsce referencji do
tego obiektu. Skoro tak, można wywołać metodę equals. To właśnie dzięki takiej kon-
strukcji unikamy wyjątku NullPointerException, gdyż nawet jeśli line będzie miało
wartość null, to metoda equals po prostu zwróci wartość false, bo null na pewno jest
różne od ciągu znaków quit. Ostatecznie pełny prawidłowy kod będzie miał postać
przedstawioną na listingu 6.4. Przykład działania tego kodu został z kolei zaprezento-
wany na rysunku 6.5.
Listing 6.4.
import java.io.*;
3
Czyli inaczej stała znakowa (napisowa), stały ciąg znaków umieszczony w kodzie programu.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 277
Rysunek 6.5.
Wprowadzanie w pętli
kolejnych linii tekstu
Wprowadzanie liczb
Potrafimy już odczytywać w aplikacji wiersze tekstu wprowadzane z klawiatury, równie
ważną umiejętnością jest wprowadzanie liczb. Jak to zrobić? Trzeba sobie uzmysłowić, że
z klawiatury zawsze wprowadzany jest tekst. Jeśli próbujemy wprowadzić do aplika-
cji wartość 123, to w rzeczywistości wprowadzimy trzy znaki, "1", "2" i "3" o kodach
ASCII 61, 62, 63. Mogą one zostać przedstawione w postaci ciągu "123", ale to dopie-
ro aplikacja musi przetworzyć ten ciąg na wartość 123. Takiej konwersji w przypadku
wartości całkowitej można dokonać np. za pomocą metody parseInt z klasy Integer.
Jest to metoda statyczna, można ją więc wywołać, nie tworząc obiektu klasy Integer4.
Przykładowe wywołanie może wyglądać następująco:
int liczba = Integer.parseInt(ciąg_znaków);
Zmiennej liczba zostanie przypisana wartość typu int zawarta w ciągu znaków ciąg_
znaków. Gdyby ciąg przekazany jako argument metody parseInt nie zawierał popraw-
nej wartości całkowitej, zostałby wygenerowany wyjątek NumberFormatException.
Aby zatem wprowadzić do aplikacji wartość całkowitą, można odczytać wiersz tekstu,
korzystając z metody readLine, a następnie wywołać metodę parseInt. Ten właśnie spo-
sób został wykorzystany w programie z listingu 6.5. Zadaniem tego programu jest wczy-
tanie liczby całkowitej oraz wyświetlenie wyniku mnożenia tej liczby przez wartość 2.
Listing 6.5.
import java.io.*;
4
Jak widać, metody statyczne, które były przedmiotem lekcji 18., przydają się w praktyce.
b4ac40784ca6e05cededff5eda5a930f
b
278 Java. Praktyczny kurs
catch(IOException e){
System.out.println("Błąd podczas odczytu strumienia.");
return;
}
int liczba;
try{
liczba = Integer.parseInt(line);
}
catch(NumberFormatException e){
System.out.print("Podana wartość nie jest liczbą całkowitą.");
return;
}
System.out.print(liczba + " * 2 = " + liczba * 2);
}
}
Kod zaczyna się od utworzenia obiektu brIn klasy BufferedReader powiązanego poprzez
obiekt pośredniczący klasy InputStreamReader ze standardowym strumieniem wejścio-
wym. Stosujemy technikę opisaną przy omawianiu przykładu z listingu 6.2. Następnie
wyświetlamy prośbę o wprowadzenie liczby całkowitej oraz odczytujemy wiersz tek-
stu za pomocą metody readLine obiektu brIn, nie zapominając o przechwyceniu ewentu-
alnego wyjątku IOException. W przypadku wystąpienia takiego wyjątku wyświetlamy sto-
sowną informację na ekranie oraz kończymy wykonywanie programu. Warto zwrócić
uwagę, że używana jest w tym celu instrukcja return bez żadnych parametrów, co ozna-
cza wyjście z funkcji main (bez zwracania wartości), a tym samym zakończenie dzia-
łania aplikacji.
Jeśli odczyt wiersza tekstu się powiedzie, zostanie on zapisany w zmiennej line. Dekla-
rujemy więc dalej zmienną liczba typu int oraz przypisujemy jej wynik działania metody
parseInt z klasy Integer. Metodzie przekazujemy ciąg znaków zapisany w zmiennej
line. Jeśli wprowadzony przez użytkownika ciąg znaków nie reprezentuje poprawnej
wartości liczbowej, wygenerowany zostaje wyjątek NumberFormatException. W takim wy-
padku wyświetlamy komunikat o tym fakcie oraz kończymy działanie funkcji main, a tym
samym programu, wywołując instrukcję return. Jeśli jednak konwersja tekstu na liczbę
powiedzie się, odpowiednia wartość zostanie zapisana w zmiennej liczba, można za-
tem wykonać mnożenie liczba * 2 i wyświetlić wartość wynikającą z tego mnożenia
na ekranie. Przykładowy wynik działania programu jest widoczny na rysunku 6.6.
Rysunek 6.6.
Przykładowy wynik
działania programu
z listingu 6.5
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 279
Ćwiczenie 31.1.
Napisz klasę Main, która będzie zawierała metodę readLine. Zadaniem tej metody będzie
zwrócenie wprowadzonej przez użytkownika linii tekstu. Nie używaj do odczytu danych
innych metod niż System.in.read(). Przetestuj działanie własnej metody readLine.
Ćwiczenie 31.2.
Zmodyfikuj kod z listingu 6.3 tak, aby warunek pętli while miał postać while(!line.
equals("quit")), ale by nie występował błąd NullPointerException, kiedy osiągnięty
zostanie koniec strumienia (np. po wciśnięciu kombinacji klawiszy Ctrl+C).
Ćwiczenie 31.3.
Napisz program (analogiczny do przedstawionego na listingu 6.5), który będzie umożli-
wiał wprowadzenie liczby zmiennoprzecinkowej.
Ćwiczenie 31.4.
Napisz klasę pochodną od BufferedReader, która będzie zawierała metody getInt oraz
getDouble odczytujące ze strumienia liczby całkowite oraz zmiennopozycyjne (zmien-
noprzecinkowe).
Ćwiczenie 31.5.
Napisz klasę Main testującą działanie metod getInt oraz getDouble z ćwiczenia 31.4.
b4ac40784ca6e05cededff5eda5a930f
b
280 Java. Praktyczny kurs
z wykorzystaniem metod z klas Integer, Double lub Float5. Istnieje również inna metoda:
użycie klasy StreamTokenizer. Klasa ta dzieli strumień wejściowy na jednostki leksy-
kalne, czyli tokeny. Przykładowo jeśli strumień zawiera dane:
123 test 18
to można w nim wyróżnić trzy tokeny: 123, test i 18. Po przetworzeniu otrzymamy dwie
liczby oraz ciąg znaków. To, jakie znaki będą separatorami oddzielającymi poszcze-
gólne jednostki leksykalne (w powyższym przykładzie są to spacje), można zdefinio-
wać samodzielnie, jednak dla dalszych ćwiczeń i w wielu typowych zastosowaniach
domyślne ustawienia są w zupełności wystarczające (to wszystkie tzw. białe znaki,
czyli spacje, tabulatory, znaki końca wiersza itp.).
Koniec jednak z teorią, czas sprawdzić, jak w praktyce wygląda wykorzystanie klasy
StreamTokenizer. Napiszmy program, który ze standardowego wejścia wczyta liczbę
całkowitą i wyświetli na ekranie jej wartość pomnożoną przez 2 (jest to zadanie podobne
do realizowanych w poprzedniej lekcji). Odpowiedni kod jest widoczny na listingu 6.6.
Listing 6.6.
import java.io.*;
try{
strTok.nextToken();
5
W sposób analogiczny można wykorzystać metody klas Byte, Long, Short.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 281
}
catch(IOException e){
System.out.print("Błąd podczas odczytu danych ze strumienia.");
return;
}
if(strTok.ttype != StreamTokenizer.TT_NUMBER){
System.out.print("To nie jest prawidłowa liczba.");
return;
}
Efekt będzie taki sam: utworzenie obiektu strTok klasy StreamTokenizer powiązanego ze
standardowym strumieniem wejściowym. Kiedy obiekt zostanie utworzony, wyświetlamy
na ekranie prośbę o podanie liczby oraz wywołujemy metodę nextToken. Odczytuje
ona ze strumienia kolejną jednostkę leksykalną. Wywołanie umieszczamy w bloku try,
aby przechwycić ewentualny wyjątek IOException, który może się pojawić podczas
czytania ze strumienia.
6
Dokładniej: obiekt klasy Reader lub pochodnej od Reader. Zdecydowanie nie należy stosować
bezpośrednio obiektów klasy InputStream, mimo że taka wersja konstruktora również istnieje.
7
W rzeczywistości jest to pole statyczne typu int.
8
Gdyby odczytana jednostka leksykalna nie zawierała liczby, ale inny ciąg znaków, ciąg ten można
by było odczytać z pola o nazwie sval.
b4ac40784ca6e05cededff5eda5a930f
b
282 Java. Praktyczny kurs
Rysunek 6.7.
Wczytanie wartości
liczbowej przy użyciu
klasy StreamTokenizer
Zastanówmy się teraz, jak usprawnić tę aplikację. W obecnej sytuacji, kiedy użytkownik
nie wprowadzi wartości liczbowej, np. pomyli się, aplikacja zakończy działanie. Nie
jest to zbyt dobre zachowanie. Najlepiej byłoby ponownie poprosić o wprowadzenie
prawidłowej liczby. Przyda się zatem pętla while, która będzie działać tak długo, aż po-
dawane dane będą poprawne. Takie rozwiązanie zostało przedstawione na listingu 6.7.
Listing 6.7.
import java.io.*;
try{
while(strTok.nextToken() != StreamTokenizer.TT_NUMBER){
System.out.println("To nie jest poprawna liczba.");
System.out.print("Wprowadź liczbę: ");
}
}
catch(IOException e){
System.out.print("Błąd podczas odczytu danych ze strumienia.");
return;
}
Obiekt przypisywany zmiennej strTok jest tworzony dokładnie tak samo jak w poprzed-
nim przykładzie. Po wyświetleniu w bloku try prośby o wprowadzenie liczby rozpo-
czyna się jednak działanie pętli typu while. Działa ona tak długo, aż wartość zwrócona
przez wywołanie metody nextToken będzie równa TT_NUMBER. Dopóki więc użytkownik
nie poda prawidłowej liczby, będzie proszony o jej podanie i wywoływana będzie metoda
nextToken. Kiedy poprawna wartość liczbowa zostanie wprowadzona, pętla zakończy
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 283
swoje działanie i wartość ta zostanie przypisana zmiennej liczba. Na ekranie jest na-
tomiast wyświetlany wynik mnożenia wartości zapisanej w zmiennej liczba przez 2.
Listing 6.8.
import java.io.*;
try{
while(strTok.nextToken() != StreamTokenizer.TT_NUMBER){
System.out.println("To nie jest poprawna liczba.");
System.out.print("Wprowadź pierwszą liczbę: ");
}
}
catch(IOException e){
System.out.print("Błąd podczas odczytu danych ze strumienia.");
return;
}
b4ac40784ca6e05cededff5eda5a930f
b
284 Java. Praktyczny kurs
Rysunek 6.8.
Mnożenie dwóch liczb
if (A == 0){
System.out.println ("To nie jest równanie kwadratowe: A = 0!");
}
else{
double delta = B * B - 4 * A * C;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 285
double paramA = 0;
double paramB = 0;
double paramC = 0;
try{
System.out.print("Podaj parametr A: ");
paramA = pobierzLiczbe(strTok);
System.out.print("Podaj parametr B: ");
paramB = pobierzLiczbe(strTok);
System.out.print("Podaj parametr C: ");
paramC = pobierzLiczbe(strTok);
System.out.println("");
}
catch(IOException e){
System.out.println("Błąd podczas odczytu strumienia.");
return;
}
oblicz(paramA, paramB, paramC);
}
public static void main (String args[]) {
Main main = new Main();
main.start();
}
}
b4ac40784ca6e05cededff5eda5a930f
b
286 Java. Praktyczny kurs
Pozostała jeszcze metoda start. Jej zadaniem jest utworzenie obiektu klasy Stream
Tokenizer, który zostanie wykorzystany w metodzie pobierzLiczbe, przypisanie wartości
zmiennym paramA, paramB i paramC oraz wywołanie metody oblicz. Oczywiście wy-
mienione zmienne odzwierciedlają kolejne parametry równania kwadratowego. Wykony-
wanie kodu rozpocznie się tradycyjnie od metody main, w której tworzymy nowy obiekt
klasy Main i wywołujemy metodę start. O tym, że program rzeczywiście rozwiązuje
równania kwadratowe, powinien przekonać rysunek 6.9, który prezentuje rozwiązanie
równania o parametrach A = 2, B = -2 i C = -4, czyli równania o postaci: 2x2 + 2x – 4 = 0.
Rysunek 6.9.
Efekt działania
programu
rozwiązującego
równania kwadratowe
Klasa Scanner
W Java 5 pojawiła się nowa klasa, o nazwie Scanner, upraszczająca wiele operacji wczy-
tywania i przetwarzania danych. Zawiera ona konstruktory, które mogą przyjmować
obiekty klas: File, InputReader i String, a także obiekty implementujące interfejsy
Readable lub ReadableByteChannel. Jest to więc zestaw pozwalający na obsługę bardzo
wielu formatów wejściowych. Metod klasy Scanner jest bardzo wiele, nie ma potrzeby,
aby je wszystkie omawiać. Warto jednak wiedzieć, że najbardziej dla nas interesujące
rodziny next i hasNext są konstruowane na bardzo prostej zasadzie, którą schematycznie
można przedstawić jako:
hasNextNazwaTypuProstego
i
nextNazwaTypuProstego
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 287
leksykalna. Dodatkowo dostępna jest metoda hasNextLine, która określa, czy w stru-
mieniu znajduje się linia tekstu.
Metody z rodziny next, a więc nextInt, nextDouble, nextByte itd., zwracają natomiast
kolejną jednostkę leksykalną w postaci wartości danego typu prostego. Metoda next
zwraca zaś token w postaci ciągu znaków. Podobnie jak w przypadku opisanym w po-
przednim akapicie, istnieje również metoda nextLine, która zwraca całą linię tekstu.
Zobaczmy, jak wykorzystać to w praktyce: napiszmy program analogiczny do prezento-
wanego w poprzedniej części lekcji, ale korzystający z klasy Scanner. Będzie wczyty-
wał ze standardowego strumienia wejściowego wartość całkowitą i wyświetli rezultat
jej mnożenia przez 2. Kod realizujący takie zadanie został przedstawiony na listingu 6.10.
Listing 6.10.
import java.util.*;
while(!scanner.hasNextInt()){
System.out.print("To nie jest wartość całkowita: ");
System.out.println(scanner.next());
System.out.print("Wprowadź wartość całkowitą: ");
}
int value = scanner.nextInt();
int result = value * 2;
System.out.println(value + " * 2 = " + result);
}
}
Jak widać, pętla zostanie zakończona, kiedy metoda hasNextInt obiektu zwróci war-
tość true, a więc wtedy, kiedy w strumieniu wejściowym pojawi się wartość, która będzie
mogła być zinterpretowana jako całkowita. Oczywiście w sytuacji, gdy hasNextInt
zwróci wartość false, wykonywane będzie wnętrze pętli, w którym wyświetlana jest in-
formacja o tym, że wprowadzono niepoprawną wartość. Wartość ta będzie pobierana
ze strumienia za pomocą metody next i w celach informacyjnych wyświetlana na ekranie:
System.out.println(scanner.next());
b4ac40784ca6e05cededff5eda5a930f
b
288 Java. Praktyczny kurs
Z kolei kiedy w strumieniu pojawi się wartość typu int, pętla zostanie zakończona,
a wczytana wartość zostanie pobrana za pomocą wywołania metody nextInt i zapisana
w zmiennej value:
int value = scanner.nextInt();
Następnie jest wykonywane mnożenie przez 2, a wynik tej operacji zostaje wyświetlony
na ekranie. Przykładowy efekt działania programu widać na rysunku 6.10.
Rysunek 6.10.
Wczytywanie wartości
za pomocą klasy Scanner
Klasa Console
Począwszy od Java 6 (1.6), w JDK dostępna jest klasa Console wyspecjalizowana w ob-
słudze konsoli (zawarta jest w pakiecie java.io). Dzięki niej możliwe jest pobranie od-
wołania do konsoli, na której operuje Java (o ile taka jest udostępniona; nie musi to być
też konsola systemowa), i pobieranie oraz wyświetlanie danych. Jest to klasa finalna
i dziedziczy bezpośrednio po Object. Aby uzyskać obiekt typu Console będący odwoła-
niem do bieżącej konsoli systemowej, nie wywołuje się bezpośrednio konstruktora,
ale metodę statyczną console z klasy System. To wywołanie ma postać:
System.console()
W efekcie zostanie uzyskany obiekt konsoli, za którego pośrednictwem będzie można wy-
konywać różne operacje, lub też wartość null, o ile konsola nie jest dostępna. Wspomnia-
ne operacje mogą być realizowane za pomocą metod przedstawionych w tabeli 6.3.
Prosty program pozwalający na wczytanie wiersza tekstu, a następnie wyświetlający
uzyskane dane na ekranie został przedstawiony na listingu 6.11.
Listing 6.11.
import java.io.Console;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 289
Ponieważ klasa Console znajduje się w pakiecie java.io, kod rozpoczyna się odpowiednią
instrukcją import. Odwołanie do konsoli pobierane jest w sposób opisany powyżej i zapi-
sywane w zmiennej con. Następnie badane jest, czy wartość con jest równa null (byłoby
tak, gdyby aplikacja nie miała dostępu do konsoli). Jeśli jest równa null, za pomocą
instrukcji System.out.println następuje próba podania komunikatu o błędzie i jest to
koniec działania aplikacji. W przeciwnym razie (blok else) wyświetlany jest tekst
z prośbą o wprowadzenie wiersza tekstu i wiersz ten jest wczytywany (wywołanie me-
tody readLine). Po odczytaniu danych są one ponownie wyświetlane za pomocą me-
tody printf.
Program z listingu 6.11 można w łatwy sposób przerobić, tak aby odczytywał dane w pętli
while aż do osiągnięcia pewnego ciągu znaków, np. quit. To jednak pozostanie ćwicze-
niem do samodzielnego wykonania. W tej części lekcji przyjrzymy się jeszcze użytej
na listingu metodzie printf. Pozwala ona na wyświetlanie danych w postaci sforma-
towanej według pewnego wzorca. Wystarczy w ciągu znaków będącym pierwszym
argumentem umieścić znaczniki, które zostaną zamienione na wartości pobrane z ko-
lejnych argumentów. Schematycznie wywołanie metody printf będzie miało zatem
postać (przy założeniu, że obiekt con reprezentuje konsolę):
con.printf("format", wartość1, wartość2, ..., wartośćN);
9
W rzeczywistości ciąg formatujący może mieć dużo bardziej złożoną postać i określać np. precyzję,
z jaką ma być wyświetlona wartość. Dokładne dane można znaleźć w dokumentacji technicznej JDK.
b4ac40784ca6e05cededff5eda5a930f
b
290 Java. Praktyczny kurs
to ich wartości można wpleść w ciąg znaków, który zostanie wyświetlony na ekranie
w następujący sposób:
con.printf("zmienna1 = %b, zmienna2 = %d", zmienna1, zmienna2);
Na listingu 6.12 został przedstawiony program wyświetlający jedną wartość (50) na kilka
różnych sposobów: jako znak, jako wartość dziesiętną, ósemkową i szesnastkową.
Listing 6.12.
import java.io.Console;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 291
Klasa PrintStream
Obiekt out z klasy System, reprezentujący standardowy strumień wyjściowy, jest obiektem
typu PrintStream. Używany był wielokrotnie przy wyprowadzaniu danych na ekran za
pomocą metod println i print. Jak już wiemy, metody te występują w wielu przeciążo-
nych wersjach i umożliwiają wyświetlanie różnorodnych typów danych. Wszystkie
dostępne metody klasy PrintStream zostały zebrane w tabeli 6.5.
b4ac40784ca6e05cededff5eda5a930f
b
292 Java. Praktyczny kurs
Najpierw trzeba sobie przypomnieć, skąd bierze się problem wyświetlania polskich
znaków. Otóż typowa sytuacja jest następująca: w systemie Windows w napisanym
przez nas kodzie źródłowym polskie znaki są kodowane w standardzie CP1250 (Win-
dows 1250)11, Java natomiast przechowuje je w standardzie Unicode i tak też są one
10
W niektórych wersjach JDK 1.5 znaki na konsoli były standardowo wyświetlane zgodnie
z kodowaniem CP852, nie było więc potrzeby wykonywania opisywanych czynności.
11
Chyba że został użyty edytor pozwalający na zapis w innym kodowaniu, np. UTF-8, i to ono zostało
wybrane jako standard zapisu.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 293
Pierwszy z nich używa domyślnej strony kodowej, nie będzie więc w tym przypadku
przydatny. Drugi13 przyjmuje argument, który określa stronę kodową, w jakiej mają
być kodowane znaki w strumieniu. Co w związku z tym należy zrobić? Utworzyć obiekt
klasy OutputStreamWriter (powiązany ze standardowym strumieniem wyjściowym)
korzystający ze strony kodowej 852. W tym celu należy zastosować konstrukcję:
new OutputStreamWriter(System.out, "Cp852");
Listing 6.13.
import java.io.*;
12
W polskiej wersji systemu Windows.
13
Dostępny, począwszy od JDK 1.4.
b4ac40784ca6e05cededff5eda5a930f
b
294 Java. Praktyczny kurs
System.out.println();
Podstawową operacją jest utworzenie obiektu outp klasy PrintWriter. Używamy go zamiast
tradycyjnego System.out. Deklarujemy zatem najpierw zmienną outp i przypisujemy jej
wartość null, a następnie w bloku try tworzymy sam obiekt. Pierwszym parametrem kon-
struktora klasy PrintWriter jest nowo utworzony obiekt klasy OutputStreamWriter po-
wiązany ze standardowym strumieniem wyjściowym reprezentowanym przez System.out
i wykorzystujący stronę kodową 852. Należy zwrócić uwagę na drugi parametr równy
true, który będzie wymuszać wypchnięcie danych do strumienia po wykonaniu każdej
instrukcji print lub println. Niezastosowanie go będzie skutkowało przetrzymywaniem
danych w buforze i nie będą się one prawidłowo pojawiały na ekranie14.
Warto zwrócić również uwagę na to, co się dzieje, kiedy wystąpi wyjątek Unsupported
EncodingException. Otóż w takiej sytuacji nie można pozwolić na to, aby zmienna outp
nie została zainicjowana, gdyż doprowadziłoby to do błędów w dalszej części programu
(niemożność wykorzystania obiektu outp). W związku z tym ponownie konstruujemy
obiekt klasy PrintWriter, tym razem nie wymuszając zastosowania strony kodowej 852.
Kiedy utworzymy już obiekt outp, można używać go zamiast standardowego System.out15,
tak jak jest to widoczne w kolejnych instrukcjach, w których dokonujemy porównania
obu sposobów wyświetlania polskich znaków. Efekt działania programu jest widoczny
na rysunku 6.11.
Rysunek 6.11.
Efekt użycia klas
PrintWriter
i OutputStreamWriter
do wyświetlenia
polskich znaków
14
W praktyce może się jednak okazać konieczne wywoływanie metody flush po każdej instrukcji
print lub println (patrz również rozwiązanie ćwiczenia 32.4).
15
Oczywiście outp korzysta ze strumienia System.out, wcześniej jednak dokonuje przekodowania
znaków zgodnego z wybraną stroną kodową.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 295
Ćwiczenie 32.1.
Zmodyfikuj program z listingu 6.7 tak, aby poprawnie reagował (tzn. nie wyświetlał ko-
lejnej prośby o wprowadzenie liczby) na zakończenie pracy aplikacji przez naciśnięcie
klawiszy Ctrl+C (jest to sytuacja równoznaczna z osiągnięciem końca strumienia).
Ćwiczenie 32.2.
Napisz program, który będzie wczytywał ze standardowego wejścia tekst. Po wprowa-
dzeniu każdej linii program powinien wyświetlić oddzielone od siebie słowa, z których
się ona składa. Koniec pracy ma nastąpić, kiedy w tekście pojawi się słowo quit lub
zostanie wciśnięta kombinacja klawiszy Ctrl+C.
Ćwiczenie 32.3.
Napisz program umożliwiający użytkownikowi wykonywanie na dwóch liczbach czterech
podstawowych działań arytmetycznych: dodawania, odejmowania, mnożenia i dzielenia.
Ćwiczenie 32.4.
Napisz program, który za pomocą klasy Console będzie wczytywał wiersze tekstu i od
razu wyświetlał wczytywane dane na ekranie wraz z numerem wprowadzonego wiersza.
Odczyt danych ma się zakończyć po wprowadzeniu ciągu quit.
Ćwiczenie 32.5.
Zmodyfikuj program z listingu 6.9 w taki sposób, aby wyświetlał znaki na ekranie
przy użyciu strony kodowej 852.
Klasa File
Klasa File pozwala na wykonywanie podstawowych operacji na plikach i katalogach,
takich jak ich tworzenie i usuwanie, operacje na nazwach czy pobieranie parametrów,
np. czasu utworzenia bądź modyfikacji plików lub katalogów. Nie jest to jednak klasa,
która umożliwiałaby modyfikację zawartości pliku. Metody przez nią udostępniane
zostały zebrane w tabeli 6.6. Będziemy je wykorzystywać w dalszej części lekcji.
b4ac40784ca6e05cededff5eda5a930f
b
296 Java. Praktyczny kurs
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 297
b4ac40784ca6e05cededff5eda5a930f
b
298 Java. Praktyczny kurs
Listing 6.14.
import java.io.*;
Konstruujemy obiekt file klasy File, podając w konstruktorze ścieżkę dostępu do katalo-
gu, którego zawartość chcemy wylistować — jest to katalog bieżący oznaczony jako .,
można też użyć zapisu ./. Format, w jakim podaje się nazwę ścieżki dostępu, jest za-
leżny od konwencji stosowanej w danym systemie operacyjnym, klasa File sama radzi
sobie z rozkodowaniem takiego zapisu. Następnie deklarujemy zmienną dirList i przypi-
sujemy jej obiekt zwrócony przez wywołanie metody list. Ponieważ ten obiekt to w rze-
czywistości tablica obiektów klasy String, pozostaje zastosowanie pętli for do odczytania
jego zawartości i wyświetlenia jej na ekranie. Przykładowy efekt wykonania kodu z listin-
gu 6.14 jest widoczny na rysunku 6.12.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 299
Rysunek 6.12.
Wyświetlenie zawartości
katalogu przy użyciu
metody list klasy File
Listing 6.15.
import java.io.*;
import java.util.regex.*;
16
Omówienie tematu wyrażeń regularnych wykracza poza ramy niniejszej książki. Osoby nieznające tej
tematyki powinny zapoznać się z publikacjami ogólnodostępnymi w internecie.
b4ac40784ca6e05cededff5eda5a930f
b
300 Java. Praktyczny kurs
Jeżeli jednak drugi argument wiersza poleceń istnieje, to oznacza, że jest to wzorzec,
z którym będą porównywane pliki, wykorzystujemy go więc jako argument konstruktora
klasy MyFilenameFilter. Zapis:
dirList = file.list(
(FilenameFilter) new MyFileNameFilter(args[1])
);
Niezależnie zatem od tego, czy wzorzec został przekazany, czy nie, zmienna dirList
będzie zawierała listę plików i katalogów znajdujących się we wskazanym katalogu.
Ostatecznie wyświetlamy ją na ekranie, wykorzystując klasyczną pętlę for.
Przyjrzyjmy się teraz konstrukcji klasy MyFilenameFilter. Po co się ją tworzy? Otóż użyta
metoda list wymaga argumentu będącego obiektem klasy implementującej interfejs
17
Jest to klasa pakietowa, zatem cały listing należy zapisać w jednym pliku — Main.java.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 301
Jak to będzie działać? Otóż metoda list dla każdego znalezionego pliku będzie wywo-
ływała metodę accept obiektu przekazanego jej (metodzie list) jako argument. W na-
szym przypadku będzie to metoda accept z klasy MyFilenameFilter. Metoda ta powinna
zwrócić wartość true, o ile dany plik lub katalog ma być uwzględniony w listingu,
lub false w przeciwnym przypadku. Trzeba zatem tak skonstruować tę metodę, aby
sprawdzała, czy nazwa pliku pasuje do wyrażenia regularnego. Pomogą nam w tym
klasy z pakietu java.util.regex, stworzone specjalnie do operowania na wyrażeniach
regularnych.
staje się jednocześnie wynikiem całej funkcji. Jeśli jest to wartość true, oznacza to, że
name jest zgodne z wyrażeniem regularnym; jeśli jest to wartość false, zgodności nie ma.
Zauważmy też, że w rzeczywistości można byłoby również całą treść metody accept
zapisać w jednej linii, nie ma bowiem potrzeby tworzenia dodatkowej zmiennej matcher.
Równie dobrze sprawdzi się kod w postaci19:
return pattern.matcher(name).matches();
18
Można również użyć obiektu klasy anonimowej.
19
Przykład mógłby być znacznie prostszy, gdyby zastosować metodę matches z klasy Pattern w połączeniu
z klasą anonimową. Wtedy jednak kompilacja wyrażenia regularnego odbywałaby się przy każdym
wywołaniu metody accept, co zgodnie z dokumentacją JDK jest metodą wolniejszą i powinno być
stosowane tylko przy pojedynczych porównaniach.
b4ac40784ca6e05cededff5eda5a930f
b
302 Java. Praktyczny kurs
Przykładowy efekt działania tego programu został przedstawiony na rysunku 6.13. Pierw-
szy znak . określa katalog bieżący, a użyte wyrażenie regularne ^M.*\.java$ oznacza
ciągi znaków (nazwy plików) zaczynające się (^) od dużej litery M, po której następuje
dowolna liczba dowolnych znaków (.*), a nazwa jest zakończona ($) ciągiem .java20.
Program działa zatem zgodnie z założeniami, choć zawiera pewien błąd. Jego odnalezie-
nie i poprawienie pozostanie jednak ćwiczeniem do samodzielnego wykonania (podpo-
wiedź: co się stanie, jeśli w wywołaniu zostanie użyta nazwa nieistniejącego katalogu?).
Rysunek 6.13.
Efekt działania
programu listującego
zawartość katalogu
Listing 6.16.
import java.io.*;
20
Dokładniejszy opis wyrażeń regularnych można znaleźć m.in. w publikacji JavaScript. Praktyczny kurs
(http://helion.pl/ksiazki/jscpk.htm).
21
Metoda ta pojawiła się w JDK 1.2, wcześniejsze wersje JDK jej nie zawierają.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 303
return;
}
}
}
Pierwszą instrukcją jest zatem sprawdzenie, czy nazwa ta została rzeczywiście poda-
na. Jeśli nie, pozostaje wyświetlić stosowny komunikat i zakończyć pracę, wykorzy-
stując instrukcję return. Jeśli tak, należy utworzyć obiekt file klasy File poprzez
przekazanie konstruktorowi pierwszego argumentu z wiersza poleceń oraz wywołać
metodę createNewFile. Jeśli plik nie istniał i został poprawnie utworzony, metoda ta
zwraca wartość true, natomiast jeżeli plik o wskazanej nazwie istniał wcześniej, zwracana
jest wartość false, a próba utworzenia nie jest podejmowana. Jeśli plik nie istniał, ale
próba jego utworzenia się nie powiodła, generowany jest wyjątek IOException. Może
tak się stać np. w sytuacji, kiedy podamy nieistniejącą ścieżkę dostępu.
Równie ważne jak tworzenie plików jest tworzenie katalogów, w których te pierwsze
mogą być umieszczane. Umożliwiają to dwie metody klasy File: mkdir oraz mkdirs.
Różnica między nimi jest taka, że w przypadku metody mkdir musi istnieć pełna
ścieżka dostępu, natomiast mkdirs potrafi utworzyć niezbędne katalogi nadrzędne. To
znaczy, że jeśli istnieje przykładowy katalog /java/test/, to próba utworzenia katalogu
/java/test/files/source przy użyciu mkdir zakończy się niepowodzeniem (nie istnieje
bowiem ścieżka /java/test/files/), natomiast przy wykorzystaniu mkdirs zakończy się
sukcesem (zostanie bowiem utworzony również brakujący podkatalog files). Obie metody
zwracają wartość true, gdy próba utworzenia katalogu zakończyła się powodzeniem,
oraz false w przeciwnym wypadku. Oznacza to, że w sytuacji, kiedy katalog istnieje
i podjęta zostanie próba jego utworzenia, w wyniku otrzymamy wartość false (mimo
że katalog jest na dysku). Warto więc przed wywołaniem mkdir lub mkdirs sprawdzać, czy
katalog istnieje, za pomocą metody exists. Ten sposób został zastosowany w programie
przedstawionym na listingu 6.17.
Listing 6.17.
import java.io.*;
b4ac40784ca6e05cededff5eda5a930f
b
304 Java. Praktyczny kurs
Listing 6.18.
import java.io.*;
Nazwa pliku lub katalogu do usunięcia jest przekazywana aplikacji w wierszu poleceń
jako pierwszy parametr wywołania. Zostaje ona użyta w konstruktorze obiektu klasy File.
Sprawdzamy następnie, czy wskazany plik lub katalog istnieje (if(!file.exists()){),
jeśli nie, kończymy pracę programu (przez wywołanie instrukcji return). Jeśli jednak
istnieje, wykonujemy metodę delete i w zależności od tego, jaka wartość zostanie przez
nią zwrócona, wyświetlamy komunikat o powodzeniu lub niepowodzeniu operacji.
Trzeba jednak wiedzieć, że jeśli spróbujemy usunąć katalog, który ma jakąś zawartość
(nie jest pusty), operacja taka nie zostanie wykonana (metoda delete zwróci wartość
false). Program usuwający katalog wraz z całą zawartością będzie musiał być skonstru-
owany inaczej. Można zastosować w tym celu technikę rekurencji, czyli wywoływania
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 305
funkcji przez samą siebie22. Kod realizujący takie zadanie został przedstawiony na li-
stingu 6.19.
Listing 6.19.
import java.io.*;
Tym razem funkcja main zajmuje się jedynie sprawdzeniem, czy aplikacji został prze-
kazany przynajmniej jeden argument, utworzeniem obiektu klasy Delete oraz wywoła-
niem jego metody delete. Właściwa funkcjonalność, czyli procedury usuwające pliki
i katalogi, zawiera się właśnie w metodzie delete. Jako argument otrzymuje ona na-
zwę katalogu, który ma zostać usunięty. Wewnątrz tworzymy nowy obiekt klasy File
i pobieramy listę obiektów znajdujących się w tym katalogu. Tym razem, inaczej niż
we wcześniejszych przykładach, wykorzystujemy metodę listFiles, która zwraca tablicę
obiektów klasy File.
W pętli for sprawdzamy po kolei obiekty zawarte w tablicy dirList. Jeżeli dany obiekt
jest katalogiem, wywołujemy rekurencyjnie funkcję delete, przekazując jej jako argument
jego nazwę (uzyskaną dzięki wywołaniu metody getPath, patrz też tabela 6.6), czyli
usuwamy ten podkatalog wraz z jego zawartością (delete(dirList[i].getPath())).
Niezależnie jednak od tego, czy mamy do czynienia z plikiem, czy z katalogiem, wy-
22
Osoby nieznające technik rekurencyjnych powinny zapoznać się z dowolną książką opisującą podstawy
algorytmów.
b4ac40784ca6e05cededff5eda5a930f
b
306 Java. Praktyczny kurs
Ćwiczenie 33.1.
Napisz program, który wyświetli listę plików i podkatalogów katalogu o nazwie prze-
kazanej w postaci argumentu w taki sposób, że najpierw pojawi się ponumerowana lista
plików, a następnie ponumerowana lista katalogów. Pliki i katalogi powinny być nume-
rowane osobno.
Ćwiczenie 33.2.
Zmodyfikuj kod klas z listingu 6.15 w taki sposób, aby w przypadku, kiedy podane
w wywołaniu wyrażenie regularne nie jest prawidłowe, program nie zgłaszał błędu,
ale też nie wyświetlał żadnych plików. W klasie Main nie może występować instrukcja
try przechwytująca wyjątek PatternSyntaxException.
Ćwiczenie 33.3.
Zmodyfikuj program z listingu 6.19 tak, aby po wykonaniu operacji usuwania wyświetlił
liczbę faktycznie usuniętych plików oraz katalogów.
Ćwiczenie 33.4.
Zmodyfikuj program z listingu 6.19 tak, by usuwał jedynie pliki, pozostawiając niena-
ruszoną strukturę katalogów.
Ćwiczenie 33.5.
Zmodyfikuj program z listingu 6.19 tak, aby nie było konieczności tworzenia obiektu
klasy Delete.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 307
Klasa RandomAccessFile
Klasa RandomAccessFile daje możliwość wykonywania wszelkich operacji na plikach
o dostępie swobodnym. Pozwala na odczytywanie i zapisywanie danych w pliku oraz
przemieszczanie się po pliku. Jest dostępna we wszystkich JDK, począwszy od JDK 1.0.
Metody udostępniane przez RandomAccessFile są zebrane w tabeli 6.7.
b4ac40784ca6e05cededff5eda5a930f
b
308 Java. Praktyczny kurs
Oba tworzą powiązany z plikiem wskazywanym przez file lub name strumień służący
do odczytu i (lub) zapisu danych. Jeśli zostanie użyty parametr mode równy r, plik musi
wcześniej istnieć na dysku, inaczej zostanie zgłoszony wyjątek FileNotFoundException.
Parametr mode może przyjmować jedną z czterech wartości:
r — otwarcie tylko do odczytu, próba wykonania jakiejkolwiek operacji
zapisu spowoduje wygenerowanie wyjątku IOException;
rw — otwarcie do odczytu i zapisu, jeżeli plik nie istnieje, zostanie podjęta
próba jego utworzenia;
rws — otwarcie do odczytu i zapisu wymuszające synchroniczny zapis
w rzeczywistym urządzeniu (np. na dysku twardym) przy każdej modyfikacji
danych lub metadanych pliku;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 309
if(!file.exists()){
System.out.println("Nie ma takiego pliku.");
return;
}
RandomAccessFile raf = null;
try{
raf = new RandomAccessFile(file, "r");
}
catch(FileNotFoundException e){
System.out.println("Nie ma takiego pliku.");
return;
}
String line = "";
try{
while((line = raf.readLine()) != null){
System.out.println(line);
}
raf.close();
}
catch(IOException e){
System.out.println("Błąd wejścia-wyjścia.");
}
}
}
b4ac40784ca6e05cededff5eda5a930f
b
310 Java. Praktyczny kurs
Skoro obiekt raf został utworzony, można przystąpić do odczytu pliku. Deklarujemy
zmienną line typu String, która będzie tymczasowo przechowywała kolejne linie tekstu.
Odczyt odbywa się w pętli while. Warunek tej pętli jest instrukcją złożoną: line =
raf.readLine()) != null. W każdym przebiegu najpierw jest wykonywana metoda
readLine obiektu wskazywanego przez raf, następnie wynik jej działania zostaje przy-
pisany zmiennej line i ostatecznie zostaje wykonane sprawdzenie, czy line jest różne od
null. Porównanie wartości zwróconej przez readLine z null jest konieczne do stwierdze-
nia, że został osiągnięty koniec pliku. W środku pętli znajduje się tylko jedna instrukcja,
wyświetlająca wartość przypisaną line na ekranie. Po zakończeniu pętli zamykamy stru-
mień, wywołując metodę close. Całość jest ujęta w blok try, gdyż podczas odczytu
może wystąpić wyjątek IOException.
To, jak odczytywać dane z pliku, wiadomo już z poprzedniej części lekcji. Czas więc po-
znać sposób, w jaki dokonywany jest zapis. Napiszemy program, który będzie odczy-
tywał ze standardowego wejścia dane wprowadzane przez użytkownika i zapisywał je
w pliku tekstowym. Nazwę pliku będzie można podać w wywołaniu programu lub też
wprowadzić w trakcie jego wykonania. Kod realizujący przedstawione zadanie jest
widoczny na listingu 6.21.
Listing 6.21.
import java.io.*;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 311
if(file.exists()){
System.out.println("Plik o tej nazwie już istnieje.");
return;
}
RandomAccessFile raf = null;
try{
raf = new RandomAccessFile(file, "rw");
}
catch(FileNotFoundException e){
System.out.println("Nie można utworzyć pliku.");
return;
}
try{
while(true){
line = brIn.readLine();
if("quit".equals(line) || line == null){
break;
}
raf.writeBytes(line + "\n");
}
raf.close();
}
catch(IOException e){
System.out.print("\nBłąd wejścia-wyjścia.");
return;
}
}
}
b4ac40784ca6e05cededff5eda5a930f
b
312 Java. Praktyczny kurs
Kopiowanie plików
Skoro potrafimy już odczytywać i zapisywać pliki, nie powinno sprawić nam trudności
ich kopiowanie. Wystarczy utworzyć jeden obiekt RandomAccessFile dla pliku źródło-
wego, drugi dla pliku docelowego, a następnie w pętli odczytywać zawartość pierwszego
i zapisywać w drugim. Wykonajmy zatem taki przykład. Nazwy plików źródłowego i do-
celowego będą podawane przy wywołaniu programu. Klasę realizującą to zadanie na-
zwijmy Kopiuj; jej kod jest widoczny na listingu 6.22.
Listing 6.22.
import java.io.*;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 313
if(!sourceFile.exists()){
System.out.println("Plik źródłowy nie istnieje.");
return;
}
if(destFile.exists()){
System.out.println("Plik docelowy istnieje.");
return;
}
RandomAccessFile rafSource = null;
RandomAccessFile rafDest = null;
try{
rafSource = new RandomAccessFile(sourceFile, "r");
rafDest = new RandomAccessFile(destFile, "rw");
}
catch(FileNotFoundException e){
System.out.println("Błąd podczas otwierania plików: " + e);
return;
}
int b;
try{
while((b = rafSource.read()) != -1){
rafDest.write(b);
}
rafSource.close();
rafDest.close();
}
catch(IOException e){
System.out.print("\nBłąd wejścia-wyjścia podczas kopiowania pliku.");
return;
}
}
}
Kopiowanie odbywa się przez cykliczne wywoływanie metody read obiektu źródłowego
oraz metody write obiektu docelowego. Deklarujemy zmienną b typu int, która będzie
pośredniczyła w wymianie danych. Dlaczego ta zmienna jest typu int? Po zajrzeniu
do tabeli 6.7 okaże się, że metoda read, odczytując bajt, nie zwraca wartości typu byte,
ale int. Dokładniej, jest to wartość typu int z zakresu 0 – 255. Dzięki temu można łatwo
b4ac40784ca6e05cededff5eda5a930f
b
314 Java. Praktyczny kurs
zasygnalizować koniec pliku, metoda read zwraca wtedy wartość –1. Gdyby wartość
zwracana była typu byte, nie byłoby to możliwe i koniec pliku musiałby być sygnali-
zowany wystąpieniem wyjątku bądź w inny, mniej wygodny sposób.
Po tych wyjaśnieniach znaczenie wyrażenia warunkowego pętli while powinno być jasne.
Jest to wyrażenie złożone, podobnie jak w przypadku przykładu z listingu 6.20. Instruk-
cja ta odczytuje kolejne bajty z pliku źródłowego i przypisuje je zmiennej b tak długo,
aż metoda read zwróci wartość –1, czyli zostanie osiągnięty koniec pliku. Wewnątrz
pętli while za pomocą metody write zapisuje się każdy odczytany bajt w docelowym pli-
ku wskazywanym przez rafDest. Po wykonaniu kopiowania należy zamknąć oba pliki
(czy też dokładniej: strumienie powiązane z plikami), wywołując metodę close.
Przedstawiona metoda kopiowania jest najprostsza w realizacji, jednak nadaje się jedy-
nie do kopiowania plików o niewielkich rozmiarach. Ponieważ klasa RandomAccessFile
tworzy niebuforowany strumień operujący na pliku, czytanie i zapis bajt po bajcie jest
metodą bardzo nieefektywną (łatwo się o tym przekonać, próbując kopiować za pomocą
przedstawionego programu pliki o dużych rozmiarach). Rozwiązaniem jest wykorzy-
stanie buforowanych klas strumieniowych (zostanie to pokazane w dalszej części lekcji)
lub samodzielna obsługa buforowania. Wystarczy przecież umieścić w utworzonej
klasie niewielki bufor i skorzystać z metod czytających naraz większą ilość danych. Jak
wynika z danych zawartych w tabeli 6.7, takie metody są dostępne w klasie RandomAccess
File. Program wykonujący kopiowanie plików, korzystający z własnego bufora,
został przedstawiony na listingu 6.23.
Listing 6.23.
import java.io.*;
int count = 0;
int buffSize = 10000;
byte[] buff = new byte[buffSize];
try{
while((count = rafSource.read(buff)) != -1){
rafDest.write(buff, 0, count);
}
rafDest.close();
rafSource.close();
}
catch(IOException e){
System.out.print("\nBłąd wejścia-wyjścia podczas kopiowania pliku.");
return;
}
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 315
Początek kodu jest taki sam jak w przykładzie z listingu 6.20, nie ma zatem potrzeby,
aby go powtórnie omawiać. Zmienia się natomiast metoda kopiowania danych. Przede
wszystkim zostają zadeklarowane dodatkowe zmienne:
count — przechowująca liczbę odczytanych bajtów;
buffSize — przechowująca wielkość bufora;
buff — tablica typu byte będąca buforem danych.
W pętli while za pomocą metody read jest odczytywana porcja danych. Zostaną one zapi-
sane w tablicy buff. Liczba odczytanych bajtów zostanie zwrócona przez tę metodę
i zapisana w zmiennej count. Jeżeli będzie to wartość –1, będzie to oznaczało osiągnięcie
końca pliku. Po każdym odczytaniu dane są zapisywane w pliku docelowym za pomocą
metody write. Metodzie tej przekazuje się trzy parametry, pierwszy to bufor (czyli
tablica bajtów), drugi to indeks komórki tablicy, od której rozpocznie się zapis, trzeci to
liczba bajtów do zapisania.
W celach testowych dobrze byłoby teraz uruchomić obie wersje programu (z listingu
6.22 i 6.23) i wykonać kopiowanie jakiegoś dużego pliku (rzędu kilku – kilkunastu
megabajtów). Różnica w czasie wykonania będzie aż nadto widoczna. Ciekawym ekspe-
rymentem jest również zmienianie wielkości bufora i obserwowanie czasu wykonania
programu.
W tym miejscu warto jeszcze zwrócić uwagę na sposób działania wykorzystanej w pro-
gramie metody read. Liczba odczytanych bajtów może wahać się od jednego do wiel-
kości tablicy będącej buforem, nigdy nie wolno zatem zakładać, że bufor jest za każ-
dym razem zapełniany do końca. Nie jest więc możliwe zastosowanie konstrukcji:
while(rafSource.read(buff) != -1){
rafDest.write(buff, 0, buff.length);
}
W tym przypadku błąd jest raczej oczywisty. Przecież plik nie musi mieć rozmiaru będą-
cego wielokrotnością rozmiaru bufora, a takie założenie zostało w tym fragmencie
uczynione. Błąd zostanie też szybko wykryty, gdyż plik źródłowy i docelowy praktycznie
w każdym wypadku będą się różnić. Dużo groźniejsze jest natomiast poprawienie tego
błędu w sposób następujący:
while((count = rafSource.read(buff)) != -1){
rafDest.write(buff, 0, count);
if(count != buffSize) break;
}
lub:
while(true){
count = rafSource.read(buff);
if(count != -1) rafDest.write(buff, 0, count);
if(count != buffSize) break;
}
Oba powyższe przykłady są niepoprawne, gdyż opierają się na założeniu, że liczba od-
czytanych bajtów jest zawsze równa wielkości bufora, a jedynie przy ostatnim odczycie
(przy samym końcu) może być mniejsza (ze względu na to, że rozmiar pliku nie jest
b4ac40784ca6e05cededff5eda5a930f
b
316 Java. Praktyczny kurs
Odczyt danych
Do dyspozycji są dwie podstawowe klasy FileReader oraz FileInputStream. Pierwsza
z nich powinna być stosowana, kiedy chce się skorzystać ze strumienia znakowego,
czyli raczej dla plików tekstowych, druga — kiedy chce się skorzystać ze strumienia
binarnego, czyli raczej dla plików binarnych. Obie klasy mają po trzy przeciążone
konstruktory. Argumentami tych konstruktorów mogą być:
ciąg znaków zawierający nazwę pliku;
obiekt klasy FileDescriptor;
obiekt klasy File.
Klasa FileInputStream udostępnia trzy metody odczytujące dane (te metody zebrano
w tabeli 6.8). Klasa FileReader udostępnia jedynie dwie metody odczytujące dane, obie
odziedziczone po klasie InputStreamReader (te metody są zawarte w tabeli 6.9).
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 317
Metod tych można używać bezpośrednio lub też wykorzystać obiekty klas FileReader
i FileInputStream jako argumenty dla konstruktorów klas dających większą funkcjonal-
ność, np. BufferedInputStream lub BufferedReader. Ta technika jest już znana z przykła-
dów obrazujących obsługę standardowego strumienia wejściowego. Napiszmy więc przy-
kładowy program, który za pomocą wymienionych klas będzie odczytywał linie tekstu
z pliku i wypisywał je na ekranie. Tak działający program przedstawiono na listingu 6.24.
Listing 6.24.
import java.io.*;
try{
while((line = brIn.readLine()) != null){
System.out.println(line);
}
brIn.close();
}
catch(IOException e){
System.out.print("\nBłąd wejścia-wyjścia.");
return;
}
}
}
b4ac40784ca6e05cededff5eda5a930f
b
318 Java. Praktyczny kurs
RandomAccessFile. Obiekt brIn jest tworzony przez kaskadowe (podobnie jak w przy-
kładach z lekcji 31.) wywołanie konstruktorów klasy BufferedReader oraz FileReader.
Konstruktorowi klasy FileReader jest przekazywany argument odczytany z wiersza pole-
ceń. Jeśli plik o przekazanej nazwie nie istnieje, zostanie wygenerowany wyjątek FileNot
FoundException, dlatego też cała konstrukcja jest ujęta w blok try. Odczyt danych
odbywa się w pętli while, instrukcja readLine obiektu brIn jest wywoływana tak długo, aż
zwróci wartości null, co oznaczać będzie osiągnięcie końca pliku. Każda odczytana
linia jest zapisywana w zmiennej line, a następnie wyświetlana na ekranie za pomocą
instrukcji System.out.println.
Zapis do pliku
Dane zapisuje się w plikach, wykorzystując klasy obsługujące strumienie wyjściowe.
Podobnie jak w przypadku odczytu, do dyspozycji są dwie podstawowe klasy: FileWriter
oraz FileOutputStream. Pierwsza z nich powinna być wykorzystywana do zapisu pli-
ków tekstowych, druga do zapisu strumieni binarnych. Klasa FileOutputStream oferuje
trzy metody służące do zapisu danych (te metody zebrano w tabeli 6.10). Klasa File
Writer dysponuje również trzema metodami zapisującymi. Wszystkie one zostały
odziedziczone po klasie nadrzędnej, OutputStreamWriter (zestawiono je w tabeli 6.11).
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 319
Jeżeli podany plik istnieje, zostanie otwarty, jeśli nie istnieje, zostanie podjęta próba jego
utworzenia. Wszystkie konstruktory generują wyjątek FileNotFoundException w na-
stępujących sytuacjach:
podana nazwa wskazuje na katalog, a nie na plik;
podany plik nie istnieje i nie można go również utworzyć;
nie można z jakiegoś powodu otworzyć istniejącego pliku.
Powyższe wiadomości pozwalają już bez problemu napisać program, który będzie wczy-
tywał dane ze standardowego wejścia i zapisywał je do pliku. Taki program jest przedsta-
wiony na listingu 6.25.
Listing 6.25.
import java.io.*;
try{
fileWriter = new FileWriter(args[0], true);
}
catch(IOException e){
System.out.println("Nie mogę otworzyć wskazanego pliku.");
return;
}
try{
while(true){
line = brIn.readLine();
if("quit".equals(line) || line == null){
break;
}
line += "\n";
fileWriter.write(line, 0, line.length());
}
fileWriter.close();
b4ac40784ca6e05cededff5eda5a930f
b
320 Java. Praktyczny kurs
}
catch(IOException e){
System.out.print("\nBłąd wejścia-wyjścia.");
return;
}
}
}
Za odczyt danych odpowiada obiekt wskazywany przez zmienną brIn klasy Buffered
Reader, za zapis — obiekt fileWriter klasy FileWriter. Obiekt zapisujący dane jest
tworzony w taki sposób, że jeśli plik istnieje, dane będą dopisywane na jego końcu
(drugi argument konstruktora równy true). W pętli while za pomocą metody readLine
obiektu brIn odczytujemy ze standardowego wejścia kolejne linie tekstu. Sprawdzamy,
czy zwrócona wartość jest równa quit, co by oznaczało, że użytkownik zakończył
wprowadzanie danych, oraz czy jest ona równa null, co wskazuje na osiągnięcie końca
strumienia (if("quit".equals(line) || line == null)). Osiągnięcie końca strumienia
w przypadku standardowego strumienia wejściowego oznacza zwykle sytuację niepra-
widłową, np. niespodziewane przerwanie działania programu przez użytkownika.
Odczytany tekst znajdzie się w zmiennej line. Dodajemy do niej znak końca linii (znak
ten jest usuwany przez metodę readLine podczas odczytu, nie ma go zatem w zmiennej
line) oraz wywołujemy metodę write obiektu fileWriter. Tym samym zapisujemy
odczytaną linię tekstu w pliku. Metoda write wymaga podania parametrów określających,
jaką część ciągu znaków chcemy zapisać. Ponieważ zapisana ma być cała zawartość
zmiennej line, pierwszym parametrem jest 0 (indeks pierwszego znaku, który ma zna-
leźć się w strumieniu), natomiast drugim — długość ciągu pobierana przez wywołanie
metody length (liczba znaków, które mają znaleźć się w strumieniu).
Kopiowanie plików
Skoro wiadomo już, jak zapisywać i odczytywać pliki, można napisać strumieniową
wersję programu wykonującego operację kopiowania. Tym razem zarówno strumień
wejściowy, jak i wyjściowy będą strumieniami buforowanymi. Wykorzystamy klasy
operujące na strumieniach binarnych BufferedOutputStream oraz BufferedInputStream.
Kod realizujący to zadanie jest widoczny na listingu 6.26.
Listing 6.26.
import java.io.*;
if(!sourceFile.exists()){
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6. System wejścia-wyjścia 321
try{
source = new BufferedInputStream(
new FileInputStream(sourceFile)
);
dest = new BufferedOutputStream(
new FileOutputStream(destFile)
);
}
catch(FileNotFoundException e){
System.out.println("Błąd podczas tworzenia strumieni: " + e);
return;
}
int b;
try{
while((b = source.read()) != -1){
dest.write(b);
}
source.close();
dest.close();
}
catch(IOException e){
System.out.print("\nBłąd wejścia-wyjścia podczas kopiowania pliku.");
return;
}
}
}
b4ac40784ca6e05cededff5eda5a930f
b
322 Java. Praktyczny kurs
Ćwiczenie 34.1.
Napisz program realizujący takie samo zadanie jak kod z listingu 6.20, nie korzystaj
jednak z klasy File.
Ćwiczenie 34.2.
Zmodyfikuj program z listingu 6.23 w taki sposób, aby w sytuacji, kiedy docelowy plik
istnieje, pytał, czy może skasować jego zawartość.
Ćwiczenie 34.3.
Zmodyfikuj program z listingu 6.26 tak, by występowało w nim wewnętrzne buforowanie
danych, tak jak w przykładzie z listingu 6.23. Sprawdź, czy pojawią się znaczące różnice
w prędkości działania programów.
Ćwiczenie 34.4.
Napisz program, który wyświetli zawartość pliku tekstowego w taki sposób, że poszcze-
gólne wiersze będą ponumerowane. Numeracja powinna zaczynać się od 1 lub od wartości
przekazanej jako parametr wywołania programu.
Ćwiczenie 34.5.
Napisz program, który będzie sumował liczby zapisane w pliku tekstowym i wyświetlał
wynik na ekranie. Nazwa pliku z danymi do sumowania powinna być podana w wierszu
poleceń jako argument wywołania aplikacji.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7.
Kontenery i typy
uogólnione
W każdym programie trzeba przechowywać najrozmaitsze dane, wykorzystując w tym
celu różne struktury, takie jak tablice, listy czy stosy. Java wspomaga programistę
w realizacji tego typu zadań, oferując cały zestaw klas użytkowych mogących służyć za
kontenery na dane. Rozdział 7. jest poświęcony właśnie podstawom kontenerów oraz
typów uogólnionych. O ile kontenery, omówione w lekcji 35., dostępne są w Javie
praktycznie od jej początków, o tyle typy uogólnione, którym poświęcona jest lekcja 36.,
są stosunkowo nową funkcjonalnością wprowadzoną w Java 5 (1.5). To dosyć rozle-
gły i bardziej zaawansowany temat niż dotychczas omawiane, jednak każdy, kto poważnie
myśli o programowaniu w Javie, musi znać przynajmniej jego podstawy.
b4ac40784ca6e05cededff5eda5a930f
b
324 Java. Praktyczny kurs
Listing 7.1.
public class TablicaInt{
private int tab[];
TablicaInt(int size){
tab = new int[size];
}
public int get(int index){
if(index >= tab.length || index < 0){
throw new ArrayIndexOutOfBoundsException("index = " + index);
}
else{
return tab[index];
}
}
public void set(int index, int value){
if(index < 0){
throw new ArrayIndexOutOfBoundsException("index = " + index);
}
if(index >= tab.length){
resize(index + 1);
}
tab[index] = value;
}
protected void resize(int size){
int newTab[] = new int[size];
for(int i = 0; i < tab.length; i++){
newTab[i] = tab[i];
}
tab = newTab;
}
public int size(){
return tab.length;
}
}
Klasa zawiera jedno prywatne pole tab, któremu w konstruktorze jest przypisywana
nowo utworzona tablica liczb typu int. Rozmiar tej tablicy jest określony przez argu-
ment konstruktora. Do pobierania danych służy metoda get, która przyjmuje jeden ar-
gument — index — określający indeks wartości, jaka ma zostać zwrócona. Indeks po-
bieranego elementu nie może być w tym przypadku większy niż całkowity rozmiar
wewnętrznej tablicy pomniejszony o 1 (jak pamiętamy, elementy tablicy są indeksowa-
ne od 0) ani też mniejszy od 0. Jeśli zatem indeks znajduje się poza zakresem, gene-
rowany jest wyjątek ArrayIndexOutOfBoundsException:
throw new ArrayIndexOutOfBoundsException("index = " + index);
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7. Kontenery i typy uogólnione 325
Jeśli zaś argument przekazany metodzie jest poprawny, zwrócona zostaje wartość od-
czytana spod wskazanego indeksu tablicy:
return tab[index];
Metoda set przyjmuje dwa argumenty. Pierwszy — index — określa indeks elementu,
który ma zostać zapisany, drugi — value — wartość, która ma się znaleźć pod tym in-
deksem. W tym przypadku na początku sprawdzamy, czy argument index jest mniej-
szy od 0, a jeśli tak, zgłaszamy wyjątek ArrayIndexOutOfBoundsException. Jest to jasne
działanie, nie można bowiem zapisywać ujemnych indeksów (chociaż ciekawym
rozwiązaniem byłoby wprowadzenie takiej możliwości, co będzie dobrym ćwiczeniem
do samodzielnego wykonania). Inaczej będzie w przypadku, gdy okaże się, że indeks
przekracza aktualny rozmiar tablicy. Skoro tablica ma dynamicznie zwiększać swoją
wielkość w zależności od potrzeb, taka sytuacja jest w pełni poprawna. Zwiększamy
wtedy tablicę, wywołując metodę resize1. Na końcu metody set przypisujemy war-
tość wskazaną przez value komórce określonej przez index:
tab[index] = value;
Pozostało więc przyjrzeć się metodzie resize. Nie wykonuje ona żadnych skompli-
kowanych czynności. Skoro nie można zwiększyć rozmiaru już raz utworzonej tablicy,
metoda ta tworzy nową tablicę o rozmiarze wskazanym przez argument size:
int newTab[] = new int[size];
W kodzie klasy znajduje się również metoda size, która zwraca aktualny rozmiar tablicy.
O tym, że nowa klasa działa prawidłowo, można się przekonać, dodając do niej metodę
main, testującą obiekt typu TablicaInt. Przykładowy kod takiej metody został przed-
stawiony na listingu 7.2, a efekt jego działania — na rysunku 7.1.
Listing 7.2.
public static void main(String args[]){
TablicaInt tab = new TablicaInt(2);
tab.set(0, 1);
tab.set(1, 2);
tab.set(2, 3);
for(int i = 0; i < tab.size(); i++){
System.out.println("tab[" + i + "] = " + tab.get(i) + " ");
}
tab.get(3);
}
1
Warto zauważyć, że w przedstawionym przykładzie tablica jest powiększana tylko do rozmiaru
wynikającego ze zwiększenia największego indeksu o 1. Przy wstawianiu dużej liczby danych do
tablicy o małym rozmiarze początkowym odbije się to niekorzystnie na wydajności. Warto samodzielnie się
zastanowić, jak temu zapobiec (patrz też podpunkt „Ćwiczenia do samodzielnego wykonania”).
b4ac40784ca6e05cededff5eda5a930f
b
326 Java. Praktyczny kurs
Na początku tworzony jest nowy obiekt klasy TablicaInt o rozmiarze dwóch elementów,
a następnie trzykrotnie wywoływana jest metoda set. Pierwsze dwa wywołania ustawiają
indeksy 0 i 1, zatem operują na istniejących od początku komórkach. Trzecie wywo-
łanie powoduje ustawienie elementu o indeksie 2. Pierwotnie takiej komórki nie było
w tablicy. Ponieważ jednak metoda set potrafi dynamicznie zwiększać rozmiar tablicy,
również trzecia instrukcja tab.set jest wykonywana poprawnie. Zawartość obiektu
tab jest następnie wyświetlana na ekranie za pomocą pętli for. Ostatnia instrukcja pro-
gramu to próba pobrania przy użyciu metody get elementu o indeksie 3. Ponieważ taki
element nie istnieje, instrukcja ta powoduje wygenerowanie wyjątku.
Klasa TablicaInt może przechowywać tylko wartości typu int, czyli liczby całkowite. Co
zrobić, gdy trzeba zapisywać wartości innych typów? Można np. napisać kolejną klasę.
Co jednak w sytuacji, gdy przechowywane mają być wartości różnych typów? Jest to
jak najbardziej możliwe. Jak pamiętamy, każdy typ obiektowy dziedziczy bezpośrednio
lub pośrednio po klasie Object. Wiemy też już wiele o dziedziczeniu, rzutowaniu typów
i polimorfizmie. A zatem wystarczy, aby nasza klasa Tablica przechowywała referencje
do typu Object. Wtedy będą mogły być w niej zapisywane obiekty dowolnych typów.
Spójrzmy na listing 7.3.
Listing 7.3.
public class Tablica {
private Object tab[];
Tablica(int size){
tab = new Object[size];
}
public Object get(int index){
if(index >= tab.length || index < 0){
throw new ArrayIndexOutOfBoundsException("index = " + index);
}
else{
return tab[index];
}
}
public void set(int index, Object value){
if(index < 0){
throw new ArrayIndexOutOfBoundsException("index = " + index);
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7. Kontenery i typy uogólnione 327
Struktura tego kodu jest bardzo podobna do przedstawionej na listingach 7.1 i 7.2, bo
bardzo podobna jest też zasada działania. Metody get, set, size i resize wykonują
analogiczne czynności, zmienił się natomiast typ przechowywanych danych, którym
teraz jest Object. Tak więc metoda get, pobierająca dane, przyjmuje wartość typu int
określającą indeks żądanego elementu i zwraca wartość typu Object, a metoda set przyj-
muje dwa argumenty — pierwszy typu int określający indeks komórki do zmiany i drugi
typu Object określający nową wartość komórki.
Taka klasa będzie pracowała ze wszystkimi typami danych, nawet typami prostymi.
Widać to wyraźnie w metodzie main. Dwa pierwsze wywołania tab.set powodują za-
pisanie w komórkach o indeksach 0 i 1 wartości 1 i 2. Jest to możliwe, mimo że metoda
set oczekuje wartości typu Object. W takiej sytuacji następuje po prostu „zapakowanie”
typu prostego w obiekt klasy opakowującej. W tym przypadku będzie to klasa Integer
(każdy typ prosty ma właściwą sobie klasę opakowującą, dzięki czemu jego wartość może
być używana wszędzie tam, gdzie jest oczekiwany typ Object). Trzecia metoda set
powoduje umieszczenie w tablicy obiektu typu Object. Dalsze instrukcje działają tak
samo jak w poprzednim przykładzie. Po kompilacji i uruchomieniu zobaczymy więc
widok podobny do przedstawionego na rysunku 7.2. To dowód na to, że obiekt klasy
Tablica potrafi nie tylko dynamicznie zwiększać swoją pojemność, ale też przecho-
wywać dane różnych typów.
b4ac40784ca6e05cededff5eda5a930f
b
328 Java. Praktyczny kurs
Klasy kontenerowe
W poprzedniej części lekcji zostały pokazane proste przykłady prezentujące, w jaki
sposób można tworzyć obiekty przechowujące dane, dynamicznie zwiększające swoją
pojemność w miarę potrzeby. Na szczęście wcale nie trzeba tworzyć samodzielnie no-
wych klas tego typu — wraz z JDK w pakiecie java.util został dostarczony cały ich
zestaw. Określa się je jako klasy kontenerowe lub kolekcje. Odzwierciedlają one naj-
rozmaitsze struktury danych, takie jak tablice, tablice asocjacyjne, zbiory, stosy, kolejki,
drzewa itp. Oczywiście wszystkie pozwalają na dynamiczne dodawanie i usuwanie da-
nych bez ograniczeń wielkości. Nie ma tu niestety miejsca na dokładne omówienie
wszystkich klas kontenerowych występujących w Javie, przyjrzyjmy się jednak sposobom
wykorzystania dynamicznej tablicy oraz stosu.
Klasa ArrayList
Klasę ArrayList można potraktować jako dynamiczny wektor elementów (dynamiczną
tablicę)2. Wybrane metody tej klasy zostały przedstawione w tabeli 7.1. Pewne zdzi-
wienie może budzić użycie w przypadku niektórych metod nazwy E jako typu danych.
Należy go jednak na razie potraktować jako dowolny typ obiektowy; kwestia ta zostanie
dokładniej wyjaśniona w kolejnej lekcji.
ArrayList(int initialCapacity).
Pierwszy z nich tworzy listę o pierwotnej wielkości równej dziesięciu elementom, drugi
pozwala na samodzielne ustalenie tej wartości3. Jeśli więc wiadomo, że lista będzie prze-
chowywała dużo większą liczbę elementów, należy skorzystać z drugiej wersji konstrukto-
ra, aby uniknąć zbędnych operacji powiększania pojemności w trakcie dodawania danych.
2
W rzeczywistości jest to implementacja interfejsu List, która wewnętrznie do przechowywania danych
wykorzystuje tablicę, w odróżnieniu np. od LinkedList, gdzie do przechowywania danych jest
używana lista dwukierunkowa.
3
Istnieje także trzeci konstruktor pozwalający na skopiowanie elementów z innego obiektu
implementującego interfejs Collection.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7. Kontenery i typy uogólnione 329
Na listingu 7.4 został zaprezentowany prosty program, który wykorzystuje obiekt Array
List do przechowywania danych wprowadzanych przez użytkownika z klawiatury,
a następnie wyświetla je na ekranie.
Listing 7.4.
import java.io.*;
import java.util.*;
b4ac40784ca6e05cededff5eda5a930f
b
330 Java. Praktyczny kurs
list.add(line);
}
System.out.println("Koniec wczytywania danych.");
}
catch(IOException e){
System.out.println("Błąd podczas odczytu strumienia.");
}
System.out.println("Wprowadzone zostały następujące dane: ");
int size = list.size();
for(int i = 0; i < size; i++){
line = list.get(i).toString();
System.out.println("[" + i + "] = " + line);
}
}
}
Najpierw za pomocą pierwszego typu konstruktora tworzony jest obiekt list klasy
ArrayList, a następnie obiekt brIn klasy BufferedReader służący do odczytywania
danych ze standardowego strumienia wejściowego. Pobieranie danych odbywa się tak
samo jak w przykładach z lekcji 31. — przy użyciu pętli while. Każda odczytana linia
jest dodawana do listy za pomocą metody add:
list.add(line);
Proces ten kończy się wraz z wprowadzeniem z klawiatury ciągu znaków quit (ten
ciąg również jest dodawany do listy).
Rysunek 7.3.
Kompilator informuje
o braku kontroli typów
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7. Kontenery i typy uogólnione 331
Rysunek 7.4.
Przykładowy efekt
działania programu
z listingu 7.4
Klasa Stack
Klasa Stack to implementacja struktury danych nazywanej stosem. Funkcjonuje ona
w taki sposób, że dane umieszcza się na stosie za pomocą metody push, a zdejmuje z nie-
go przy użyciu metody pop (metody klasy Stack zostały zebrane w tabeli 7.2). Dostęp
mamy jednak tylko do ostatnio dodanego elementu. Tak więc dane wprowadzone na
stos w pewnej kolejności będą z niego pobierane zawsze w kolejności odwrotnej. Klasa
Stack udostępnia tylko jeden bezargumentowy konstruktor, a jak w praktyce wygląda
jej wykorzystanie, pokazano w kodzie z listingu 7.5.
Listing 7.5.
import java.io.*;
import java.util.*;
b4ac40784ca6e05cededff5eda5a930f
b
332 Java. Praktyczny kurs
}
catch(IOException e){
System.out.println("Błąd podczas odczytu strumienia.");
}
System.out.println("Wprowadzono następujące dane: ");
while(!stack.empty()){
line = stack.pop().toString();
System.out.println(line);
}
}
}
Po zakończeniu tej procedury następuje odczyt zawartości stosu — odbywa się to w pętli
while. Pętla ta zakończy działanie, kiedy stos będzie pusty, co jest sprawdzane za po-
mocą metody empty (metoda zwraca wartość true, gdy stos jest pusty). Wewnątrz pętli
jest wykonywana metoda pop, która zdejmuje obiekt ze stosu i zwraca go jako wynik
swojego działania. Na rzecz tego obiektu jest następnie wywoływana metoda toString,
a wynik całej operacji zostaje przypisany zmiennej line:
line = stack.pop().toString();
Rysunek 7.5.
Przykładowy efekt
działania aplikacji
wykorzystującej
klasę Stack
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7. Kontenery i typy uogólnione 333
Przeglądanie kontenerów
W przykładach zamieszczonych w ramach tej lekcji do przeglądania i odczytywania
danych z obiektów klas kontenerowych (ArrayList, Stack) wykorzystywane były dotąd
pętle for i while. Jak jednak wiadomo z lekcji 7., istnieje także rozszerzona wersja pętli
for, zwana również pętlą foreach. Ten właśnie rodzaj pętli doskonale nadaje się do
przeglądania kolekcji obiektów. Spójrzmy na listing 7.6.
Listing 7.6.
import java.util.*;
Powstał tu obiekt list typu ArrayList, do którego za pomocą metody add zostały do-
dane trzy elementy. Elementy te zostały następnie wyświetlone na ekranie przy użyciu
rozszerzonej pętli for oraz instrukcji System.out.println. Jak wiadomo z lekcji 7.,
w każdym przebiegu tej pętli pod zmienną o będzie podstawiany kolejny element listy
(ogólnie — kolejny element kolekcji obiektów). Zauważmy, że jest to bardzo wygodne
w sytuacji, kiedy niezbędne okazuje się przejrzenie i odczytanie wszystkich obiektów,
nie trzeba bowiem ani znać rozmiaru kolekcji (por. listing 7.4), ani też testować, czy od-
czytano już wszystkie elementy (por. listing 7.5).
Dociekliwy programista zada jednak w tym miejscu pytanie, skąd właściwie rozszerzo-
na pętla for „wie”, jak pobierać kolejne elementy, skoro można ją stosować do wielu
różnych klas kontenerowych, a każda z tych klas może inaczej przechowywać dane.
Odpowiedź jest następująca: rozszerzona pętla for potrafi przeglądać elementy każdego
obiektu implementującego interfejs Iterable, a większość klas kontenerowych ten
interfejs implementuje.
Interfejs Iterable wymusza definiowanie tylko jednej metody — iterator. Jej zadaniem
jest zwrócenie tzw. obiektu iteratora, tj. obiektu implementującego interfejs Iterator
i pozwalającego na przeglądanie kolekcji. W interfejsie Iterator zdefiniowane są z kolei
trzy metody przedstawione w tabeli 7.3. Powróćmy więc do klasy Tablica, której kod zo-
stał napisany na początku tej lekcji (listing 7.3), i sprawmy, aby mogła korzystać z dobro-
dziejstw rozszerzonej pętli for. Odpowiedni kod został zaprezentowany na listingu 7.7.
b4ac40784ca6e05cededff5eda5a930f
b
334 Java. Praktyczny kurs
Listing 7.7.
import java.util.*;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7. Kontenery i typy uogólnione 335
Klasa Tablica implementuje teraz interfejs Iterable, a więc ma też zdefiniowaną metodę
iterator. Jedynym zadaniem tej metody jest zwrócenie obiektu implementującego
interfejs Iterator. W tym przypadku jest to obiekt wewnętrznej klasy anonimowej
utworzonej w sposób prezentowany już na stronach tej książki (lekcja 30.). Ponieważ
jest to klasa wewnętrzna, ma dostęp do składowych klasy zewnętrznej (czyli Tablica), tak
więc jej metody mają dostęp do danych przechowywanych w obiekcie tab. Zadanie obiektu
iteratora jest bardzo proste — ma on pozwolić na przejrzenie wszystkich elementów
przechowywanych przez obiekt klasy Tablica. Dlatego też w klasie anonimowej zostało
zdefiniowane pole position, które będzie zawierało indeks aktualnie przeglądanego
elementu oraz wymuszone przez interfejs Iterator metody hasNext i next.
Metoda hasNext ma zwrócić wartość true, jeśli istnieje kolejny element w kolekcji, lub
false, jeśli więcej elementów nie ma. Tak więc wartość false będzie zwracana albo
przy pierwszym wywołaniu hasNext, kiedy w kolekcji (tablicy) nie ma żadnych obiektów,
albo też jeśli w kolekcji są obiekty, ale wcześniejsze wywołanie metody next zwróciło
ostatni obiekt. W naszym przypadku sprawdzenie, czy został osiągnięty koniec kolekcji,
odbywa się za pomocą warunku position < tab.length. Gdy jest on prawdziwy, istnieją
dalsze elementy, gdy jest fałszywy — nie istnieją. Odpowiednia wartość jest zwracana
przy użyciu instrukcji:
return position < tab.length;
W pierwszej chwili może się to wydawać nie w pełni czytelne, ale jest to odpowiednik
instrukcji warunkowej if w postaci:
if(position < tab.length)
return true;
else
return false;
Metoda next ma za zadanie zwrócić kolejny obiekt kolekcji, przy pierwszym wywoła-
niu pierwszy obiekt, przy drugim — drugi itd. W tym przypadku ma to być obiekt o in-
deksie wskazywanym przez pole position, jednocześnie niezbędne jest zwiększenie
wartości position o 1 (tak by wskazywała kolejny element kolekcji). Wszystko to od-
bywa się w jednej instrukcji:
return tab[position++]
Metoda remove nie jest nam do niczego potrzebna i nie będzie używana, dlatego też jedyną
czynnością w jej wnętrzu jest zgłoszenie wyjątku UnsupportedOperationException.
b4ac40784ca6e05cededff5eda5a930f
b
336 Java. Praktyczny kurs
Pozostałe elementy klasy Main działają w taki sam sposób jak w przykładzie z listingu
7.3. Z kolei metoda main testuje zachowanie obiektów wraz z pętlą foreach, dzięki czemu
można się przekonać, że implementacja interfejsu Iterable zakończyła się sukcesem.
Ćwiczenie 35.1.
Zmodyfikuj kod z ćwiczenia 7.1 tak, aby podczas wstawiania dużej liczby elementów
przy małym początkowym rozmiarze tablicy nie występowało niekorzystne zjawisko bar-
dzo częstej realokacji danych.
Ćwiczenie 35.2.
Napisz program wypełniający obiekt klasy TablicaInt (z listingu 7.1) liczbami od 1
do 1000, wykorzystujący do tego celu pętlę for. Początkowym rozmiarem tablicy ma
być 1. Pętla ma mieć taką postać, aby nastąpiła co najwyżej jedna realokacja danych.
Ćwiczenie 35.3.
Zmodyfikuj program z listingu 7.5 w taki sposób, by dane były przechowywane w obiek-
cie klasy Stack, ale zostały wyświetlone w takiej samej kolejności, w jakiej zostały
wprowadzone.
Ćwiczenie 35.4.
Zmodyfikuj implementację interfejsu Iterable z listingu 7.7 w taki sposób, aby zastoso-
wana rozszerzona pętla for wyświetlała dane w odwrotnej kolejności (tzn. od ostatniego
elementu do pierwszego).
Ćwiczenie 35.5.
Napisz program wczytujący z konsoli wiersze tekstu wprowadzane przez użytkow-
nika. Odczytane dane powinny zostać następnie wyświetlone w następującej kolejności:
1. teksty reprezentujące liczby całkowite, 2. teksty reprezentujące liczby rzeczywiste,
3. pozostałe teksty.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7. Kontenery i typy uogólnione 337
wykracza poza ramy tematyczne niniejszej publikacji, ponieważ jednak każdy progra-
mista musi znać przynajmniej podstawy tego zagadnienia, lekcja 36. została poświęcona
niezbędnym podstawom.
Listing 7.8.
public class Tablica {
/*tutaj treść klasy Tablica z listingu 7.3*/
public static void main(String args[]){
Tablica rectangles = new Tablica(3);
rectangles.set(0, new Rectangle());
rectangles.set(1, new Rectangle());
rectangles.set(2, new Triangle());
for(int i = 0; i < tab.size(); i++){
((Rectangle) rectangles.get(i)).diagonal();
}
}
}
class Triangle{};
class Rectangle{
public void diagonal(){}
}
W metodzie main został utworzony obiekt rectangles typu Tablica, którego zadaniem,
jak można się domyślić po samej nazwie, jest przechowywanie obiektów klasy Rectangle
(prostokąt). Za pomocą metody set zostały do niego dodane trzy elementy. Następnie
w pętli for została wywołana metoda diagonal każdego z pobranych obiektów. Ponie-
waż metoda get zwraca obiekt typu Object, przed wywołaniem metody set niezbędne
było dokonanie rzutowania na typ Rectangle. Wszystko działałoby oczywiście prawi-
dłowo, gdyby nie to, że trzecia instrukcja set spowodowała umieszczenie w obiekcie
rectangles obiektu typu Triangle, który nie ma metody diagonal (trójkąt, ang. triangle,
nie ma przekątnej, ang. diagonal). Kompilator nie ma jednak żadnej możliwości wy-
chwycenia takiego błędu — skoro klasa Tablica może przechowywać dowolne typy
obiektowe, to typem drugiego argumentu metody set jest Object. Zawsze więc będzie
wykonywane rzutowanie na typ Object (czyli formalnie trzecia instrukcja set jest trakto-
wana jako rectangles.set(2, (Object) new Triangle())). Błąd ujawni się zatem dopiero
w trakcie wykonywania programu: przy próbie rzutowania trzeciego elementu pobranego
z kontenera rectangles na typ Rectangle zostanie zgłoszony wyjątek ClassCastException,
tak jak jest to widoczne na rysunku 7.6.
b4ac40784ca6e05cededff5eda5a930f
b
338 Java. Praktyczny kurs
Przedstawiony problem dotyczy nie tylko tej wersji klasy Tablica. Jeśli w podobny spo-
sób wykorzystamy kontener ArrayList, efekt będzie taki sam. Spójrzmy na listing 7.9.
W metodzie main została utworzona lista typu ArrayList, do której zostały dodane
dwa obiekty, pierwszy klasy Rectangle i drugi klasy Triangle. Następnie w pętli typu
foreach została wywołana metoda diagonal, oczywiście po wcześniejszym rzutowaniu
każdego pobranego z kontenera obiektu. Po uruchomieniu efekt będzie taki sam jak w po-
przednim przykładzie — wystąpi wyjątek ClassCastException.
Listing 7.9.
import java.util.*;
class Triangle{};
class Rectangle{
public void diagonal(){}
}
Oczywiście można powiedzieć, że sami jesteśmy sobie winni. Skoro bowiem nieuważnie
wrzuciliśmy do kontenera obiekt niewłaściwego typu, to nie możemy mieć pretensji,
że aplikacja nie działa. Jak zapobiegać takim problemom? Pomysłów może być kilka,
od zupełnie fatalnych, jak np. pisanie osobnych klas kontenerowych dla każdego typu
danych, po takie jak sprawdzanie pobieranego typu danych w trakcie wykonania progra-
mu, co sprawdzi się w praktyce, choć będzie wymagało zarówno większej pracy pro-
gramisty, jak i większego narzutu związanego z wykonywaniem dodatkowych instrukcji
weryfikujących. Najwygodniejsze byłoby rozwiązanie polegające na możliwości prze-
chowywania każdego typu danych, a przy tym przerzucenie części zadań związanych
z kontrolą typów na kompilator. Jest to możliwe dzięki typom uogólnionym.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7. Kontenery i typy uogólnione 339
Listing 7.10.
import java.util.*;
b4ac40784ca6e05cededff5eda5a930f
b
340 Java. Praktyczny kurs
class Triangle{};
class Rectangle{
public void diagonal(){}
}
Tym razem przy tworzeniu zmiennej list oraz obiektu klasy ArrayList zostało zaznaczone,
że będzie on przechowywał obiekty klasy Rectangle. Typ Rectangle został umieszczony
w nawiasie kątowym, zarówno w deklaracji zmiennej list:
ArrayList<Rectangle> list
spowodowałaby błąd kompilacji widoczny na rysunku 7.8 (w wersjach Javy starszych niż
7 komunikat będzie dużo mniej szczegółowy; w Java 7 i starszych nie będzie również
informacji o opcji -Xdiags). Tym razem kompilator jest bowiem w stanie stwierdzić,
że nastąpiła próba umieszczenia w kontenerze nieprawidłowego typu danych. Warto też
zwrócić uwagę na pętlę for, w której nie ma potrzeby wykonywania rzutowań, po-
nieważ typ danych jest jasno określony. Tak samo dzieje się w przypadku instrukcji:
list.get(0).diagonal();
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7. Kontenery i typy uogólnione 341
Można bezpośrednio wywołać metodę diagonal bez potrzeby rzutowania na typ Rectangle,
ponieważ po odpowiedniej deklaracji kontenera metoda get będzie zwracała obiekty
typu Rectangle.
Znaczenie jest takie samo jak przy zapisie z listingu 7.10. W tej sytuacji mówimy o do-
określaniu typu lub inferowaniu typu (ang. type inference).
Stosowanie uogólnień
W poprzednim podpunkcie zostało pokazane, w jaki sposób używać klas korzystających
z uogólnień, ale trzeba również wiedzieć, jak taką klasę napisać. Zacznijmy jednak od
napisania kodu klasy, która mogłaby przechowywać obiekty dowolnego typu. Można
ją traktować jako opakowanie, dlatego też nazwiemy ją po prostu Opakowanie. Kod jest
widoczny na listingu 7.11.
Listing 7.11.
public class Opakowanie {
private Object data;
Opakowanie(Object value) {
set(value);
}
public Object get() {
return data;
}
public void set(Object value) {
this.data = value;
}
public static void main(String args[]){
Opakowanie opk1 = new Opakowanie(new Rectangle());
Opakowanie opk2 = new Opakowanie(new Triangle());
((Rectangle) opk1.get()).diagonal();
((Rectangle) opk2.get()).diagonal();
}
}
class Triangle{};
class Rectangle{
public void diagonal(){}
}
Dane przechowywane są w prywatnym polu data, którego typem jest Object. Pobie-
raniem danych zajmuje się metoda get, a ustawianiem — set. Metoda set przyjmuje
jeden argument typu Object i przypisuje jego wartość polu data, natomiast metoda
b4ac40784ca6e05cededff5eda5a930f
b
342 Java. Praktyczny kurs
get jest bezargumentowa i zwraca wartość typu Object. Oprócz tego do dyspozycji
mamy również typowy publiczny konstruktor, a także znane nam już dobrze dwie pa-
kietowe klasy pomocnicze, Triangle i Rectangle.
Było to rzutowanie w górę, które może być wykonywane automatycznie (temat ten był
omawiany w lekcji 23.). W dalszej części programu z obiektów opk1 i opk2 zostały pobra-
ne przechowywane przez nie dane. Ze względu na to, że metoda get zwraca wartości
typu Object, w obu przypadkach niezbędne było wykonanie rzutowania w dół — do
typu Rectangle. Tylko dzięki temu można było wywołać metodę diagonal. Oczywiście
złożoną instrukcję:
((Rectangle) opk1.get()).diagonal();
wydaje się jednak, że na tym etapie nauki Javy nie ma już takiej potrzeby.
Rzecz jasna wiemy już dobrze, że kompilacja takiego kodu uda się bez problemów, nato-
miast w trakcie wykonania pojawi się wyjątek ClassCastException, jako że w opk2
został zapisany obiekt typu Triangle, który nie może być rzutowany na typ Rectangle,
a tym bardziej nie może zostać wykonana metoda diagonal.
Listing 7.12.
public class Opakowanie<T> {
private T data;
Opakowanie(T value) {
set(value);
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7. Kontenery i typy uogólnione 343
public T get() {
return data;
}
public void set(T value) {
this.data = value;
}
}
Za nazwą klasy w nawiasie kątowym pojawił się symbol T. To informacja, że klasa będzie
korzystała z typów uogólnionych oraz że symbolem typu uogólnionego jest T (można
ten symbol zmienić na dowolny inny, niekoniecznie jednoliterowy). Ten symbol został
umieszczony w każdym miejscu kodu klasy, gdzie wcześniej występował konkretny
typ przechowywanych danych (był to typ Object). Tak więc instrukcja
private T data;
oznacza, że w klasie znajduje się prywatne pole, którego typ zostanie określony do-
piero w kodzie aplikacji przy tworzeniu obiektu klasy Opakowanie. Deklaracja:
public T get() {
oznacza metodę get zwracającą typ danych, który zostanie dokładniej określony przy
tworzeniu obiektu klasy Opakowanie itd. Każde wystąpienie T to deklaracja, że właściwy
typ danych będzie określony później. Dzięki temu powstała uniwersalna klasa, któ-
ra może przechowywać dowolne dane, ale która pozwala na kontrolę typów już w mo-
mencie kompilacji. Sprawdźmy to, dopisując do kodu metodę main oraz klasy Triangle
i Rectangle, tak jak jest to widoczne na listingu 7.13.
Listing 7.13.
public class Opakowanie<T> {
/*tutaj początek kodu klasy Opakowanie z listingu 7.12*/
public static void main(String args[]){
Opakowanie<Rectangle> opk1 = new Opakowanie<Rectangle>(
new Rectangle()
);
/*Opakowanie<Rectangle> opk2 = new Opakowanie<Rectangle>(
new Triangle()
);*/
Opakowanie<Triangle> opk3 = new Opakowanie<Triangle>(
new Triangle()
);
opk1.get().diagonal();
//opk2.get().diagonal();
opk3.get().type();
}
}
class Triangle{
public void type(){}
}
class Rectangle{
public void diagonal(){}
}
b4ac40784ca6e05cededff5eda5a930f
b
344 Java. Praktyczny kurs
(na listingu to wywołanie zostało rozbite na trzy wiersze). Począwszy od Java 8, wystar-
czy typ przechowywanego obiektu zaznaczyć tylko przy deklaracji zmiennej. Dzięki
temu kompilator „wie”, że obiekt opk1 ma przechowywać obiekty klasy Rectangle (lub
klas pochodnych od Rectangle). Bez problemu może więc zostać wykonana instrukcja:
opk1.get().diagonal();
jako że metoda get zwraca w tym przypadku dane typu Rectangle, a obiekt takiego
typu zawiera metodę diagonal.
Rysunek 7.9. Próba zastosowania niewłaściwego typu kończy się błędem kompilacji
Jeśli więc chcemy, aby obiekt klasy Opakowanie przechowywał obiekty klasy Triangle,
zaznaczamy to przy ich tworzeniu. Zostało to pokazane w instrukcji:
Opakowanie<Triangle> opk3 = new Opakowanie<Triangle>(new Triangle());
Oznacza ona, że obiekt opk3 będzie współpracował z klasą Triangle. Prawidłowa jest
też zatem instrukcja:
opk3.get().type();
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7. Kontenery i typy uogólnione 345
jako że w tym przypadku metoda get zwróci wartość typu Triangle, a w klasie Triangle
istnieje metoda type.
Uogólnianie metod
Z uogólnień typów mogą korzystać nie tylko klasy, ale także poszczególne metody
w ramach tych klas. Uogólnianie metod jest jednak niezależne od uogólnień klas, więc
w klasie uogólnionej mogą się znajdować nieuogólnione metody, a także uogólnione
metody mogą się znajdować w zwykłych klasach. Jeśli metoda ma operować na ar-
gumencie typu uogólnionego, to specyfikację tego typu należy umieścić przed typem
zwracanym przez metodę. Wygląda to tak:
modyfikator_dostępu <specyfikacja_typu_ogólnego> typ_zwracany
nazwa_metody(argumenty)
Listing 7.14.
public class Main {
public <T> void show (T value){
System.out.println(value.toString());
}
public static void main(String args[]){
Main main = new Main();
main.show(1);
main.show(0.54);
main.show("Przykładowy tekst");
main.show(new Main());
}
}
Jest to klasa Main zawierająca metodę show. Za modyfikatorem dostępu tej metody
(public), a przed typem zwracanym (void) znajduje się deklaracja typu uogólnionego
(<T>). Argumentem metody show jest value o typie T. Tak więc w praktyce będzie to
b4ac40784ca6e05cededff5eda5a930f
b
346 Java. Praktyczny kurs
mógł być typ dowolny. Pokazuje to metoda main, w której metoda show została wy-
wołana z różnymi argumentami. W pierwszym przypadku jest to argument typu int,
w drugim — double, w trzecim — String, a w czwartym — nowy obiekt klasy Main.
Ponieważ zadaniem metody show jest konwersja przekazanej jej wartości (przez wy-
wołanie metody toString) na typ String i wyświetlenie jej na ekranie, po uruchomie-
niu aplikacji zobaczymy widok przedstawiony na rysunku 7.10.
Rysunek 7.10.
Efekt działania
programu z listingu 7.14
Listing 7.15.
public class Main {
public Opakowanie getValues(){
return new Opakowanie(new Triangle(), new Rectangle());
}
public static void main(String args[]){
Main main = new Main();
Opakowanie opk = main.getValues();
((Triangle)opk.pole1).type();
((Rectangle)opk.pole2).diagonal();
}
}
class Opakowanie{
Object pole1;
Object pole2;
Opakowanie(Object pole1, Object pole2){
this.pole1 = pole1;
this.pole2 = pole2;
}
}
class Triangle{
public void type(){}
}
class Rectangle{
public void diagonal(){}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7. Kontenery i typy uogólnione 347
Klasa Opakowanie zawiera dwa pola typu Object, dzięki temu może przechowywać
obiekty dowolnych typów. Zawiera także konstruktor pozwalający na inicjalizację obu
pól. Z tych właściwości korzysta metoda getValues z klasy Main. Zadaniem tej meto-
dy jest zwrócenie dwóch wartości typów Triangle i Rectangle. Odbywa się to przez utwo-
rzenie nowego obiektu klasy Opakowanie i zwrócenie go za pomocą instrukcji return:
new Opakowanie(new Triangle(), new Rectangle());
Działanie kodu jest testowane w metodzie main, w której najpierw jest tworzony
obiekt klasy Main:
Main main = new Main();
Następnie jest wywoływana metoda getValues, a wynik jej działania jest przypisywany
zmiennej opk:
Opakowanie opk = main.getValues();
Ponieważ pole pole1 klasy Opakowanie miało być klasy Triangle, po wykonaniu rzuto-
wania można wywołać metodę type tej klasy:
((Triangle)opk.pole1).type();
i analogicznie, ze względu na to, że pole pole2 miało być klasy Rectangle, po dokona-
niu rzutowania na tę klasę można wywołać metodę diagonal:
((Rectangle)opk.pole2).diagonal();
Oczywiście można byłoby napisać specyficzną wersję klasy Opakowanie, która prze-
chowywałaby jedynie wartości typów Rectangle i Triangle, jednak straciłoby się wtedy
jej uniwersalność, a w założeniu miała ona wciąż przechowywać dowolne dane. Warto
zatem wykorzystać rozwiązanie z typami uogólnionymi. Zostało ono zaprezentowane
na listingu 7.16.
Listing 7.16.
public class Main {
public Opakowanie<Triangle, Rectangle> getValues(){
return new Opakowanie<Triangle, Rectangle>(
new Triangle(), new Rectangle()
);
}
public static void main(String args[]){
Main main = new Main();
Opakowanie<Triangle, Rectangle> opk = main.getValues();
opk.pole1.type();
opk.pole2.diagonal();
}
}
b4ac40784ca6e05cededff5eda5a930f
b
348 Java. Praktyczny kurs
class Triangle{
public void type(){}
}
class Rectangle{
public void diagonal(){}
}
Przyjrzyjmy się najpierw klasie Opakowanie. Występujący za jej nazwą nawias kątowy
mówi wyraźnie, że będzie ona korzystała z typów uogólnionych. Wewnątrz nawiasu
występują dwa symbole, M i N, a zatem będą to dwa typy danych. Wewnątrz klasy
znajdują się dwa pola, typ pierwszego określa symbol M, a drugiego — N. Podobnie
wygląda definicja konstruktora, typem pierwszego argumentu będzie ten wskazany
przez M, a drugiego — przez N.
Spójrzmy teraz na metodę getValues z klasy Main. Ma ona zwrócić obiekt klasy
Opakowanie zawierający dane typów Triangle i Rectangle, stąd deklaracja typu zwraca-
nego jako Opakowanie<Triangle, Rectangle>:
public Opakowanie<Triangle, Rectangle> getValues(){
jak również w metodzie main przy deklaracji zmiennej opk, której przypisywany jest
wynik działania metody getValues:
Opakowanie<Triangle, Rectangle> opk = main.getValues();
Ponieważ tym razem zostało dokładnie określone, jakie typy będą przechowywane
przez obiekt klasy Opakowanie, nie ma potrzeby dokonywania rzutowań i można bezpo-
średnio wywoływać metody type oraz diagonal obiektów klas Triangle i Rectangle:
opk.pole1.type();
opk.pole2.diagonal();
Kontrola typów może się teraz odbywać już na etapie kompilacji i nie traci się nic z uni-
wersalności klasy Opakowanie.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7. Kontenery i typy uogólnione 349
Ćwiczenie 36.1.
Popraw kod pętli for z listingu 7.9 tak, aby program uruchamiał się bez błędów. Możesz
użyć operatora instanceof badającego, czy obiekt jest danego typu (np. obj1 instaceof
String), albo skorzystać z innego sposobu.
Ćwiczenie 36.2.
Zmodyfikuj kod z listingu 7.10 tak, by powstała klasa Shape bazowa dla Triangle
i Rectangle. Zastosuj Shape jako typ uogólniony kontenera ArrayList. Dodaj do kontenera
obiekty Triangle i Rectangle oraz zmodyfikuj pętlę for tak, aby program działał po-
prawnie.
Ćwiczenie 36.3.
Napisz klasę uogólnioną, która będzie mogła przechowywać wartości trzech różnych
typów.
Ćwiczenie 36.4.
Do klasy Opakowanie z listingu 7.16 dopisz metody pozwalające na niezależną mani-
pulację każdym polem (osobny zapis i odczyt wartości dla każdego pola).
Ćwiczenie 36.5.
Zmodyfikuj kod klasy Tablica z listingu 7.3 (lekcja 35.) tak, aby korzystała ona ze składni
typów uogólnionych. Uwaga: nie możesz utworzyć generycznej tablicy, zapis typu
new T[] jest nieprawidłowy. Zastanów się, czy tworzenie tego typu klasy ma sens.
Ćwiczenie 36.6.
Popraw kod z ćwiczenia 35.5 (z lekcji 35.) tak, by korzystał z typów uogólnionych i żeby
w związku z tym kompilator nie generował ostrzeżeń.
b4ac40784ca6e05cededff5eda5a930f
b
350 Java. Praktyczny kurs
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.
Aplikacje i aplety
Programy w Javie mogą być apletami (ang. applet) lub aplikacjami (ang. application).
Różnica jest taka, że aplikacja jest programem samodzielnym uruchamianym z poziomu
systemu operacyjnego (a dokładniej: maszyny wirtualnej Javy operującej na poziomie
systemu operacyjnego) i tym samym ma pełny dostęp do zasobów udostępnianych
przez system, natomiast aplet jest uruchamiany pod kontrolą innego programu, naj-
częściej przeglądarki internetowej, i ma dostęp jedynie do środowiska, które ten pro-
gram mu udostępni. Aplet to inaczej program zagnieżdżony w innej aplikacji. Zazwy-
czaj nie ma on dostępu np. do dysku twardego (i innych urządzeń), tak aby ściągnięty
nieświadomie z internetu nie mógł zaszkodzić użytkownikowi, np. kasując dane, chyba że
zostanie wyposażony w specjalny podpis cyfrowy i użytkownik zgodzi się na udostęp-
nienie mu chronionych zasobów. Osobną kwestią są wykrywane co jakiś czas błędy
w mechanizmach bezpieczeństwa przeglądarek i maszyn wirtualnych, które niekiedy
powodują, że złośliwie napisane aplety mogą obejść restrykcje i dostać się do chronio-
nych zasobów systemu. Nie są to jednak sytuacje bardzo częste.
Obecnie aplety straciły na znaczeniu i nie są już tak często używane, jednak są wciąż
powszechne np. na stronach prezentujących notowania giełdowe, oferujących narzędzia
do analizy technicznej związanej z grą na giełdach papierów wartościowych, a także w apli-
kacjach typu czat czy też w niektórych grach działających w przeglądarkach WWW.
W rzeczywistości różnice w konstrukcji apletu i aplikacji nie są bardzo duże, jednak roz-
dział ten dla porządku został podzielony na dwie części. W pierwszej zostaną omówione
aplety działające pod kontrolą przeglądarek, a w drugiej — samodzielne aplikacje.
Aplety
Lekcja 37. Podstawy apletów
Wszystkie przykłady programów prezentowane w poprzednich lekcjach pracowały w try-
bie tekstowym, najwyższy czas zatem przejść w świat aplikacji pracujących w trybie
graficznym. W lekcji 37. rozpoczynamy omawianie apletów, czyli niewielkich aplikacji
b4ac40784ca6e05cededff5eda5a930f
b
352 Java. Praktyczny kurs
Pierwszy aplet
Utworzenie apletu wymaga użycia klasy Applet lub, jeśli miałyby się w nim znaleźć kom-
ponenty pakietu (biblioteki) Swing, JApplet. Co prawda w pierwszych przykładach nie
będzie wykorzystywany ten pakiet, jednak aplety zostaną wyprowadzone z nowocześniej-
szej klasy JApplet. Komponenty Swing zostaną natomiast przedstawione w lekcji 42.
Przed dokładnym omówieniem budowy tego typu programów warto na rozgrzewkę napi-
sać prosty aplet, który, jakżeby inaczej, będzie wyświetlał na ekranie dowolny napis.
Kod takiego apletu jest widoczny na listingu 8.1.
Listing 8.1.
import javax.swing.JApplet;
import java.awt.*;
1
W praktyce przed wywołaniem clearRect należałoby użyć metody setColor (patrz lekcja „Kroje
pisma (fonty) i kolory”), tak aby na każdej platformie uruchomieniowej uzyskać taki sam kolor tła.
W różnych systemach domyślny kolor tła może być bowiem inny.
2
Jest to więc odwołanie do pola width obiektu zwróconego przez wywołanie metody getSize.
Jest to obiekt klasy Dimension.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 353
Wciąż jest on rozpoznawany i używany, choć nie należy do standardu HTML3. Obecnie
zamiast niego stosuje się znacznik <object>. Niestety niektóre przeglądarki (w tym
dostępny w JDK program appletviewer) z reguły wymagają używania atrybutu code,
który w przypadku <object> jest… niestandardowy (prawidłowy jest atrybut data).
Aby zatem napisać kod zgodny ze standardami, i tak trzeba użyć rozwiązań niezgodnych
formalnie ze standardem HTML. Z reguły stosuje się znacznik <object> w postaci po-
dobnej do <applet>:
<object
type="application/x-java-applet"
code = "nazwa klasy"
width = "szerokość apletu"
height = "wysokość apletu">
</object>
Listing 8.2.
<object
type="application/x-java-applet"
code="PierwszyAplet.class"
width="320"
height="200">
</object>
Ten znacznik należy umieścić w kodzie strony HTML4. Taka przykładowa strona5 została
przedstawiona na listingu 8.36.
3
W niektórych wersjach JDK zawarte jest narzędzie o nazwie HtmlConverter służące do konwersji
kodu HTML, które potrafi automatycznie przetworzyć przestarzały kod ze znacznikami <applet>
na kod zgodny ze współczesnymi standardami.
4
Znacznik <object> może również zawierać inne atrybuty niż przedstawione w kodzie, jednak nie są
one istotne dla dalszych rozważań.
5
W książce nie ma miejsca na omawianie standardów HTML i sposobów tworzenia stron internetowych.
Osoby zainteresowane tym tematem mogą jednak sięgnąć po publikację Tworzenie stron WWW.
Praktyczny kurs (http://helion.pl/ksiazki/twspk2.htm).
b4ac40784ca6e05cededff5eda5a930f
b
354 Java. Praktyczny kurs
Listing 8.3.
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="utf-8">
<title>Moja strona WWW</title>
</head>
<body>
<div>
<object
type="application/x-java-applet"
code="PierwszyAplet.class"
width="320"
height="200">
</object>
</div>
</body>
</html>
Tak skonstruowany plik z kodem HTML można już wczytać do przeglądarki. Pakiet JDK
zawiera swoją własną przeglądarkę służącą do testowania apletów — appletviewer.
Uruchamia się ją, pisząc w wierszu poleceń:
appletviewer nazwa_pliku.html
Wynik działania naszego apletu w tej przeglądarce jest widoczny na rysunku 8.1. Na
rysunku 8.2 zaprezentowany został natomiast ten sam aplet po wczytaniu do Firefok-
sa (oczywiście należy wczytywać plik index.html, a nie PierwszyAplet.class)7. Należy
pamiętać, aby plik ze skompilowanym apletem (PierwszyAplet.class) był umieszczony
w tym samym katalogu co plik z kodem HTML (index.html), a dokładniej rzecz ujmując:
w lokalizacji wskazanej przez atrybut code znacznika <object>.
Rysunek 8.1.
Pierwszy aplet
w przeglądarce
appletviewer
6
Zastosowano kod zgodny ze standardem HTML5, z tym wyjątkiem, że w celu zachowania maksymalnej
kompatybilności użyty został parametr code zamiast formalnie poprawnego — data.
7
W celu uruchomienia apletu z pliku lokalnego może być konieczna zmiana systemowych ustawień
zabezpieczeń, w tym dopuszczenie w konfiguracji środowiska JRE otwierania apletów dostępnych
po protokole FILE.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 355
Rysunek 8.2.
Pierwszy aplet
w przeglądarce Firefox
Konstrukcja apletu
Kiedy przeglądarka obsługująca Javę odnajdzie w kodzie HTML osadzony aplet, wyko-
nuje określoną sekwencję czynności. Zaczyna oczywiście od sprawdzenia, czy w podanej
lokalizacji znajduje się plik zawierający kod danej klasy. Jeśli tak, kod ten jest łado-
wany do pamięci i tworzona jest instancja (czyli obiekt) znalezionej klasy, a tym sa-
mym wywołany zostaje konstruktor. Następnie jest wykonywana metoda init utwo-
rzonego obiektu informująca go o tym, że został załadowany do systemu. W metodzie
init można zatem umieścić, o ile zachodzi taka potrzeba, procedury inicjacyjne.
Kiedy aplet jest gotowy do uruchomienia, przeglądarka wywołuje jego metodę start,
informując, że powinien rozpocząć swoje działanie. Przy pierwszym ładowaniu apletu
metoda start jest zawsze wykonywana po metodzie init. W momencie gdy aplet powi-
nien zakończyć swoje działanie, jest z kolei wywoływana jego metoda stop. O tym, że
rzeczywiście występuje taka sekwencja instrukcji, można się przekonać, uruchamiając
(czy też wczytując do przeglądarki) kod z listingu 8.4. Wynik działania dla appletviewera
uruchamianego z konsoli systemowej jest widoczny na rysunku 8.3. Na rysunku 8.4 został
z kolei przedstawiony obraz konsoli Javy w przeglądarce korzystającej z wtyczki JRE 1.8.
Listing 8.4.
import javax.swing.JApplet;
import java.awt.*;
b4ac40784ca6e05cededff5eda5a930f
b
356 Java. Praktyczny kurs
Rysunek 8.3.
Kolejność wykonywania
metod w aplecie na
konsoli systemowej
Rysunek 8.4.
Kolejność wykonywania
metod w aplecie na
konsoli Java Plugin
Parametry apletu
W przypadku niektórych apletów pojawia się potrzeba przekazania im parametrów
z kodu HTML — na przykład wartości sterujących ich pracą. Taka możliwość oczy-
wiście istnieje, znacznik <object> (a także <applet>) pozwala na zastosowanie znacz-
ników wewnętrznych <param>. Ogólnie konstrukcja taka będzie miała postać:
<object
<!--atrybuty znacznika object-->
>
<param name = "nazwa1" value = "wartość1">
<!--tutaj dalsze znaczniki param-->
<param name = "nazwaN" value = "wartośćN">
</object>
Oznacza ona, że aplet będzie miał dostęp do parametru o nazwie tekst i wartości Testowanie
parametrów apletu. Napiszmy więc teraz kod odpowiedniej klasy, która będzie potrafiła
ten parametr odczytać. Jest on widoczny na listingu 8.5.
Listing 8.5.
import javax.swing.JApplet;
import java.awt.*;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 357
Do odczytu wartości parametru została użyta metoda getParameter, której jako argu-
ment należy przekazać nazwę parametru (w tym przypadku tekst). Metoda ta zwraca
wartość wskazanego argumentu lub wartość null, jeżeli parametr nie został uwzględ-
niony w kodzie HTML.
W klasie Aplet deklarujemy zatem pole typu String o nazwie tekst. W metodzie init,
która zostanie wykonana po załadowaniu apletu przez przeglądarkę, odczytujemy
wartość parametru tekst i przypisujemy ją polu tekst. Sprawdzamy jednocześnie,
czy pole to otrzymało wartość null (co by oznaczało, że parametr nie został umieszczo-
ny w kodzie HTML i nie można było go pobrać) — jeśli tak, przypisujemy mu wła-
sny tekst. Działanie metody paint jest takie samo jak we wcześniejszych przykładach,
wyświetla ona po prostu zawartość pola tekst na ekranie, w miejscu o wskazanych
współrzędnych (x = 60, y = 50). Pozostaje więc napisanie kodu HTML zawierającego
taki aplet, co zostało przedstawione na listingu 8.6. Po uruchomieniu kodu (nie można
zapomnieć o wcześniejszej kompilacji klasy Aplet) zobaczymy widok zaprezentowany
na rysunku 8.5.
Listing 8.6.
<object
type="application/x-java-applet"
code="Aplet.class"
width="300"
height="100">
<param name="tekst"
value="Testowanie parametrów apletu">
</object>
Rysunek 8.5.
Napis przekazany
apletowi w postaci
parametru
b4ac40784ca6e05cededff5eda5a930f
b
358 Java. Praktyczny kurs
Kroje pisma
Przykłady z poprzedniej lekcji pokazywały, w jaki sposób na powierzchni apletu wy-
świetlić napis. Przydałaby się w takim razie możliwość zmiany wielkości i, ewentualnie,
kroju pisma. Jest to jak najbardziej możliwe. W pakiecie java.awt znajduje się klasa
Font, która pozwala na wykonywanie tego typu manipulacji. Trzeba jednak wiedzieć,
że czcionki w Javie dzielą się na dwa rodzaje: czcionki logiczne oraz czcionki fizyczne.
W uproszczeniu można powiedzieć, że czcionki fizyczne są zależne od systemu, na
którym działa maszyna wirtualna, natomiast czcionki logiczne to rodziny czcionek,
które muszą być obsługiwane przez każde środowisko uruchomieniowe Javy (JRE).
W środowisku Java 2 i kolejnych (a więc w wersjach od 1.2 wzwyż) jest dostępnych
jedynie pięć czcionek logicznych: Serif, SansSerif, Monospaced, Dialog, DialogInput.
Należy jednak pamiętać, że nie są to czcionki wbudowane, które będą tak samo wy-
glądały w każdym systemie. Zamiast tego jest wykorzystywana technika mapowania,
tzn. dobierana jest czcionka systemowa (z systemu, na którym działa środowisko uru-
chomieniowe Javy) najlepiej pasująca do jednej z wymienionych rodzin.
Listing 8.7.
import javax.swing.JApplet;
import java.awt.*;
gDC.setFont(sansSerif);
gDC.drawString ("Czcionka SansSerif", 60, 80);
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 359
gDC.setFont(monospaced);
gDC.drawString ("Czcionka Monospaced", 60, 120);
gDC.setFont(dialog);
gDC.drawString ("Czcionka Dialog", 60, 160);
gDC.setFont(dialogInput);
gDC.drawString ("Czcionka DialogInput", 60, 200);
}
}
Z instrukcji zawartych w metodzie init wynika zatem, że każdy krój będzie pogrubio-
ny, o wielkości liter równej 24 punktom.
Rysunek 8.6.
Lista czcionek
dostępnych standardowo
b4ac40784ca6e05cededff5eda5a930f
b
360 Java. Praktyczny kurs
Nic nie stoi również na przeszkodzie, aby w konstruktorze klasy Font podać nazwę innej
rodziny czcionek. Co się jednak stanie, jeśli danej czcionki nie ma w systemie? W takiej
sytuacji zostanie wykorzystana czcionka domyślna, czyli kod będzie działał tak, jakby
w konstruktorze został przekazany parametr default. Czcionką domyślną jest Dialog.
Aby uniknąć takich niespodzianek, najlepiej po prostu pobrać listę wszystkich do-
stępnych fontów i tylko te wykorzystywać. Umożliwia to klasa GraphicsEnvironment.
Zawiera ona dwie interesujące nas metody: getAllFonts oraz getAvailableFontFamily
8
Names . Pierwsza z nich zwraca tablicę obiektów typu Font zawierającą wszystkie
dostępne czcionki, natomiast druga — tablicę obiektów typu String z nazwami dostęp-
nych rodzin czcionek. Na listingu 8.8 jest widoczny kod apletu, który wykorzystuje
wymienione metody do wyświetlania na ekranie czcionek dostępnych w systemie.
Listing 8.8.
import javax.swing.JApplet;
import java.awt.*;
W aplecie zostały zadeklarowane dwa pola typu tablicowego: fonts oraz fontNames.
Pierwsze z nich będzie przechowywać tablicę obiektów typu Font, drugie — typu
String. W metodzie init trzeba utworzyć obiekt typu GraphicsEnvironment, który
udostępnia potrzebne funkcje getAllFonts oraz getAvailableFontFamilyNames. Obiekt
ten tworzymy, wywołując statyczną metodę getLocalGraphicsEnvironment z klasy
GraphicsEnvironment, i przypisujemy zmiennej ge. Następnie pobieramy listę fontów
(i przypisujemy ją polu fonts) oraz listę nazw rodzin czcionek (i przypisujemy ją tablicy
fontNames).
8
Metody te są dostępne w JDK w wersji 1.2 i wyższych.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 361
Rysunek 8.7.
Lista czcionek i rodzin
czcionek dostępnych
w systemie
Listing 8.9.
import javax.swing.JApplet;
import java.awt.*;
b4ac40784ca6e05cededff5eda5a930f
b
362 Java. Praktyczny kurs
FontMetrics fm = gDC.getFontMetrics();
W metodzie init tworzymy nowy obiekt klasy Font reprezentujący pogrubioną czcionkę
Arial o wielkości 20 punktów i przypisujemy go polu font klasy Aplet. W metodzie
paint wywołujemy metodę setFont obiektu gDC, przekazując jej jako argument obiekt
font. Tym samym ustalamy krój pisma, z którego chcemy korzystać przy wyświetla-
niu napisów. Następnie pobieramy obiekt klasy FontMetrics, wywołując metodę get
FontMetrics, i przypisujemy go zmiennej fm. Ten obiekt udostępnia metody niezbęd-
ne do określania wysokości i szerokości napisu. Szerokość uzyskuje się przez wywołanie
metody stringWidth, a wysokość — przez wywołanie metody getHeight. Kiedy po-
bierzemy te wartości, pozostaje już tylko proste wyliczenie współrzędnych, od których
powinno rozpocząć się wyświetlanie napisu. Wystarczy użyć wzorów:
x = (szerokość apletu – szerokość napisu) / 2
y = (wysokość apletu + szerokość napisu) / 2
Kolory
Aby zmienić kolor, którym obiekty są domyślnie rysowane na powierzchni apletu
(jak można było przekonać się we wcześniejszych przykładach, jest to kolor czarny),
trzeba skorzystać z metody setColor zdefiniowanej z klasie Graphics. Jako argument
przyjmuje ona obiekt klasy Color określający kolor, który będzie używany w dalszych
operacjach. W klasie tej zostały zdefiniowane dwadzieścia cztery pola statyczne i fi-
nalne określające łącznie dwanaście różnych kolorów. Pola te zostały zebrane w ta-
beli 8.1. Aby zatem zmienić kolor wyświetlanego tekstu w aplecie z listingu 8.9 na
czerwony, należałoby przed wywołaniem drawString dodać do metody paint wywołanie
metody setColor(Color.red) lub setColor(Color.RED). Metoda paint miałaby wtedy
następującą postać:
public void paint (Graphics gDC) {
//tutaj początkowe instrukcje metody
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 363
Pierwszy z nich przyjmuje jeden argument, którym jest wartość typu int. Musi być
ona skonstruowana w taki sposób, aby bity 16 – 23 określały wartość składowej R
(w modelu RGB), bity 8 – 15 wartość składowej G, a 0 – 7 składowej B. Drugi z wy-
mienionych konstruktorów przyjmuje natomiast wartość składowych w postaci trzech
argumentów typu int. Warto może w tym miejscu przypomnieć, że w modelu RGB
kolor jest określany za pomocą trzech składowych: czerwonej (R, ang. red), zielonej
(G, ang. green) i niebieskiej (B, ang. blue). Każdy z parametrów przekazanych drugie-
mu konstruktorowi może przyjmować wartości z zakresu 0 – 255 (jest to więc 8-bitowy
model RGB), łącznie można zatem przedstawić 16 777 216 różnych kolorów. W ta-
beli 8.2 zostały przedstawione składowe RGB dla kilkunastu przykładowych kolorów.
Napiszmy zatem aplet wyświetlający w centrum powierzchni swojego okna napis, którego
treść oraz kolor będą zdefiniowane poprzez zewnętrzne parametry określone w kodzie
HTML. Kolor będzie określany za pomocą trzech parametrów odzwierciedlających
poszczególne składowe RGB. Kod klasy realizującej takie zadanie został przedstawiony
na listingu 8.10.
9
Rodzaj purpury, w modelu RGB z reguły określany przez składowe R = 255, G = 0, B = 255.
b4ac40784ca6e05cededff5eda5a930f
b
364 Java. Praktyczny kurs
Listing 8.10.
import javax.swing.JApplet;
import java.awt.*;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 365
W klasie Aplet deklarujemy trzy pola: tekst — będzie zawierało wyświetlany napis,
font — zawierające obiekt klasy Font opisujący krój pisma, oraz color — opisujące
kolor napisu. W metodzie init odczytujemy wartości przekazanych parametrów o na-
zwach paramR, paramG i paramB. Ponieważ metoda getParameter zwraca wartość typu
String, próbujemy dokonać konwersji na typ int za pomocą metody parseInt z klasy
Integer. Jeśli któraś z konwersji się nie powiedzie, zostanie wygenerowany wyjątek
NumberFormatException, który przechwytujemy w bloku try. Jeśli taki wyjątek wystąpi,
przywracamy początkowe wartości zmiennym paramR, paramG i paramB. Tworzymy
następnie nowy obiekt klasy Color i nowy obiekt klasy Font. Odczytujemy także wartość
parametru tekst i przypisujemy go polu o takiej samej nazwie. Jeśli okaże się, że zosta-
nie zwrócona wartość pusta null, czyli parametr o nazwie tekst nie został przekazany,
pole tekst otrzyma pusty ciąg znaków. Wyświetlaniem napisu w centrum okna zaj-
muje się metoda paint, której zasada działania jest identyczna z tą przedstawioną na
początku tego podrozdziału. Na listingu 8.11 został zaprezentowany przykładowy kod
znacznika <object> zawierający wywołanie apletu.
Listing 8.11.
<object
type="application/x-java-applet"
code="Aplet.class"
width="400"
height="210"
>
<param name="paramr" value="168">
<param name="paramg" value="18">
<param name="paramb" value="240">
<param name="tekst" value="Przykładowy tekst">
</object>
b4ac40784ca6e05cededff5eda5a930f
b
366 Java. Praktyczny kurs
Ćwiczenie 38.1.
Napisz program, który w trybie tekstowym wyświetli na konsoli systemowej listę czcio-
nek dostępnych w systemie.
Ćwiczenie 38.2.
Napisz aplet wyświetlający na środku powierzchni okna tekst, którego treść oraz wielkość
i styl czcionki będą przekazywane w postaci parametrów zawartych w kodzie HTML.
Ćwiczenie 38.3.
Napisz aplet, który będzie wyświetlał tekst o kolorze określonym przez parametr
przekazywany z kodu HTML. Parametrem tym powinna być jedna liczba typu int.
Ćwiczenie 38.4.
Wyświetl na ekranie apletu krótki tekst, w którym każda litera będzie miała inny kolor
(kolory mogą być losowe)10. Zrób tak, aby odległość pomiędzy literami była stała
(możesz użyć czcionki o stałej szerokości znaku). Tekst powinien być wyśrodkowany
w pionie i poziomie.
Ćwiczenie 38.5.
Zmodyfikuj kod ćwiczenia 38.4 w taki sposób, aby przy każdym odświeżeniu okna
apletu litery zmieniały kolory.
10
Jeśli konieczne będzie uzyskanie dostępu do poszczególnych liter tekstu, można użyć metody charAt
z klasy String. Np. w wyniku wywołania "test".charAt(1) zostanie uzyskana litera e (drugi znak,
czyli ten o indeksie 1; indeksowanie zaczyna się od 0).
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 367
Do dyspozycji jest zestaw metod pozwalających na tworzenie m.in. linii, kół, elips
i okręgów oraz wielokątów. Najpopularniejsze metody zostały zebrane w tabeli 8.3.
b4ac40784ca6e05cededff5eda5a930f
b
368 Java. Praktyczny kurs
Punkty i linie
Klasa Graphics nie oferuje oddzielnej metody, która umożliwiałaby rysowanie poje-
dynczych punktów, zamiast tego można jednak skorzystać z metody rysującej linie. Wy-
starczy, jeśli punkt początkowy i docelowy będą miały te same współrzędne. Tak więc
aby narysować punkt o współrzędnych x = 100 i y = 200, można zastosować następującą
instrukcję:
drawLine(100, 200, 100, 200);
Prosty aplet rysujący prostokąt (składający się z czterech oddzielnych linii) z poje-
dynczym punktem w środku jest widoczny na listingu 8.12, a efekt jego działania na
rysunku 8.8.
Listing 8.12.
import javax.swing.JApplet;
import java.awt.*;
Rysunek 8.8.
Efekt działania apletu
z listingu 8.12
Aby narysować punkty lub linie w różnych kolorach, trzeba skorzystać z metody set
Color, podobnie jak w przykładach z lekcji 38. Należy zatem utworzyć nowy obiekt
klasy Color opisujący dany kolor i użyć go jako argumentu tej metody. Po jej wykonaniu
wszystkie obiekty będą rysowane w wybranym kolorze.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 369
Koła i elipsy
Do tworzenia kół i elips służą dwie metody klasy Graphics: drawOval oraz fillOval.
Pierwsza rysuje sam kontur figury, druga figurę wypełnioną aktualnym kolorem. Kolor
oczywiście można zmieniać przy użyciu metody setColor. Czemu do tworzenia koła
i elipsy została zdefiniowana tylko jedna metoda? Ponieważ koło jest po prostu szczegól-
nym przypadkiem elipsy, w której oba promienie są sobie równe. Nie czas, by zagłębiać
się w niuanse matematyki, trzeba tylko wiedzieć, w jaki sposób określać rozmiary figur
w wymienionych metodach. Przyjmują one bowiem cztery parametry opisujące pro-
stokąt, w który można wpisać koło lub elipsę. Ponieważ w prostokąt o określonych
rozmiarach można wpisać tylko jeden kształt owalny, określenie prostokąta jedno-
znacznie wyznacza koło lub elipsę.
Listing 8.13.
import javax.swing.JApplet;
import java.awt.*;
Rysunek 8.9.
Efekt działania metod
fillOval i drawOval
z klasy Graphics
b4ac40784ca6e05cededff5eda5a930f
b
370 Java. Praktyczny kurs
Wielokąty
Do rysowania wielokątów wykorzystuje się kilka różnych metod — można rysować
zarówno same kontury (metody zaczynające się słowem draw), jak i figury wypełnio-
ne kolorem (metody zaczynające się słowem fill). Do rysowania prostokątów służą
metody drawRectangle oraz fillRectangle. Przyjmują one cztery argumenty — dwa
pierwsze określają współrzędne lewego górnego rogu, natomiast dwa kolejne — szero-
kość oraz wysokość figury. Istnieje również możliwość narysowania prostokąta o zaokrą-
glonych rogach11. Służą do tego metody drawRoundRect oraz fillRoundRect. W takim
przypadku do wymienionych przed chwilą argumentów dochodzą dwa dodatkowe
określające średnicę łuku zaokrąglenia w poziomie oraz w pionie (deklaracje wszystkich
wymienionych metod znajdują się w tabeli 8.3). Przykładowy aplet rysujący opisane
figury jest przedstawiony na listingu 8.14, a efekt jego działania — na rysunku 8.10.
Listing 8.14.
import javax.swing.JApplet;
import java.awt.*;
Rysunek 8.10.
Efekt działania metod
rysujących różnego
rodzaju prostokąty
11
Oczywiście formalnie taka figura nie jest już prostokątem.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 371
Parametr ile określa liczbę wierzchołków. Jeżeli ostatni punkt nie będzie się pokrywał
z pierwszym, figura zostanie automatycznie domknięta. Na listingu 8.15 jest widoczny
aplet rysujący dwa sześciokąty, wykorzystujący wymienione metody. Efekt jego działania
został zaprezentowany na rysunku 8.11.
Listing 8.15.
import javax.swing.JApplet;
import java.awt.*;
Rysunek 8.11.
Wykorzystanie metod
drawPolygon
i fillPolygon
do wyświetlenia
sześciokątów
b4ac40784ca6e05cededff5eda5a930f
b
372 Java. Praktyczny kurs
Aby z nich skorzystać, należy oczywiście najpierw utworzyć obiekt klasy Polygon. Ma
ona dwa konstruktory, jeden bezargumentowy i drugi w postaci:
Polygon(int[] x, int[] y, int ile)
Wczytywanie grafiki
Wczytywanie obrazów graficznych umożliwia metoda o nazwie getImage z klasy
JApplet (Applet). Wczytuje ona wskazany w argumencie plik graficzny w formacie GIF,
JPG lub PNG i zwraca obiekt klasy Image, który może zostać wyświetlony na ekranie.
Metoda getImage występuje w dwóch następujących wersjach:
getImage(URL url)
getImage(URL url, String name)
Pierwsza z nich przyjmuje jako argument obiekt klasy URL bezpośrednio wskazujący na
plik z obrazem, druga wymaga podania argumentu klasy URL wskazującego na umiejsco-
wienie pliku (np. http://host.domena/java/obrazy/) i drugiego, określającego nazwę
pliku. Jeśli obraz znajduje się w strukturze katalogów serwera, na którym jest umiesz-
czony dokument HTML i (lub) kod apletu, wygodne będzie użycie drugiej postaci kon-
struktora. Nie trzeba wtedy tworzyć nowego obiektu klasy URL bezpośrednio — wy-
starczy wywołać jedną z dwóch metod:
getDocumentBase()
lub:
getCodeBase()
Po otrzymaniu zwróconego przez getImage obiektu klasy Image będzie można go użyć ja-
ko argumentu metody drawImage rysującej obraz na powierzchni apletu. Metoda drawImage
występuje w kilku przeciążonych wersjach (ich opis można znaleźć w dokumentacji JDK),
w najprostszej postaci należy zastosować wywołanie:
gDC.drawImage (img, wspX, wspY, this);
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 373
Listing 8.16.
import javax.swing.JApplet;
import java.awt.*;
Rysunek 8.12.
Obraz wczytany
za pomocą metody
getImage
Czas wyjaśnić, do czego służy czwarty parametr metody drawImage, którym w przypadku
apletu z listingu 8.16 było wskazanie na obiekt tego apletu (wskazanie this). Przede
wszystkim trzeba wiedzieć, co się dzieje w kolejnych fazach pracy takiego apletu. W me-
todzie init wywołujemy metodę getImage, przekazując jej w argumencie lokalizację
pliku. Ta metoda zwraca obiekt klasy Image niezależnie od tego, czy wskazany plik
graficzny istnieje, czy nie. Ładowanie danych rozpocznie się dopiero w momencie pierw-
szego wywołania metody drawImage.
b4ac40784ca6e05cededff5eda5a930f
b
374 Java. Praktyczny kurs
Sama metoda drawImage działa natomiast w taki sposób, że po jej wywołaniu jest wy-
świetlana dostępna część obrazu (czyli albo nic, albo część obrazu, albo cały obraz)
i metoda kończy działanie. Jeśli cały obraz był dostępny, jest zwracana wartość true, jeśli
nie — wartość false. Jeśli obraz nie był w pełni dostępny i zwrócona została wartość
false, jest on ładowany w tle. W trakcie tego ładowania, czyli napływania kolejnych
danych z sieci, jest wywoływana metoda imageUpdate obiektu implementującego interfejs
ImageObserver, który został przekazany jako czwarty argument metody drawImage.
Ponieważ klasa JApplet implementuje ten interfejs, można jej obiekt wykorzystać jako
czwarty argument metody. Powstanie wtedy sytuacja, w której obiekt apletu jest infor-
mowany o postępach ładowania obrazu. Aby mieć możliwość kontrolowania procesu
wczytywania i wyświetlania obrazu, należy przeciążyć metodę imageUpdate z klasy
JApplet. Przykładowo: jeśli w trakcie ładowania obrazu na pasku stanu miałaby być wy-
świetlana informacja o tym procesie, należałoby zastosować kod widoczny na listingu 8.17.
Listing 8.17.
import javax.swing.JApplet;
import java.awt.*;
import java.awt.image.*;
Metoda imageUpdate przy każdym wywołaniu otrzymuje cały zestaw argumentów, czyli:
img — referencję do rysowanego obrazu (jest to ważne, gdyż jednocześnie może być
przecież ładowanych kilka obrazów), x i y — współrzędne powierzchni apletu, w których
rozpoczyna się obraz, width i height — wysokość i szerokość obrazu, oraz najważniej-
szy dla nas w tej chwili — flags. Argument flags to wartość typu int, w której po-
szczególne bity informują o stanie ładowanego obrazu. Informacje o ich dokładnym
znaczeniu można znaleźć w dokumentacji JDK w opisie interfejsu ImageObserver.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 375
Najważniejszy dla nas jest bit piąty, którego ustawienie na 1 oznacza, że obraz został
całkowicie załadowany i może zostać wyświetlony w ostatecznej formie. Do zbadania
stanu tego bitu wykorzystuje się zdefiniowaną w klasie ImageObserver stałą (pole
statyczne i finalne typu int) ImageObserver.ALLBITS. Jeśli wynikiem operacji bitowej
AND między argumentem flags oraz stałą ALLBITS jest 0, oznacza to, że bit ten jest wyłą-
czony, a zatem obraz nie jest załadowany, jeśli natomiast wynik tej operacji jest różny
od 0, to bit jest włączony i dysponujemy pełnym obrazem.
Rysunek 8.13.
Wyświetlanie informacji
o stanie załadowania
obrazu
Ćwiczenie 39.1.
Napisz aplet rysujący owal, którego rozmiar i położenie będą przekazywane w postaci
argumentów z kodu HTML.
Ćwiczenie 39.2.
Zmodyfikuj kod z listingu 8.15 tak, aby wykorzystywał metody rysujące wielokąty,
które przyjmują jako argumenty obiekty klasy Polygon.
Ćwiczenie 39.3.
Zmień kod apletu z listingu 8.17 tak, aby informacja o stanie ładowania obrazu była
wyświetlana nie w wierszu statusu przeglądarki, ale na środku powierzchni apletu.
Ćwiczenie 39.4.
Napisz aplet rysujący wzór podobny do pokazanego na rysunku 8.14. Do rysowania
figur spróbuj użyć wyłącznie metody drawRect. Jeśli to możliwe, zrób tak, aby w całym
kodzie było tylko jedno wywołanie tej metody.
Ćwiczenie 39.5.
Napisz aplet rysujący wielobarwne koło. Postaraj się uzyskać płynną zmianę kolorów.
b4ac40784ca6e05cededff5eda5a930f
b
376 Java. Praktyczny kurs
Rysunek 8.14.
Wzór graficzny
do ćwiczenia 39.4
Interfejs MouseListener
Interfejs MouseListener jest zdefiniowany w pakiecie java.awt.event, a zatem klasa,
która będzie go implementowała, musi zawierać odpowiednią dyrektywę import. Jest
on dostępny we wszystkich JDK, począwszy od wersji 1.1. Znajdują się w nim deklaracje
pięciu metod zebranych w tabeli 8.4.
Każda klasa implementująca ten interfejs musi zatem zawierać definicję wszystkich
wymienionych metod, nawet jeśli nie będzie ich wykorzystywała. Szkielet apletu będzie
miał postać widoczną na listingu 8.18. Jak widać, możliwe jest reagowanie na pięć róż-
nych zdarzeń opisanych w tabeli 8.4 i w komentarzach w zaprezentowanym kodzie.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 377
Listing 8.18.
import javax.swing.JApplet;
import java.awt.event.*;
Każda metoda otrzymuje jako argument obiekt klasy MouseEvent pozwalający określić
rodzaj zdarzenia oraz współrzędne kursora. Aby dowiedzieć się, który przycisk został
wciśnięty, korzysta się z metody getButton12, współrzędne natomiast można otrzy-
mać, wywołując metody getX i getY. Metoda getButton zwraca wartość typu int, którą
należy porównywać ze stałymi zdefiniowanymi w klasie MouseEvent:
MouseEvent.BUTTON1,
MouseEvent.BUTTON2,
MouseEvent.BUTTON3,
MouseEvent.NOBUTTON.
Pierwsze trzy określają numer przycisku, natomiast ostatnia informuje, że żaden przycisk
podczas danego zdarzenia nie był wciśnięty. Na listingu 8.19 jest widoczny przykła-
dowy aplet, który wyświetla współrzędne ostatniego kliknięcia myszą oraz informację
o tym, który przycisk został użyty.
Listing 8.19.
import javax.swing.JApplet;
import java.awt.*;
import java.awt.event.*;
12
Metoda ta jest dostępna, począwszy od JDK 1.4, wcześniejsze wersje JDK jej nie zawierają.
b4ac40784ca6e05cededff5eda5a930f
b
378 Java. Praktyczny kurs
}
public void paint (Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.drawString(tekst, 20, 20);
}
public void mouseClicked(MouseEvent evt) {
int button = evt.getButton();
switch(button){
case MouseEvent.BUTTON1 : tekst = "Przycisk 1, ";break;
case MouseEvent.BUTTON2 : tekst = "Przycisk 2, ";break;
case MouseEvent.BUTTON3 : tekst = "Przycisk 3, ";break;
default : tekst = "";
}
tekst += "współrzędne: x = " + evt.getX() + ", ";
tekst += "y = " + evt.getY();
repaint();
}
public void mouseEntered(MouseEvent evt){}
public void mouseExited(MouseEvent evt){}
public void mousePressed(MouseEvent evt){}
public void mouseReleased(MouseEvent evt){}
}
Ponieważ interesuje nas jedynie zdarzenie polegające na kliknięciu myszą, treść me-
tod niezwiązanych z nim, czyli mouseEntered, mouseExited, mousePressed, mouseReleased,
pozostaje pusta. Niemniej ich definicje muszą znajdować się w klasie Aplet, gdyż
wymaga tego interfejs MouseListener (por. lekcje 26. i 27.).
Bardzo ważny jest również fragment kodu wykonywany w metodzie init. Otóż wy-
wołujemy tam metodę addMouseListener, której w argumencie przekazujemy wskazanie
do apletu. Takie wywołanie oznacza, że wszystkie zdarzenia związane z obsługą myszy,
określone przez interfejs MouseListener, będą obsługiwane przez obiekt wskazany
w argumencie metody. Ponieważ argumentem jest referencja do obiektu apletu (choć
może być to obiekt dowolnej klasy implementującej interfejs MouseListener), w tym
przypadku informujemy po prostu maszynę wirtualną, że nasz aplet samodzielnie będzie
obsługiwał zdarzenia związane z myszą.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 379
O wywołaniu metody addMouseListener trzeba koniecznie pamiętać, gdyż jeśli jej za-
braknie, kompilator nie zgłosi żadnego błędu, a program po prostu nie będzie działał.
Jest to błąd stosunkowo często popełniany przez początkujących programistów. Jeśli
jednak będziemy o tej instrukcji pamiętać, to po skompilowaniu i uruchomieniu
apletu oraz kliknięciu pierwszego przycisku na ekranie pojawi się widok podobny do
zaprezentowanego na rysunku 8.15.
Rysunek 8.15.
Wynik działania
apletu wyświetlającego
informacje
o kliknięciach myszą
Listing 8.20.
import javax.swing.JApplet;
import java.awt.event.*;
b4ac40784ca6e05cededff5eda5a930f
b
380 Java. Praktyczny kurs
Klasa Aplet nie implementuje w tej chwili interfejsu MouseListener, gdyż obsługa myszy
jest przekazywana innej klasie. Pozostała w niej jedynie metoda init, w której wywołuje
się metodę addActionListener. Argumentem przekazanym metodzie addActionListener
jest nowy obiekt klasy MyMouseListener. Oznacza to, że aplet ma reagować na zdarzenia
myszy, ale ich obsługa została przekazana obiektowi klasy MyMouseListener.
Klasa MyMouseListener jest klasą pakietową, może być zatem zdefiniowana w tym samym
pliku co klasa Aplet (lekcja 17.). Implementuje ona oczywiście interfejs MouseListener,
inaczej obiekt tej klasy nie mógłby być argumentem metody addActionListener.
Wewnątrz klasy MyMouseListener zostały zdefiniowane metody z interfejsu, jednak
kod wykonywalny zawiera jedynie metoda mouseClicked. Jej treść jest bardzo podobna do
kodu metody mouseClicked z poprzedniego przykładu. Jedyną różnicą jest to, że zmienna
tekst jest zdefiniowana wewnątrz tej metody i zamiast metody repaint jest wykony-
wana instrukcja System.out.println wyświetlająca na konsoli współrzędne kliknięcia.
Uruchomienie takiego apletu spowoduje, że współrzędne kliknięcia będą się pojawiały na
konsoli, tak jak jest to widoczne na rysunku 8.16.
Rysunek 8.16.
Współrzędne kliknięcia
pojawiają się na konsoli
Tym razem nie da się wyświetlić tekstu bezpośrednio w obszarze apletu, gdyż klasa
MyMouseListener nie ma do niego dostępu. Jak go uzyskać? Można by na przykład prze-
kazać referencję do obiektu apletu w konstruktorze klasy MyMouseListener (ćwiczenie 40.4
z podpunktu „Ćwiczenia do samodzielnego wykonania”). O wiele lepszym rozwiązaniem
byłoby jednak zastosowanie klasy wewnętrznej, a jeszcze lepiej anonimowej klasy we-
wnętrznej (lekcje 28. – 30.). Takie rozwiązanie zostanie pokazane już w kolejnej lekcji.
Interfejs MouseMotionListener
Interfejs MouseMotionListener pozwala na obsługiwanie zdarzeń związanych z ruchem
myszy. Zawiera definicję dwóch metod — zostały przedstawione w tabeli 8.5. Pierwsza
jest wywoływana, jeśli przycisk myszy został wciśnięty i mysz się porusza, natomiast
druga przy każdym ruchu myszy bez wciśniętego przycisku.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 381
Opierając się na przykładzie z listingu 8.19, bez problemu można napisać aplet, który
będzie wyświetlał aktualne współrzędne położenia kursora myszy. Kod realizujący to
zadanie został przedstawiony na listingu 8.21.
Listing 8.21.
import javax.swing.JApplet;
import java.awt.*;
import java.awt.event.*;
b4ac40784ca6e05cededff5eda5a930f
b
382 Java. Praktyczny kurs
MouseEvent.ALT_DOWN_MASK,
MouseEvent.CTRL_DOWN_MASK.
Pierwsza z nich oznacza, że został wciśnięty klawisz Shift, druga — że został wci-
śnięty klawisz Alt, a trzecia — że został wciśnięty klawisz Ctrl14. Porównania z wartością
zwróconą przez getModifiers dokonuje się przez wykonanie operacji bitowej AND,
czyli iloczynu bitowego. Jeśli więc wynikiem operacji:
getModifiers() & MouseEvent.SHIFT_DOWN
jest wartość 0, oznacza to, że klawisz Shift nie był wciśnięty, a jeśli wartość tej operacji
jest różna od 0, Shift był wciśnięty. Przykładowy aplet wykorzystujący opisaną tech-
nikę do stwierdzenia, które z klawiszy funkcyjnych były wciśnięte podczas przesu-
wania kursora myszy, jest widoczny na listingu 8.22, a efekt działania tego apletu
przedstawiono na rysunku 8.17.
Listing 8.22.
import javax.swing.JApplet;
import java.awt.*;
import java.awt.event.*;
13
Przez stałą rozumiemy pole statyczne i finalne klasy.
14
W wersjach JDK poniżej 1.4 były wykorzystywane stałe SHIFT_DOWN, ALT_DOWN i CTRL_DOWN.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 383
Rysunek 8.17.
Przykładowy efekt
działania apletu
z listingu 8.22
Dźwięki
Pisane przez nas aplety można wyposażyć w możliwość odtwarzania dźwięków. Java
obsługuje standardowo kilka formatów plików dźwiękowych, są to AU, AIFF, WAVE
oraz MIDI. W klasie JApplet została zdefiniowana metoda play, która pobiera i od-
twarza klip dźwiękowy. Występuje ona w dwóch przeciążonych wersjach:
play(URL url)
play(URL url, String name)
Pierwsza z nich przyjmuje jako argument obiekt klasy URL bezpośrednio wskazujący
na plik dźwiękowy, druga wymaga podania argumentu klasy URL wskazującego na umiej-
scowienie pliku (np. http://host.domena/java/sound/) i drugiego określającego nazwę
pliku. Jeśli plik znajduje się w strukturze katalogów serwera, na którym umieszczony
jest dokument HTML i (lub) kod apletu, wygodniejsze może być użycie drugiej po-
staci konstruktora, podobnie jak w przypadku metody getImage omawianej w lekcji 39.
Przykład apletu, który podczas uruchamiania odtwarza plik dźwiękowy, jest widoczny
na listingu 8.23.
Listing 8.23.
import javax.swing.JApplet;
b4ac40784ca6e05cededff5eda5a930f
b
384 Java. Praktyczny kurs
Znaczenie argumentów jest takie samo jak w przypadku opisanej powyżej metody play.
Proste wykorzystanie interfejsu AudioClip zostało zilustrowane w przykładzie z li-
stingu 8.24. Po uruchomieniu apletu rozpoczyna się odtwarzanie dźwięku, a gdy aplet
kończy pracę, odtwarzanie jest zatrzymywane.
Listing 8.24.
import java.applet.Applet;
import java.applet.AudioClip;
Listing 8.25.
import java.applet.Applet;
import java.applet.AudioClip;
import java.awt.event.*;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 385
Ćwiczenie 40.1.
Zmień kod apletu z listingu 8.19 w taki sposób, aby reagował nie na kliknięcia, ale na
samo naciśnięcie przycisku myszy.
Ćwiczenie 40.2.
Napisz aplet, w którym obsługa ruchów myszy będzie realizowana przez oddzielną
klasę MyMouseMotionListener. Przy każdym ruchu myszy wyświetl współrzędne kur-
sora myszy na konsoli.
Ćwiczenie 40.3.
Napisz aplet, który będzie odtwarzał plik dźwiękowy, kiedy użytkownik zbliży kursor
myszy na mniej niż 10 pikseli od brzegów powierzchni apletu. Wysokość oraz szerokość
obszaru apletu można uzyskać dzięki wywołaniu metody getWidth oraz getHeight
klasy JApplet (Applet).
Ćwiczenie 40.4.
Napisz aplet, który przy kliknięciu będzie odtwarzał dźwięki. Odtwarzanie powinno być
realizowane przez pakietową klasę MyAudioClip implementującą interfejs AudioClip.
Ćwiczenie 40.5.
Napisz aplet, który będzie pozwalał na rysowanie na jego powierzchni kolorowych
odcinków. Kolor każdego odcinka powinien być losowy.
b4ac40784ca6e05cededff5eda5a930f
b
386 Java. Praktyczny kurs
Aplikacje
Lekcja 41. Tworzenie aplikacji
Na początku rozdziału 8. była mowa o różnicach między aplikacją i apletem, wiadomo
zatem, że aplikacja potrzebuje do uruchomienia jedynie maszyny wirtualnej, a aplet jest
programem wbudowanym, zagnieżdżonym w innym programie, najczęściej w przeglą-
darce internetowej. Wszystkie programy, które powstawały w rozdziałach 1. – 7.,
były właśnie aplikacjami, pracującymi jednak w trybie tekstowym. W tej lekcji będzie
pokazane, w jaki sposób tworzy się aplikacje pracujące w trybie graficznym, czyli po-
pularne aplikacje okienkowe.
Pierwsze okno
W lekcji 37. powstał nasz pierwszy aplet, przedstawiony na listingu 8.1. Zobaczmy teraz,
jak napisać aplikację, która będzie wykonywała to samo zadanie, czyli wyświetli napis na
ekranie. Będzie to wymagało napisania klasy np. o nazwie PierwszaAplikacja, która
będzie dziedziczyć po klasie JFrame. Jest to klasa zawarta w pakiecie javax.swing. Alter-
natywnie można użyć również klasy Frame z pakietu java.awt, jednak jest ona uznawana
za przestarzałą. Kod pierwszej aplikacji został przedstawiony na listingu 8.26.
Listing 8.26.
import javax.swing.*;
import java.awt.*;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 387
Obiekt klasy PierwszaAplikacja jest tworzony w metodzie main, od której rozpoczyna się
wykonywanie kodu. Po uruchomieniu zobaczymy widok przedstawiony na rysunku 8.18.
Rysunek 8.18.
Wygląd prostej aplikacji
Wraz z platformą Java 2 SE5 pojawił się nowy model obsługi zdarzeń dla biblioteki
Swing, w którym operacje związane z komponentami (a okno aplikacji jest komponen-
tem) nie powinny być obsługiwane bezpośrednio, ale mają trafiać do kolejki zdarzeń.
Dotyczy to również samego uruchamiania aplikacji. Należy użyć metody invokeLater
klasy SwingUtilities, która umieści wywołanie w kolejce zdarzeń. Argumentem tej
metody musi być obiekt implementujący interfejs Runnable, a operacja, którą chce się
wykonać, powinna się znaleźć w metodzie run tego interfejsu. Zgodnie z tym stan-
dardem metoda main powinna mieć postać:
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new PierwszaAplikacja();
}
});
}
b4ac40784ca6e05cededff5eda5a930f
b
388 Java. Praktyczny kurs
Listing 8.27.
import javax.swing.*;
import java.awt.event.*;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 389
okna. W takiej sytuacji wywoływana jest metoda dispose, która powoduje zwolnienie
zasobów związanych z oknem, zamknięcie okna i jeżeli jest to ostatnie okno aplikacji,
zakończenie pracy aplikacji.
b4ac40784ca6e05cededff5eda5a930f
b
390 Java. Praktyczny kurs
Warto przy tym zauważyć, że ta klasa mogłaby być z powodzeniem klasą anonimową
(por. lekcja 30.), jej nazwa w przedstawionej sytuacji tak naprawdę do niczego nie jest
potrzebna. Program mógłby więc przyjąć postać widoczną na listingu 8.29.
Listing 8.29.
import javax.swing.*;
import java.awt.event.*;
Podobnie można postąpić przy obsłudze zdarzeń związanych z myszą. Jeśli potrzebna
jest implementacja interfejsu MouseListener, należy skorzystać z klasy MouseAdapter,
jeśli natomiast niezbędny jest interfejs MouseMotionListener, trzeba skorzystać z klasy
MouseMotionAdapter. Oba te adaptery są zdefiniowane w pakiecie java.awt. Pakiet
javax.swing.event udostępnia natomiast dodatkowy adapter zbiorczy implementujący
wszystkie interfejsy związane z myszą. Jest to MouseInputAdapter.
Wróćmy do listingu 8.21 z lekcji 40., do kodu apletu, który pokazywał aktualne
współrzędne kursora myszy, i przeróbmy go w taki sposób, aby do obsługi zdarzeń
był wykorzystywany obiekt anonimowej klasy dziedziczącej po MouseInputAdapter.
Kod takiego apletu jest widoczny na listingu 8.30 (usunięta została jedynie istniejąca
na listingu 8.21 obsługa metody mouseDragged).
Listing 8.30.
import javax.swing.JApplet;
import javax.swing.event.MouseInputAdapter;
import java.awt.Graphics;
import java.awt.event.*;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 391
Menu
Rzadko która aplikacja okienkowa może obyć się bez menu. W Javie w celu dodania
menu trzeba skorzystać z kilku klas: JMenuBar, JMenu i JMenuItem. Pierwsza z nich opisuje
pasek menu, druga — menu znajdujące się na tym pasku, a trzecia — poszczególne ele-
menty menu. Pasek menu dodaje się do okna aplikacji za pomocą metody setJMenuBar,
natomiast menu do paska — przy użyciu metody add. Jak to wygląda w praktyce, zo-
brazowano na listingu 8.31.
Listing 8.31.
import javax.swing.*;
mb.add(menu1);
mb.add(menu2);
mb.add(menu3);
setJMenuBar(mb);
setSize(320, 200);
setVisible(true);
}
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Aplikacja();
b4ac40784ca6e05cededff5eda5a930f
b
392 Java. Praktyczny kurs
}
});
}
}
Rysunek 8.19.
Aplikacja z trzema
pozycjami menu
Do tak stworzonego menu należy dodać poszczególne pozycje. Służy do tego klasa
JMenuItem. Obiekt tej klasy dodaje się do konkretnego menu za pomocą metody add.
Jeśli zatem każde menu utworzone w aplikacji z listingu 8.31 ma mieć po dwie pozycje,
konstruktor należy zmodyfikować w sposób przedstawiony na listingu 8.32.
Listing 8.32.
public Aplikacja() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JMenuBar mb = new JMenuBar();
menu1.add(menuItem11);
menu1.add(menuItem12);
menu2.add(menuItem21);
menu2.add(menuItem22);
menu3.add(menuItem31);
menu3.add(menuItem32);
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 393
mb.add(menu1);
mb.add(menu2);
mb.add(menu3);
setJMenuBar(mb);
setSize(320, 200);
setVisible(true);
}
Rysunek 8.20.
Menu z rozwiniętymi
pozycjami
Wiadomo już, jak tworzyć menu, warto więc teraz dowiedzieć się, w jaki sposób spowo-
dować, aby program reagował na wybranie jednej z pozycji. Łatwo się domyślić, że trze-
ba będzie skorzystać z jakiegoś interfejsu typu MenuListener i przekazać zdarzenia do
pewnego obiektu, być może obiektu aplikacji, być może obiektu dodatkowej klasy.
Rzeczywiście tak należy postąpić. Nie ma jednak interfejsu o nazwie MenuListener, trzeba
zatem skorzystać z interfejsu ActionListener. Jest w nim zdefiniowana tylko jedna meto-
da o nazwie actionPerformed. Do reagowania na wybór danej pozycji menu można więc
zastosować technikę przedstawioną na listingu 8.33.
Listing 8.33.
import javax.swing.*;
import java.awt.event.*;
public Aplikacja() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
b4ac40784ca6e05cededff5eda5a930f
b
394 Java. Praktyczny kurs
setJMenuBar(mb);
miZamknij.addActionListener(al);
setSize(320, 200);
setVisible(true);
}
public static void main(String args[]) {
//kod metody main
}
}
Aplikacja ma w tej chwili jedynie menu Plik z jedną pozycją o nazwie Zamknij (wi-
dać to na rysunku 8.21). Sposób utworzenia menu jest analogiczny do wykorzystane-
go w poprzednich przykładach, z tą różnicą, że zmienna określająca pozycję menu
Zamknij jest prywatnym polem klasy Aplikacja. Chodzi o to, aby mieć do tej pozycji
dostęp nie tylko w obrębie konstruktora, ale w całej klasie. Po utworzeniu paska menu
(obiekt mb), samego menu (obiekt menu) oraz pozycji (obiekt miZamknij) do tej pozycji
dodaje się obsługę zdarzeń, wywołując instrukcję:
miZamknij.addActionListener(al);
Rysunek 8.21.
Menu aplikacji
z listingu 8.33
Tym samym zdarzenia związane z obsługą tego menu będą przekazywane do metody
actionPerformed obiektu al. Obiekt ten jest obiektem klasy anonimowej implementującej
interfejs ActionListener i został zdefiniowany na początku klasy Aplikacja.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 395
Listing 8.34.
import javax.swing.*;
import java.awt.event.*;
public Aplikacja() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JMenuBar mb = new JMenuBar();
JMenu menu = new JMenu("Plik");
Obiekt pozycji menu Zamknij jest tworzony na takiej samej zasadzie jak w poprzednim
przykładzie, z tą różnicą, że nie ma teraz pola miZamknij w klasie Aplikacja. Zamiast
niego używana jest jedynie tymczasowa zmienna referencyjna miZamknij (zostanie
utracona po zakończeniu pracy konstruktora). Za pomocą wywołania setActionCommand
obiektowi menu jest też przypisywana komenda Zamknij15, którą będzie można odczy-
tać z obiektu obsługującego zdarzenie przekazywanego metodzie actionPreformed.
Konstrukcja tej metody również przypomina tę z listingu 8.33, jednak inaczej wyglą-
da instrukcja warunkowa. Tym razem sprawdzane jest, czy komenda pobrana za po-
mocą metody getActionCommand z klasy ActionEvent odpowiada ciągowi znaków Zamknij
(czyli czy została wybrana pozycja menu z przypisaną komendą Zamknij). Jeśli tak,
zostaje wywołana metoda dispose zamykająca okno i kończąca pracę aplikacji.
15
Tak naprawdę w tym konkretnym przypadku jest to czynność nadmiarowa, gdyż jeśli nie zostanie
wywołana metoda setActionCommand, za komendę przypisaną zdarzeniu uznaje się tekst danej
pozycji menu.
b4ac40784ca6e05cededff5eda5a930f
b
396 Java. Praktyczny kurs
Menu kaskadowe
Pozycje menu można łączyć kaskadowo i uzyskiwać w ten sposób rozbudowane struk-
tury podmenu. Utworzenie tego typu konstrukcji jest możliwe poprzez dodanie do menu
innego menu, czyli przekazanie metodzie add z klasy JMenu (w postaci argumentu) in-
nego obiektu tej klasy. Przykład tego typu konstrukcji jest widoczny na listingu 8.35.
Listing 8.35.
import javax.swing.*;
import java.awt.event.*;
public Aplikacja() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JMenuBar mb = new JMenuBar();
JMenu menu = new JMenu("Menu 1");
JMenu submenu1 = new JMenu("Pozycja 4");
JMenu submenu2 = new JMenu("Pozycja 8");
menu.add(submenu1);
submenu1.add(submenu2);
mb.add(menu);
setJMenuBar(mb);
setSize(320, 200);
setVisible(true);
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 397
Zostały utworzone trzy różne menu (obiekty klasy JMenu) o nazwach menu, submenu1
i submenu2. Pierwsze menu otrzymuje etykietę Menu 1, drugie — Pozycja 4, a trzecie —
Pozycja 8. Dlaczego akurat takie nazwy etykiet? Otóż każde z menu otrzymuje po trzy
pozycje, a następnie do pierwszego dodaje się submenu1, a do niego — submenu2. Ety-
kieta menu dodawanego staje się ostatnią pozycją menu, do którego zostało ono dodane.
Jeśli więc etykietą submenu1 jest Pozycja 4, to po dodaniu submenu1 do menu etykieta
Pozycja 4 staje się ostatnią pozycją w menu. Jeśli opis nadal nie jest do końca jasny, ry-
sunek 8.22 powinien rozwiać wszelkie wątpliwości. Jest to ilustracja struktury menu
z listingu 8.34.
Rysunek 8.22.
Menu kaskadowe
z listingu 8.34
to aby otrzymać odpowiednią referencję, trzeba wywołać metodę getItem, podając jako
argument indeks wybranego menu. Indeks pierwszego menu ma wartość 0, drugiego
— 1 itd.
Nowe instrukcje pojawiły się również w metodzie actionPerformed. Jej zadaniem jest wy-
świetlenie na konsoli nazwy menu, które zostało wybrane przez użytkownika. W celu po-
brania tej nazwy jest wykonywana złożona instrukcja:
String text = ((JMenuItem)e.getSource()).getText();
b4ac40784ca6e05cededff5eda5a930f
b
398 Java. Praktyczny kurs
W tym miejscu warto zwrócić uwagę na to, że taki sposób obsługi był możliwy, ponieważ
jedynymi źródłami zdarzeń były obiekty związane z pozycjami menu, czyli klasy
JMenuItem. W ramach ćwiczeń do samodzielnego przemyślenia można wykonać takie
samo zadanie w sytuacji, kiedy źródłami zdarzeń są również inne komponenty niż
JMenuItem.
CheckBoxMenu
Oprócz zwykłych menu zaprezentowanych na poprzednich stronach pakiet javax.swing
oferuje też menu, które umożliwiają zaznaczanie poszczególnych pozycji (ang. checkbox
menu). Za ich reprezentację odpowiada klasa o nazwie JCheckBoxMenuItem. Tworzenie
tego typu menu odbywa się na takiej samej zasadzie jak zwykłych, z tą różnicą, że zamiast
klasy JMenuItem używa się JCheckBoxMenuItem. Zmienić niestety trzeba również spo-
sób obsługi zdarzeń. Należy wykorzystać dodatkowy interfejs o nazwie ItemListener.
Definiuje on tylko jedną metodę o nazwie itemStateChanged, która jest wywoływana
za każdym razem, kiedy zmieni się stan komponentu (w naszym przypadku stan pozycji
menu). Kod aplikacji zawierającej przykładowe menu z możliwością zaznaczania po-
szczególnych pozycji został przedstawiony na listingu 8.36.
Listing 8.36.
import javax.swing.*;
import java.awt.event.*;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 399
public Aplikacja() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JMenuBar mb = new JMenuBar();
JMenu menu = new JMenu("Menu 1");
mb.add(menu);
setJMenuBar(mb);
setSize(320, 200);
setVisible(true);
}
public static void main(String args[]) {
//treść metody main
}
}
Struktura menu jest tworzona w taki sam sposób, jak w przypadku elementów typu JMenuItem.
Wykorzystuje się jedynie dodatkowy argument konstruktora klasy CheckBoxMenuItem,
który określa, czy dana pozycja ma być zaznaczona (true), czy nie (false). Wygląd menu
jest widoczny na rysunku 8.23.
Rysunek 8.23.
Menu umożliwiające
zaznaczanie
poszczególnych pozycji
b4ac40784ca6e05cededff5eda5a930f
b
400 Java. Praktyczny kurs
czy dana pozycja jest zaznaczona, czy nie, należy skorzystać z kolei z metody get
State. Jeżeli zwróci ona wartość true, pozycja jest zaznaczona, jeśli false — nie jest.
Menu kontekstowe
Istnieje także możliwość wyposażenia aplikacji w menu kontekstowe. Należy wtedy sko-
rzystać z klasy JPopupMenu. Konstrukcja taka jest bardzo podobna do konstrukcji zwyczaj-
nego menu, z tą różnicą, że menu kontekstowego nie dodaje się do paska menu. Przykła-
dowa aplikacja zawierająca menu kontekstowe została przedstawiona na listingu 8.37.
Listing 8.37.
import javax.swing.*;
import java.awt.event.*;
public Aplikacja() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
popupMenu = new JPopupMenu();
miPozycja1 = new JMenuItem("Pozycja 1");
miPozycja2 = new JMenuItem("Pozycja 2");
miZamknij = new JMenuItem("Zamknij");
miPozycja1.addActionListener(al);
miPozycja2.addActionListener(al);
miZamknij.addActionListener(al);
popupMenu.add(miPozycja1);
popupMenu.add(miPozycja2);
popupMenu.addSeparator();
popupMenu.add(miZamknij);
addMouseListener(ma);
setSize(320, 200);
setVisible(true);
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 401
}
public static void main(String args[]) {
//treść metody main
}
}
Obiekt menu kontekstowego tworzy się, wywołując konstruktor klasy JPopupMenu. Kon-
struktor ten może być bezargumentowy, tak jak na powyższym listingu, lub też może
przyjmować jeden argument typu String. W tym drugim przypadku menu otrzyma
identyfikującą je nazwę. Struktura menu jest tworzona dokładnie tak samo jak w przypad-
ku wcześniej omawianych zwyczajnych menu — dodaje się po prostu kolejne obiekty
klasy JMenuItem, które staną się pozycjami menu. Dodatkowo za pomocą wywołania
metody addSeparator dodany został również separator pozycji menu. Otrzymaną strukturę
przedstawiono na rysunku 8.24. Obsługa zdarzeń jest również taka sama jak w poprzed-
nich przykładach. Obiektem obsługującym zdarzenia powiązanym z każdym z obiek-
tów JMenuItem jest obiekt klasy anonimowej pochodnej od ActionListener. W metodzie
actionPerformed sprawdzane jest, czy wybrana pozycja menu to Zamknij (czyli czy
obiektem źródłowym zdarzenia jest miZamknij). Jeśli tak, wywołana zostaje metoda
dispose obiektu aplikacji, która zwalnia zasoby związane z oknem i zamyka okno.
Ponieważ jest to jedyne okno aplikacji, czynność ta jest równoznaczna z zakończeniem
pracy całego programu.
Rysunek 8.24.
Aplikacja wyposażona
w menu kontekstowe
Aby jednak menu kontekstowe pojawiło się na ekranie, użytkownik aplikacji musi
nacisnąć prawy klawisz myszy. To oznacza, że aplikacja musi reagować na zdarzenia
związane z obsługą myszy, a jak wiadomo z lekcji 38., wymaga to implementacji inter-
fejsu MouseListener. W tym celu został użyty obiekt klasy anonimowej pochodnej od
MouseAdapter, dzięki czemu obsługiwane są metody mousePressed oraz mouseReleased.
Jest to niezbędne, gdyż menu kontekstowe jest wywoływane w różny sposób w róż-
nych systemach. W obu przypadkach metoda isPopupTrigger pozwala sprawdzić, czy
zdarzenie jest wynikiem wywołania przez użytkownika menu kontekstowego. Jeśli tak
(wywołanie isPopupTrigger zwróciło wartość true), menu jest wyświetlane w miejscu
wskazywanym przez kursor myszy.
Wyświetlenie menu odbywa się za pomocą metody show obiektu wskazywanego przez
popupMenu. Pierwszym parametrem jest obiekt, na którego powierzchni ma się pojawić
menu i względem którego będą obliczane współrzędne wyświetlania, przekazywane
jako drugi i trzeci argument. W naszym przypadku pierwszym argumentem jest po pro-
b4ac40784ca6e05cededff5eda5a930f
b
402 Java. Praktyczny kurs
Ćwiczenie 41.1.
Popraw kod z listingu 8.26 w taki sposób, aby wyświetlany tekst znajdował się w centrum
okna aplikacji. Możesz skorzystać z rozwiązania podanego w lekcji poświęconej
apletom.
Ćwiczenie 41.2.
Zmień kod z listingu 8.27 tak, aby obsługa zdarzeń odbywała się poprzez obiekt klasy
wewnętrznej implementującej interfejs WindowListener.
Ćwiczenie 41.3.
Zmodyfikuj kod z listingu 8.27 tak, by obsługa zdarzeń odbywała się za pomocą obiektu
niezależnej klasy pakietowej implementującej interfejs WindowListener.
Ćwiczenie 41.4.
Napisz aplikację zawierającą wielopoziomowe menu kontekstowe.
Ćwiczenie 41.5.
Napisz aplikację, która będzie wyświetlała w swoim oknie aktualne współrzędne kursora
myszy.
Ćwiczenie 41.6.
Napisz aplikację zawierającą menu. Po wybraniu z niego dowolnej pozycji powinno
pojawić się nowe okno (w tym celu utwórz nowy obiekt klasy JFrame). Okno to musi
dać się zamknąć przez kliknięcie odpowiedniej ikony z paska tytułu.
16
Często spotyka się też termin „lista rozwijalna”.
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 403
Klasa Component
Praktycznie wszystkie graficzne komponenty, które pozwalają na zastosowanie typo-
wych elementów środowiska okienkowego, dziedziczą, pośrednio lub bezpośrednio,
po klasie JComponent, a zatem są wyposażone w jej metody17. Metod tych jest dosyć
dużo, ich pełną listę można znaleźć w dokumentacji JDK. W tabeli 8.7 zostały zebrane
niektóre z nich, przydatne przy wykonywaniu typowych operacji. Większość została
odziedziczona po klasie Component biblioteki AWT. W kolumnie Od JDK została zawarta
informacja na temat tego, kiedy po raz pierwszy dana metoda pojawiła się w JDK.
Wartości w nawiasach oznaczają natomiast, kiedy dana metoda została redefiniowana
w klasie JComponent.
17
A także klas, po których dziedziczy JComponent.
b4ac40784ca6e05cededff5eda5a930f
b
404 Java. Praktyczny kurs
Etykiety
Etykiety tekstowe to jedne z najprostszych komponentów graficznych. Umożliwiają
wyświetlanie tekstu. Aby utworzyć etykietę, należy skorzystać z klasy JLabel. Konstruk-
tor klasy JLabel może być bezargumentowy, tworzy wtedy pustą etykietę, może też
przyjmować jako argument tekst, który ma być na niej wyświetlany. Po utworzeniu
etykiety znajdujący się na niej tekst można pobrać za pomocą metody getText, natomiast
aby go zmienić, trzeba skorzystać z metody setText. Etykietę umieszcza się w oknie
lub na innym komponencie, wywołując metodę add. Prosty przykład obrazujący wykorzy-
stanie klasy JLabel jest widoczny na listingu 8.38, natomiast efekt jego działania na
rysunku 8.25.
Listing 8.38.
import javax.swing.*;
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 405
add(label1);
add(label2);
setSize(320, 200);
setVisible(true);
}
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Aplikacja();
}
});
}
}
Rysunek 8.25.
Wygląd przykładowych
etykiet tekstowych
Metoda setLayout ustala rozkład elementów w oknie. Przekazany jej argument null
oznacza, że rezygnujemy z automatycznego rozmieszczania elementów i będziemy je
pozycjonować ręcznie18. W tym przypadku jest to jednoznaczne z tym, że argumenty
metody setBounds będą traktowane jako bezwzględne współrzędne okna aplikacji.
Przyciski
Za obsługę i wyświetlanie przycisków odpowiada klasa JButton. Podobnie jak w przy-
padku klasy JLabel, konstruktor może być bezargumentowy, powstaje wtedy przycisk
bez napisu na jego powierzchni, może również przyjmować argument klasy String.
W tym drugim przypadku przekazany napis pojawi się na przycisku. Jeśli zostanie zasto-
sowany konstruktor bezargumentowy, późniejsze przypisanie tekstu przyciskowi będzie
18
Czytelnicy zainteresowani rozkładami automatycznymi powinni zapoznać się z opisem klasy
LayoutManager oraz klas pochodnych, a także z opisem metody setLayout klasy Container.
b4ac40784ca6e05cededff5eda5a930f
b
406 Java. Praktyczny kurs
Listing 8.39.
import javax.swing.*;
import java.awt.event.*;
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(null);
add(button1);
add(button2);
setSize(320, 200);
setVisible(true);
}
public static void main(String args[]) {
//kod metody main
}
}
Rysunek 8.26.
Aplikacja zawierająca
dwa przyciski
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 407
Pola tekstowe
Istnieje kilka rodzajów pól tekstowych: JTextField, JPasswordField, JFormattedText
Field, JTextArea, JEditorPane i JTextPane. Wszystkie te klasy dziedziczą bezpośred-
nio bądź pośrednio po klasie JTextComponent, która z kolei dziedziczy po JComponent.
Nie ma niestety miejsca na dokładne omówienie wszystkich tych komponentów,
przedstawione zostaną więc jedynie przykłady użycia dwóch podstawowych typów:
JTextField oraz JTextArea. Pierwszy z nich pozwala na wprowadzenie tekstu w jednej linii,
drugi — tekstu wielowierszowego. Ich obsługa, jak okaże się za chwilę, jest podobna.
Klasa JTextField
Klasa JTextField tworzy jednowierszowe pole tekstowe, takie jak zaprezentowane na
rysunku 8.27. Oferuje ona pięć konstruktorów przedstawionych w tabeli 8.8. Przykład
wykorzystujący pole tekstowe jest widoczny na listingu 8.40.
Listing 8.40.
import javax.swing.*;
import java.awt.event.*;
b4ac40784ca6e05cededff5eda5a930f
b
408 Java. Praktyczny kurs
setTitle(textField.getText());
}
};
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(null);
add(button1);
add(textField);
setSize(320, 200);
setVisible(true);
}
public static void main(String args[]) {
//kod metody main
}
}
Rysunek 8.27.
Tekst z pola tekstowego
stanie się tytułem okna
aplikacji
Pole tekstowe tworzymy za pomocą instrukcji, których znaczenie nie powinno budzić
żadnych wątpliwości. Jego szerokość ustawiamy na 100 pikseli, a wysokość na 20. Do
dyspozycji mamy również przycisk, którego kliknięcie będzie powodowało, że tekst
znajdujący się w polu stanie się tytułem okna aplikacji. Do obsługi zdarzeń wykorzystu-
jemy interfejs ActionListener i metodę actionPerformed, podobnie jak miało to miejsce
w poprzednich przykładach. Tekst zapisany w polu tekstowym odczytujemy za pomocą
metody getText, natomiast do zmiany tytułu okna aplikacji wykorzystujemy metodę
setTitle.
Sytuacja nieco się komplikuje, jeśli chcemy mieć możliwość reagowania na każdą
zmianę tekstu, jaka zachodzi w polu tekstowym JTextField. Otóż należy monitoro-
wać zmiany dokumentu (obiektu klasy Document) powiązanego z komponentem JText
Field. Trzeba zatem skorzystać z interfejsu DocumentListener. Najpierw tworzy się no-
wy obiekt implementujący ten interfejs, a następnie przekazuje się go jako argument me-
tody setDocumentListener wywołanej na rzecz obiektu zwróconego przez wywołanie
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 409
Aby zatem napisać aplikację zawierającą pole tekstowe, taką, że każda zmiana w tym
polu powodowałaby zmianę napisu znajdującego się na pasku tytułowym okna, można
wykorzystać kod przedstawiony na listingu 8.41.
Listing 8.41.
import javax.swing.*;
import javax.swing.event.*;
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(null);
add(textField);
setSize(320, 200);
setVisible(true);
}
public static void main(String args[]) {
//kod metody main
}
}
b4ac40784ca6e05cededff5eda5a930f
b
410 Java. Praktyczny kurs
Klasa JTextArea
Klasa JTextArea pozwala na tworzenie komponentów umożliwiających wprowadzenie
większej ilości tekstu. Oferuje ona pięć konstruktorów przedstawionych w tabeli 8.9.
Przykład wykorzystania obiektu tej klasy jest widoczny na listingu 8.42.
Listing 8.42.
import javax.swing.*;
import java.awt.event.*;
import java.io.*;
add(textArea);
add(button1);
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 411
setSize(320, 300);
setVisible(true);
}
public void save() {
FileWriter fileWriter = null;
String text = textArea.getText();
try{
fileWriter = new FileWriter("test.txt", true);
fileWriter.write(text, 0, text.length());
fileWriter.close();
}
catch(IOException e){
//System.out.println("Błąd podczas zapisu pliku.");
}
}
public static void main(String args[]) {
//treść metody main
}
}
Tworzony jest jeden komponent klasy JTextArea (reprezentowany przez pole textArea)
oraz jeden przycisk, tak jak widać na rysunku 8.28. Aplikacja działa w taki sposób, że
po kliknięciu przycisku cały tekst z pola tekstowego zostaje zapisany w pliku o nazwie
test.txt (jeżeli pliku nie będzie na dysku, zostanie utworzony, a jeśli będzie, zawartość
pola zostanie dopisana na jego końcu). Obsługę zdarzeń związanych z klikaniem przyci-
sku zapewnia obiekt klasy anonimowej implementującej interfejs ActionListener,
dokładnie tak samo jak w przypadku wcześniej prezentowanych przykładów. Po klik-
nięciu przycisku jest zatem wywoływana metoda actionPerformed, która sprawdza,
czy sprawcą zdarzenia jest rzeczywiście przycisk button1. Jeśli tak, wywoływana jest
metoda save z klasy Aplikacja. Metoda save korzysta z obiektu klasy FileWriter do
zapisania zawartości komponentu textArea w pliku (por. lekcja 34.).
Rysunek 8.28.
Aplikacja
wykorzystująca pole
tekstowe klasy TextArea
Pola JCheckBox
Klasa JCheckBox tworzy komponenty będące polami wyboru, które umożliwiają zazna-
czanie opcji. Konstruktory klasy zostały przedstawione w tabeli 8.10, a program wyświe-
tlający sześć pól typu JCheckBox pokazano na listingu 8.43. Wygląd okna takiej aplikacji
jest widoczny na rysunku 8.29.
b4ac40784ca6e05cededff5eda5a930f
b
412 Java. Praktyczny kurs
Listing 8.43.
import javax.swing.*;
import java.awt.*;
add(cb1);
add(cb2);
add(cb3);
add(cb4);
add(cb5);
add(cb6);
setSize(320, 200);
setVisible(true);
}
public static void main(String args[]) {
//kod metody main
}
}
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 413
Rysunek 8.29.
Pola JCheckBox
umieszczone w oknie
aplikacji korzystającej
z rozkładu siatkowego
Od razu widać pewną różnicę w stosunku do poprzednich przykładów: tym razem nie
są podawane żadne współrzędne miejsc, w których miałyby się znaleźć poszczególne
elementy okna. Zamiast tego zastosowano jeden z rozkładów automatycznych, tzw. roz-
kład tabelaryczny lub siatkowy (ang. grid layout). Za jego ustawienie odpowiada linia:
setLayout(new GridLayout(3, 2));
Parametrem metody setLayout jest nowy obiekt klasy GridLayout. Dzięki temu po-
wierzchnia okna aplikacji zostanie podzielona na sześć części, tabelę o trzech wierszach
i dwóch kolumnach. Komponenty — dodawane do okna aplikacji — będą od tej chwili
automatycznie umieszczane w kolejnych komórkach takiej tabeli, dzięki czemu uzyskamy
ich równomierny rozkład.
Listy rozwijane
Tworzenie list rozwijalnych umożliwia klasa o nazwie JComboBox. Jej konstruktory
zostały przedstawione w tabeli 8.11, a wybrane metody w tabeli 8.12. Oczywiście
w rzeczywistości metod jest o wiele więcej, jednak te zaprezentowane poniżej umoż-
liwiają wykonywanie podstawowych operacji. Przykład użycia listy został zobrazo-
wany w kodzie widocznym na listingu 8.44.
Listing 8.44.
import javax.swing.*;
import java.awt.event.*;
b4ac40784ca6e05cededff5eda5a930f
b
414 Java. Praktyczny kurs
public Aplikacja() {
ActionListener al = new ActionListener() {
public void actionPerformed(ActionEvent e) {
int itemIndex = cmb.getSelectedIndex();
if(itemIndex < 1){
return;
}
String itemText = cmb.getSelectedItem().toString();
label.setText(itemText);
}
};
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(null);
b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8. Aplikacje i aplety 415
add(cmb);
add(label);
setSize(340, 200);
setVisible(true);
}
public static void main(String args[]) {
//kod metody main
}
}
Została tu utworzona aplikacja zawierająca jedną listę rozwijaną oraz jedną etykietę, tak
jak jest to widoczne na rysunku 8.30. Działanie programu jest następujące: po wybraniu
z listy nowej pozycji przypisany jej tekst pojawi się na etykiecie.
Rysunek 8.30.
Wygląd aplikacji
z listingu 8.44
Sposób utworzenia listy oraz etykiety nie powinien budzić żadnych wątpliwości, odbywa
się to podobnie jak we wcześniejszych przykładach z bieżącej lekcji. W przypadku li-
sty została użyta składnia typów uogólnionych, dzięki czemu wiadomo, że na liście
będą umieszczane elementy typu String (ciągi znaków). Elementy listy są dodawane
za pomocą metody addItem z klasy JComboBox. Zdarzenia nadchodzące z listy obsługuje
obiekt klasy anonimowej implementującej interfejs ActionListener (obiekt reprezen-
towany przez zmienną al). Jest w nim oczywiście zdefiniowana tylko jedna metoda —
actionPerformed. Za pomocą wywołania cmb.getSelectedIndex() pobiera ona naj-
pierw indeks zaznaczonego elementu listy. Jeśli okaże się, że indeks ten jest mniejszy
od 1 (co by oznaczało, że albo nie został zaznaczony żaden z elementów, albo też za-
znaczony jest element o indeksie 0), metoda kończy działanie, wywołując instrukcję
return. Jeśli indeks jest większy od 0, pobierany jest tekst przypisany zaznaczonemu
elementowi listy:
String itemText = cmb.getSelectedItem().toString();
który następnie jest używany jako argument metody setText obiektu etykiety:
label.setText(itemText);
b4ac40784ca6e05cededff5eda5a930f
b
416 Java. Praktyczny kurs
Ćwiczenie 42.1.
Napisz aplikację zawierającą pole tekstowe, etykietę i przycisk. Po kliknięciu przycisku
zawartość pola tekstowego powinna się znaleźć na etykiecie.
Ćwiczenie 42.2.
Zmodyfikuj kod z listingu 8.39 tak, aby zamknięcie aplikacji następowało jedynie po
kliknięciu przycisków w kolejności: Pierwszy przycisk, Drugi przycisk.
Ćwiczenie 42.3.
Do aplikacji z listingu 8.44 dodaj przycisk. Po jego kliknięciu powinny zostać usunięte
wszystkie elementy listy.
Ćwiczenie 42.4.
Napisz aplikację pozwalającą użytkownikowi na dodawanie i usuwanie elementów listy
rozwijanej.
Ćwiczenie 42.5.
Napisz aplikację umożliwiającą prostą edycję plików tekstowych (wczytywanie, zapi-
sywanie, modyfikację treści).
b4ac40784ca6e05cededff5eda5a930f
b
Skorowidz
A D
adapter MouseInputAdapter, 390 deklaracja, 24
aplety, 351, 354 metody, 120
konstrukcja, 355 tablicy, 77, 80, 85
parametry, 356 typu uogólnionego, 345
tworzenie, 352 zmiennej typu klasowego, 93
aplikacja, 351, 386 deklaracje
Eclipse, 6 proste, 24
jEdit, 6 wielu zmiennych, 25
NetBeans, 6 dekrementacja, 35
Notepad++, 6 domyślna strona kodowa, 293
argumenty domyślne wartości typów danych, 99
finalne, 162 dookreślanie typu, 341
konstruktorów, 111 dostęp
metod, 100 chroniony, protected, 133
ASCII, 270 do katalogu, 298
do klasy wewnętrznej, 253
B do klasy zewnętrznej, 245
do obiektu, 248
blok do składowych, 261
default, 56 prywatny, private, 131
else, 48 publiczny, public, 130
finally, 192 dynamiczna zmiana pojemności, 327
switch, 55 dynamiczny wektor elementów, 328
try…catch, 165, 169–171, 180 działania arytmetyczne, 34
błąd działanie
arytmetyczny, 179 etykietowanej instrukcji break, 72
implementacji interfejsu, 234 etykietowanej instrukcji continue, 70, 72
kompilacji, 31, 39, 127, 143, 197, 256 konstruktorów odziedziczonych, 126
w pętli while, 274 operatora dekrementacji, 37
w programie, 275 operatora inkrementacji, 36
bufor, 315 pętli for, 77
przeciążonej metody, 203
C dziedziczenie, 121, 122
interfejsów, 235, 238
ciąg po klasie zewnętrznej, 252
formatujący, 289 dzielenie przez zero, 173
znaków quit, 330 dźwięki, 383
czcionki, 359
b4ac40784ca6e05cededff5eda5a930f
b
418 Java. Praktyczny kurs
E if…else if, 51
Math.sqrt(), 50
edytor tekstowy, 6 return, 97
elementy tablic, 76 switch, 53–57
elipsa, 369 System.out.print, 33, 87
enkapsulacja, 134 System.out.println, 28
etykiety, 71 throw, 183, 190
puste, 405 instrukcje sterujące, 47
tekstowe, 404, 405 interfejs, 222, 224
ActionListener, 393, 397
AudioClip, 383
F DocumentListener, 408
figury geometryczne, 366 Drawable, 224, 228, 229
finalne Iterable, 333–335
argumenty metod, 162 Iterator, 335
klasy, 156 jako typ zwracany, 254
metody, 161 MouseListener, 376–380, 385, 390, 401
pola, 157, 159 MouseMotionListener, 380
formatowanie, 290 WindowListener, 387
interfejsy funkcjonalne, 261, 264
G
J
grafika, 366
jawna konwersja typu, 197
JDK, Java Development Kit, 6
H JRE, Java Runtime Environment, 6, 10
hermetyzacja, 134
hierarchia
klas, 224
K
wyjątków, 176 katalogi, 298, 304
pobranie zawartości, 298
tworzenie, 302
I usuwanie, 304
iloczyn logiczny, 43 klasa, 17, 91
implementacja ActionEvent, 394, 398
interfejsu, 223 Aplet, 365
interfejsu MouseListener, 401 Applet, 352, 383
wielu interfejsów, 230 ArrayList, 328
importowanie pakietu, 271 BufferedInputStream, 321
inferowanie typu, 341 BufferedOutputStream, 321
informacje BufferedReader, 272, 320
o kliknięciach, 379 CheckBoxMenu, 398
o stanie załadowania obrazu, 375 Color, 363
inicjalizacja Component, 403
obiektu, 110 Console, 288, 289
pól finalnych, 160 Dane, 118
tablicy, 80, 85 DivideByZeroException, 190
inkrementacja, 35 Double, 278
instalacja Exception, 183
JDK, 9 File, 295, 302
w systemie Linux, 11 FileInputStream, 316
w systemie Windows, 10 FileOutputStream, 318
instrukcja, 23 FileReader, 316, 318
break, 55, 66–68 FileWriter, 318, 411
continue, 68–71 Font, 359
if…else, 47 FontMetrics, 361
b4ac40784ca6e05cededff5eda5a930f
b
Skorowidz 419
b4ac40784ca6e05cededff5eda5a930f
b
420 Java. Praktyczny kurs
L hasNextInt, 287
imageUpdate, 374
liczba, 277 invokeLater, 387
linie, 368 list, 298
lista czcionek, 359, 361 main, 106
listy rozwijane, 413 mkdir, 303
mkdirs, 303
mouseClicked, 378, 380, 385
M mouseDragged, 380
maszyna wirtualna, 6 next, 335
menu, 391 nextLine, 287
kaskadowe, 396, 397 nextToken, 280–282
kontekstowe, 400, 401 paint, 362
umożliwiające zaznaczanie, 399 parseDouble, 278
metoda parseInt, 277
actionPerformed, 393, 408 play, 385
actionPreformed, 395 printf, 289
add, 330, 392 read, 270, 271, 313
addActionListener, 379 readLine, 272, 275
addMouseListener, 379, 385 remove, 335
addWindowListener, 387–390 resize, 325, 327
clearRect, 352 save, 411
createNewFile, 302 set, 325, 326
delete, 304 setActionCommand, 395
dispose, 390, 395 setBounds, 405
draw, 213 setColor, 362
drawImage, 373 setDocumentListener, 408
drawOval, 369 setFont, 362
drawPolygon, 371 setJMenuBar, 392
drawRectangle, 370 setLayout, 405
drawShape, 215 setSize, 386
drawString, 386 setText, 405
exists, 303, 310 show, 345, 401
fillOval, 369 size, 325
fillPolygon, 371 start, 286
fillRectangle, 370 stringWidth, 362
finalize, 118 super, 127
get, 326, 343 System.gc, 118
getActionCommand, 395 toString, 202
getAllFonts, 360 windowClosing, 389
getAudioClip, 384 write, 313
getAvailableFontFamilyNames, 360 metody, 95
getButton, 377 argumenty, 100
getCodeBase, 372 domyślne, 227, 229
getDocumentBase, 372 finalne, 161
getFontMetrics, 362 interfejsu DocumentListener, 409
getHeight, 362 interfejsu Iterable, 334
getImage, 372, 373 interfejsu MouseListener, 376
getItem, 397 interfejsu MouseMotionListener, 380
getLocalGraphicsEnvironment, 360 interfejsu WindowListener, 388
getMessage(), 174 klasy anonimowej, 259
getParameter, 357 klasy ArrayList, 329
getSource, 394, 399 klasy Component, 403, 404
getValues, 347 klasy Console, 289
getX, 377 klasy File, 296, 297, 298
getY, 377 klasy FileInputStream, 316
hasNext, 335 klasy FileOutputStream, 318
b4ac40784ca6e05cededff5eda5a930f
b
Skorowidz 421
b4ac40784ca6e05cededff5eda5a930f
b
422 Java. Praktyczny kurs
pliki
dźwiękowe, 383
R
kopiowanie, 312, 314, 320 referencja, reference, 27
odczyt danych, 309, 316 referencje do obiektu, 102, 114, 207
o dostępie swobodnym, 307 reprezentacja liczb, 42
operacje strumieniowe, 316 RGB, 363
tworzenie, 302 rodzaje
usuwanie, 304 klas wewnętrznych, 246, 249
zapis danych, 310, 318 pól tekstowych, 407
pobranie wyjątków, 173
tokena, 281 rodziny czcionek, 361
zawartości katalogu, 298 rozmiar tablicy, 78, 326
pola, 91, 149 równanie kwadratowe, 50, 284, 286
finalne, 157 rysowanie
finalne typów prostych, 158 elips, 369
finalne typów referencyjnych, 159 figur, 366
interfejsów, 226 kół, 369
JCheckBox, 411 linii, 368
niezainicjowane, 98 prostokątów, 368, 370
statyczne, 153 punktów, 368
tekstowe, 407, 408 wielokątów, 370, 371
pole rzeczywisty typ obiektu, 204
nval, 281 rzutowanie
sval, 281 na interfejs, 256
ttype, 280 na typ Object, 201
polecenie na typy interfejsowe, 254
chcp, 30 na typy klasowe, 257
cmd, 12 obiektów, 195
import, 138 typów obiektowych, 128, 197, 201, 341
javac, 14 w górę, 206
polimorficzne wywoływanie metod, 208, 220, 259
polimorfizm, 195, 206
polskie znaki, 292 S
ponowne zgłaszanie wyjątków, 187 sekcja finally, 190
poprawność danych, 165 sekwencja ucieczki, 32
późne wiązanie, 204, 206 sekwencje znaków specjalnych, 32
praklasa, 202 składnia
priorytety operatorów, 45 ExtendedOutside.Inside, 253
problem kontroli typów, 337 Outside.Inside, 253
programowanie obiektowe, 91, 195 składowe
przechowywanie wielu danych, 323 klas wewnętrznych, 242
przechwytywanie wielu wyjątków, 177 klasy, 92
przeciążanie RGB, 364
konstruktorów, 112 statyczne, 152
metod, 108 skrypt startowy powłoki, 13
przeglądanie kontenerów, 333 słowo kluczowe
przekroczenie zakresu wartości, 41 else, 47
przesłanianie extends, 235
metod, 144–149 final, 156
pól, 146, 149, 151 implements, 226
przyciski, 405 package, 137
przypisanie, 24 super, 211
punkty, 368 this, 114, 116
void, 96, 100
b4ac40784ca6e05cededff5eda5a930f
b
Skorowidz 423
b4ac40784ca6e05cededff5eda5a930f
b
424 Java. Praktyczny kurs
wartość wywołania
aktualnego tokena, 280 konstruktorów, 216
null, 27, 120, 275 metod, 115, 219
warunek zakończenia pętli, 274 metod klas pochodnych, 204
wbudowane typy danych, 18 metod klasy wewnętrznej, 247
wczesne wiązanie, 208 metod nieistniejących, 225
wczytywanie metod przesłoniętych, 147
danych, 277, 283, 286, 288 złożone konstruktorów, 219
grafiki, 372
tekstu, 271, 273, 277
wersje Javy, 7 Z
wiązanie zagnieżdżanie bloków try…catch, 180, 186
czasu wykonania, 206 zakończenie działania programu, 274
dynamiczne, 206 zakres
wielokąty, 370 wartości zmiennej, 40
wiersz typów arytmetycznych, 18
poleceń, 12 typów zmiennoprzecinkowych, 19
tekstu, 273 zapis do pliku, 310, 318
wirtualna maszyna Javy, 15 zaznaczanie pozycji, 399
właściwość length, 78, 83 zdarzenia
wprowadzanie, Patrz wczytywanie myszy, 377
współrzędne związane z oknem, 387
biegunowe, 134 zgłaszanie
kliknięcia, 380 wyjątku, 182
wybór składników JDK, 10 ponowne wyjątku, 185
wyjątek, exception, 165, 169 zmiana
ArithmeticException, 175, 179, 181 czcionki, 31
ArrayIndexOutOfBoundsException, 76, 165, strony kodowej, 30
169, 324 zmienna środowiskowa
ClassCastException, 205, 206, 337 CLASSPATH, 139, 140
DivideByZeroException, 189 PATH, 13
FileNotFoundException, 308, 310 zmienne, 23, 102
IOException, 271, 303 referencyjne, 94
NullPointerException, 179, 181, 276 typów odnośnikowych, 26
NumberFormatException, 277, 278, 365 znacznik
PatternSyntaxException, 300 <applet>, 353
UnsupportedEncodingException, 293, 294 <object>, 353
UnsupportedOperationException, 335 znaczniki formatujące printf, 290
wyjątki własne, 182, 188 znak %, 289
wykonanie programu, 15 znaki
wykorzystanie polskie, 29
kontenerów, 339 specjalne, 32
obiektów, 140 zwracanie
pakietów, 139 wielu wartości, 346
wyrażenia wyniku, 97
lambda, 266
regularne, 299–302
wyświetlanie
elementów tablicy nieregularnej, 87
menu, 401
napisu, 386
polskich znaków, 294
przycisków, 405
systemowego komunikatu, 175
wartości zmiennych, 27
zawartości katalogu, 299
zawartości tablicy, 81
b4ac40784ca6e05cededff5eda5a930f
b
b4ac40784ca6e05cededff5eda5a930f
b