Subido por Piotr J

Lis M. - Java. Praktyczny kurs. Wydanie IV

Anuncio
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
4
Java. Praktyczny kurs
Rozdział 4. Wyjątki .......................................................................................... 165
Lekcja 20. Blok try…catch ..................................................................................... 165
Lekcja 21. Wyjątki to obiekty ................................................................................ 173
Lekcja 22. Własne wyjątki ..................................................................................... 182
Rozdział 5. Programowanie obiektowe. Część II .............................................. 195
Polimorfizm .................................................................................................................. 195
Lekcja 23. Konwersje typów i rzutowanie obiektów .............................................. 195
Lekcja 24. Późne wiązanie i wywoływanie metod klas pochodnych ..................... 204
Lekcja 25. Konstruktory oraz klasy abstrakcyjne ................................................... 212
Interfejsy ....................................................................................................................... 222
Lekcja 26. Tworzenie interfejsów .......................................................................... 222
Lekcja 27. Wiele interfejsów .................................................................................. 230
Klasy wewnętrzne ........................................................................................................ 238
Lekcja 28. Klasa w klasie ....................................................................................... 238
Lekcja 29. Rodzaje klas wewnętrznych i dziedziczenie ......................................... 246
Lekcja 30. Klasy anonimowe i zagnieżdżone ......................................................... 254
Rozdział 6. System wejścia-wyjścia ................................................................ 269
Lekcja 31. Standardowe wejście ............................................................................ 269
Lekcja 32. Standardowe wejście i wyjście ............................................................. 279
Lekcja 33. System plików ...................................................................................... 295
Lekcja 34. Operacje na plikach .............................................................................. 306
Rozdział 7. Kontenery i typy uogólnione .......................................................... 323
Lekcja 35. Kontenery ............................................................................................. 323
Lekcja 36. Typy uogólnione ................................................................................... 336
Rozdział 8. Aplikacje i aplety ......................................................................... 351
Aplety ........................................................................................................................... 351
Lekcja 37. Podstawy apletów ................................................................................. 351
Lekcja 38. Kroje pisma (fonty) i kolory ................................................................. 358
Lekcja 39. Grafika .................................................................................................. 366
Lekcja 40. Dźwięki i obsługa myszy ...................................................................... 376
Aplikacje ...................................................................................................................... 386
Lekcja 41. Tworzenie aplikacji .............................................................................. 386
Lekcja 42. Komponenty ......................................................................................... 402
Skorowidz .................................................................................... 417
Wstęp
Krótka historia Javy
Trudno znaleźć osobę interesującą się zagadnieniami informatycznymi, która nie słyszałaby o Javie. Java jest językiem, który w bardzo krótkim czasie zdobył popularność i uznanie programistów na całym świecie. Dawniej kojarzono go głównie z internetem i siecią
WWW, a także z telefonami komórkowymi, choć tak naprawdę zawsze był to doskonały
obiektowy (zorientowany obiektowo) język programowania, nadający się do różnorodnych zastosowań: od krótkich apletów do bardzo poważnych i rozbudowanych aplikacji.
Jego początki były jednak dużo skromniejsze.
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 kuchenkami mikrofalowymi, a więc praktycznie każdym urządzeniem, które zawiera mikroprocesor. 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 uruchomić 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 został opracowany przez zespół kierowany przez Jamesa Goslinga w firmie Sun Microsystems, choć w rzeczywistości prace były prowadzone na Uniwersytecie Stanford. Projekt 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 świecie. 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
6
Java. Praktyczny kurs
to doskonale skonstruowany język programowania, który programistom przypada do
gustu zwykle już przy pierwszym kontakcie. Od Javy po prostu nie ma już odwrotu.
Obecnie to dojrzała, choć wciąż rozwijana technologia, z której korzystają rzesze programistów na całym świecie.
Narzędzia
Do programowania w Javie będzie potrzebne odpowiednie środowisko programistyczne
zawierające niezbędne narzędzia: minimum to kompilator oraz tzw. maszyna wirtualna. 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 zaznaczane w tekście.
Do testowania apletów przedstawionych w rozdziale 8. można użyć dowolnej przeglądarki internetowej obsługującej język Java lub zawartej w JDK aplikacji appletviewer.
Większość obecnie dostępnych na rynku przeglądarek udostępnia Javę poprzez mechanizm wtyczek, umożliwiając zastosowanie najnowszych wersji JRE (Java Runtime
Environment), czyli środowiska uruchomieniowego.
Oprócz JDK będzie potrzebny jedynie dowolny edytor tekstowy umożliwiający wpisywanie 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 oferujący takie udogodnienia jak kolorowanie składni i numerowanie wierszy kodu. Dla środowiska 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.
Istnieje również możliwość zastosowania zintegrowanych graficznych środowisk programistycznych, takich jak Eclipse (http://www.eclipse.org/) czy NetBeans (http://netbeans.
org/), które ułatwiają projektowanie aplikacji (oba zostały napisane w Javie). Dla
osób początkujących lepszy byłby jednak najprostszy edytor tekstowy — takie rozwiązanie pozwoli najpierw poznać dobrze sam język, a dopiero potem bardziej zaawansowane środowiska programistyczne.
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.
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 kilkunastu 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”, logicznej 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.
Java. Praktyczny kurs
w
w
w
.e
bo
o
k4
al
l.p
l
8
Rozdział 1.
Podstawy
�
Przed wypłynięciem na szerokie wody programowania w Javie�eba zapoznać się z pod­
stawami. W tym rozdziale zostanie pokazane, jak wygląda �'b,.ra programu w Javie,
czym jest i jak przeb�ega ko�p lacja ?ra� jak t pro�am . c?omić.. Prz�dstawione
zostaną podstawy ob1ektowosc1, czyh naJważmeJS � d!u ma z dz1edzmy progra­
mowania obiektowego, oraz pojęcie typu danych. Z · · się tu także krótkie omówie­
eścić w progra­
ni� typów danych występujących w Javie. Okaż�i , jak sposób
_ '-.w
rme komentarz, co, wbrew pozorom, Jes
zynnoscią,
pomeważ
pozwala na
t
dokumentowanie kodu, poprawia jego czy
i ułatwia analizę.
�
��
��
�
�
��
�
�
Czytelnicy, którzy znają podstawy p
wania, mogą spokojnie pominąć ten roz­
dział i przejść do drugiego, aby��' jak w Javie wyglądają typowe konstrukcje
programistyczne, takie jak pęt��trukcje warunkowe, deklaracje zmiennych oraz
tablice. Natomiast osoby, które\c:W#ero zaczynają swoją przygodę z programowaniem,
powinny przeczytać ten r ziAł dokładnie- nie jest on długi i stanowi wprowadzeię przejmować, jeśli nie wszystko od razu będzie całkiem
nie w świat Javy. Nie
jasne. Z wieloma P.
·ami i konstrukcjami programistycznymi trzeba się po prostu
oswoić, wyko
tyczne przykłady. Temu przecież służy ta książka.
�
�p
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.
10
Java. Praktyczny kurs
Instalacja w systemie Windows
W chwilę po uruchomieniu pliku instalacyjnego (np. jdk-8-windows-i586.exe dla JDK 81
lub jdk-1. 7.0.windows-i586.exe dla JDK 7; są dostępne pod adresami podanymi we wstę­
pie) na ekranie ukaże się okno startowe instalatora, w któ-rym należy kliknąć przycisk
Next. Kolejne okno, widoczne na rysunku 1.1, pozwala na wybranie składników in­
stalacji oraz katalogu docelowego (standardowo C: \Program FilesVava!Jdkl.8.0). Po
kliknięciu Next rozpocznie się właściwy proces instalacji.
Rysunek 1.1.
Java SE Development Kit 8 - Custom Setup
Okno wyboru
składnikó w JDK
�)ja a··
-.::::
ORACLE"
V
-
---
-
------
Select optional feab.Jres to install from the list below, You can change your choice of feab.Jres after
installation by using the Add/Remove Programs ulility in the eontroi Panel
�
Fe
e Descriplion
velopment Kit 8,
g e JavaFX SDK" a
JRE, and the Java
eontroi mols suite, lhis
l ill require 180MB on your hard
drive.
J
Source eode
Public: JRE
S
lnstall to:
c,�mg<Om
' ' "�"''''';.:._a
__________
V
Jeżeli w oknie wyboru JD
L
< !łack
]l
LQ�h ange.�
_
_
�e)(t >
lL
-_ _ _ _
�ancel
J
�
ostała wyłączona opcja instalacji JRE (ang. Java
· lrodowiska uruchomieniowego pozwalającego na uruchamianie aplikacji i a e
napisanych w Javie, a JRE nie było wcześniej instalowane
w systemie jako o
produkt, w trakcie instalacji JDK na ekranie pojawi się okno
pozwalające na� bór katalogu docelowego JRE (rysunek 1.2). Po kliknięciu Next
JRE również �ie zainstalowane.
Runtime Environment),
Rysunek 1.2.
Okno wyboru
składnikó w JDK
Java Setup - Destination Folder
� java·
ORACLE"
Install to:
Qange�
e: \program Files\Java\jre8 \
eancel
1 Uwaga: Java 8 oficjalnie nie współpracuje
z
systemami Windows poniżej wersji 7.
Rozdział 1. + Podstawy
11
Instalacja w systemie Linux
Do wyboru mamy instalację za pomocą dystrybucji binarnej lub w postaci pakietu
RPM. W pierwszym przypadku do dyspozycji jest plik o schematycznej nazwie:
jdk-wersja-linux-proc.tar.gz
np.:
jdk-8-linux-i586.tar.gz
jdk-1.7.0-linux-i586.tar.gz
Należy go skopiować lub przenieść do wybranego katalogu (do katalogu, w którym
ma być zainstalowana Java, np. /usrljava), wydając jedno z poleceń:
cp .jjdk-8-linux-i586.tar.gz jusrjjava/
mv .jjdk-8-linux-i586.tar.gz jusrjjava/
Następnie można przejść do tego katalogu, wydając korne
cd jusrjjava
�
�
�
�
��
Wtedy w bieżącym katalogu (� omawiany��kładzi � bę�ie
06
1i;m znajdą Się phki pakietu.
utworzony podkatalog nazwie
oraz rozpakować plik, wpisując polecenie:
•
tar xvfz jdk-8-linux-i586.tar.gz
O
jdkJ.8.
�o_lusr1"aval) zostanie
�ać plik do wybranego katalogu, korzystając
�-
Alternatywnie można od razu ri)z
z opcji C archiwizatora tar, np.:
�
�
�
�·
tar xvfz jdk-8-linux-i586.ta
W drugim przypa
jdk-wersjanp.
-C jusrjjava
M) do dyspozycji jest plik o nazwie:
rpm
jdk-8-linux-i586-rpm
jdk-1.7.0-linux-i586-rpm
Instalacja odbywa się przez wydanie polecenia:
rpm -ivh jdk-8-linux-i586.rpm
Po jej zakończeniu pakiet JDK jest gotowy do użycia.
Przygotowanie do pracy z
JDK
Narzędzia zawarte w JDK pracują w trybie tekstowym i w nim też będą uruchamiane
pierwsze programy ilustrujące cechy języka. Ponieważ w dobie systemów oferujących
graficzny interfejs użytkownika z takiego trybu korzysta się coraz rzadziej, warto przy­
pomnieć, jak uruchomić go w systemach Linux i Windows.
12
Java. Praktyczny kurs
Jeśli pracujemy w Linuksie na konsoli tekstowej, nie trzeba oczywiście wykonywać
żadnych dodatkowych czynności. Przy korzystaniu z interfejsu graficznego wystarczy
natomiast uruchomić program konsoli (terminala). Pojawi się wtedy okno (podobne
do przedstawionego na rysunku 1.3), w którym można wydawać polecenia tekstowe.
Rysunek 1.3.
Konsola systemowa
w Lżnuksże
kowalski@localhost:�
.Eiik Edycja Widok Terminal
[ j kmo�alski@localhos t --] $
Zakłagki
Pomo�
l
Jeśli pracujemy w systemie Windows, wciskamy na klawiaturze kombinację klawiszy
2
Windows+R , w polu Otwórz (w niektórych wersjach systemu- Uruchom) wpisujemy
3
cmd lub cmdexe i klikamy przycisk OK lub wciskamy klawisz Enter (rysunek 1.4).
W systemach XP i starszych pole Uruchom jest też dostępne bezpośrednio w menu
Start. Można też w dowolny inny sposób uruchomić apli ję cmdexe. Pojawi się
ie można wydawać
wtedy okno wiersza poleceń (wiersza polecenia), w który B
komendy. Jego wygląd dla systemów z rodziny Window
stał przedstawiony na
rysunku 1.5 (w innych wersjach wygląda ono bardz
�
�
�
�001 .
Rysunek 1.4.
Uru
Uruchamianie wżersza
poleceń w systemie
Windows
Qtwórz:
OK
l LAnuluj-.J [iZeglądaj=:J
Rysunek 1.5.
Konsola systemowa
w systemie Windows
Po instalacji, aby usprawnić pracę z JDK, warto dodać do zmiennej środowiskowej
PATH ścieżkę do podkatalogu bin tego pakietu (dla typowych instalacji jest to C: \Program
FilesVavdydkl.8.0\bin lub /usrljavaljdkl.8.0/bin/- zależnie od użytego systemu).
2
Klawisz funkcyjny Windows jest też opisywany jako Start.
W starszych systemach (Windows 98, Me) należy uruchomić aplikację commandexe
(Start/Uruchom/commandexe ).
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.
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\jdkl.8.0\bin\javac
lub w Linuksie:
jusrjjavajjdkl.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\jdkl.8.0\bin
»�
Aby natomiast wykonać tę operację w Linuksie w
bash, trzeba wydać polecenie:
�
"'��
�
PATH=$PATH:jścieżka_dostępu
np.:
PATH=$PATH:jusrjjavajjdkl.8.0/bin/
u powłoki systemowej
o0v
�
e na stałe, należy skorzystać z odpowiednich
Jeżeli zmiany ścieżki mają b�
uksie jest to dopisanie odpowiedniego polecenia do
mechanizmów systemowych.
skryptu startowego powło
np. .bash_profile), w Windowsie w oknie właściwości sys­
temu należy odnaleźć R
ennych środowiskowych i tam wprowadzić modyfikację
do zmiennej PATH.
�
i.._f<{
�
�
�
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.
14
Java. Praktyczny kurs
Listing 1.1.
class Main {
public static void main(String args[])
System.out.println("To jest napis.");
Kod widoczny na lisringu 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 wiers
leceń, pisząc:
�
�
�
��
a�
javac Main.java
0
Należy przy tym zwrócić uwagę, aby to polecenie
ać, gdy katalogiem bieżą­
a
I �żąc�, używa się polecema cd,
cymJest ten z phkiemM_ażn.!a�a (aby �emc
mż bieżący, trzeba dodatkowo
np. cd c:\java\). Jeżeh phk Jest w mn m
· ·ava jest zapisany na dysku c: w kapodać jego lokalizację. Przykładowo jeśli
ć postać:
talogu java, to komenda kompilująca mo e
�
javac c:\java\Main.java
�
Uwaga
<::)
""''
Jeśli po wykonaniu polecenia t?v aXai n .java na ekranie pojawi się komunikat o nie­
rozpoznanym polecen ·u
va �sunki 1.6 i 1.7), należy postąpić zgodnie z opisem
przedstawionym w pu
Przygotowanie do pracy z JDK" lub przy kompilacji podać
pełną ścieżkę dosl_
�
u
kompilatora. Komenda będzie miała wtedy postać (o ile
.
�·
ę
kompilator znajd
c:\Progr
�
w lokalizacji
c:\Program FilesVavaVdk1.8.0\bin):
s\java\jdkl,B,O\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\jdkl.8.0\bin\javac c:\java\Main.java
c,\
E:\WINDOWS\system32\cmd.exe
lli1E1
E:\java>javac Main.java
Nazwa 'javac' nie jest roz�oznawana jako polecenie wewnętrzne lub zewnętrzne,
program wykonywalny lub pl1k wsadowy.
E:\java>_
Rysunek 1.6. Komunikat o nierozpoznanym poleceniu javac w Windows XP
11
l
Rozdział 1. + Podstawy
15
Rysunek 1.7. Komunikat o nierozpoznanym poleceniu javac w Windows 8
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
nap i s. (rysunek 1.8).
ienia, napis:
To jest
•
Rysunek 1.8.
Kompilacja i wykonanie
pierwszego programu
w Javie
U:WiiQ'il
Zarówno przy pisaniu
, jak i przy wydawaniu komend w wierszu pole­
ceń należy zwrócić uw·�� :,ielkość wpisywanych liter. Jakakolwiek pomyłka w większości przypadków
uje, że kompilacja się nie uda.
�
��
W tym miejscu- .a zazwyczaj pytanie, czemu do uruchomienia skompilowanego pro­
gramu używa
lecenia java i dlaczego powstały plik ma rozszerzenie class. Otóż
projektanci Javy założyli, że będzie to język przenośny, zatem stworzone w nim programy
będą mogły być uruchamiane na różnych platformach sprzętowo-systemowych, czyli
różnych komputerach, systemach operacyjnych i innych urządzeniach. Skoro program
skompilowany na pececie pod systemem Windows ma się również uruchomić pod
Linuksem działającym na komputerze wyposażonym w procesor Motoroli, podczas
kompilacji nie może powstawać kod charakterystyczny dla jednego procesora (jednej
rodziny procesorów).
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 Mai n. c l ass to polecenie oznaczające: uruchom wirtualną ma­
szynę Javy i przekaż jej do wykonania kod zawarty w plikuMain.class.
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 klasi i rozszerzeniu java. Zatem klasa o nazwie
Main musi znajdować się w pliku o nazwie Mainjava, 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 ll 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 l. 2.
class Main {
public static void main(String args[])
l/tutaj instrukcje do wykonania
}
C- w1czen1a
.
" do
samod"l
z1e nego wy konan1a
.
Ćwiczenie 1.1.
.�
��
� To jest mój drugi program.
Q
�V
Napisz program wyświetlający na ek
piluj go i uruchom.
Ćwiczenie 1.2.
� �{
�
apis:
Skom-
0
� o rozszerzeniu innym niż java, np. txt. Spróbuj dokonać
��
Zapisz kod z listin
kompilacji tego pli .
Lekcja
2.
Po
obserwuj, co się stanie.
�awy obiektowaści i typy danych
Wiadomo już, jak napisać w Javie najprostszy program wyświetlający na ekranie napis,
jak go skompilować i uruchomić. Teraz czas poznać nieco teorii dotyczącej podstaw
programowania obiektowego oraz typów danych. W lekcji 2. zostaną przedstawione
klasy i obiekty, pojęcie typu danych, a także to, jakie typy danych występują w Javie.
Znajomość tych informacji będzie potrzebna przy zapoznawaniu się z kolejnymi lekcjami
w rozdziale 2., omawiającym instrukcje języka, oraz w rozdziale 3., poświęconym
budowaniu klas.
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ą.
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 podstawowe pojęcia związane z programowaniem obiektowym. Będzie o tym mowa w rozdziale 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 programistyczny. 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 programistycznemu (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 mieszkanie 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 wykonywać. 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 wydawać 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.
18
Java. Praktyczny kurs
wydawać polecenia realizowane potem przez obiekty. Obiekt powstaje jednak dopiero 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.
Wbudowane typy danych
Java udostępnia pewną liczbę wbudowanych typów danych, czyli takich, które oferuje
sam język i z których można korzystać bez potrzeby ich definiowania. Nazywa się je
typami prostymi lub podstawowymi (ang. primitive types). Można je podzielić na typy
arytmetyczne, typ char oraz typ logiczny (boolean). Typy arytmetyczne dzielą się z kolei
na typy całkowitoliczbowe i zmiennopozycyjne (zmiennoprzecinkowe). Oprócz tego istnieje typ obiektowy (odnośnikowy, referencyjny), który pozwala na posługiwanie się
obiektami, jednak nim zajmiemy się dopiero w rozdziale 3.
Typy arytmetyczne całkowitoliczbowe
Typy arytmetyczne całkowitoliczbowe służą do reprezentacji liczb całkowitych i dzielą
się na cztery rodzaje:
 byte,
 short,
 int,
 long.
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
Osoby programujące w C lub C++ powinny zwrócić uwagę, że zakres możliwych do
przedstawienia wartości jest ustalony z góry i nie zależy od platformy systemowej, na której jest uruchamiany program w Javie. To oznacza, że niezależnie od tego, na jakim
systemie się pracuje (16-, 32- czy 64-bitowym) oraz z jakiego kompilatora się korzysta,
dokładnie wiadomo, ile bitów zajmie zmienna danego typu oraz jaki zakres liczb może
być przedstawiony. Dzięki temu w Javie unikniemy występujących w C i C++ problemów
z przenośnością programów związanych z różną reprezentacją typów liczbowych7.
7
Uwaga ta dotyczy również pozostałych typów podstawowych.
Rozdział 1.  Podstawy
19
Typy arytmetyczne zmiennopozycyjne
Typy zmiennopozycyjne występują w dwóch odmianach różniących się, podobnie jak
w przypadku typów całkowitoliczbowych, rozmiarem oraz zakresem liczb możliwych
do zaprezentowania. Są to:
 float (pojedynczej precyzji),
 double (podwójnej precyzji).
Zakres liczbowy oraz liczbę bitów niezbędnych do zapisania liczby przy zastosowaniu
danego typu przedstawiono w tabeli 1.2.
Tabela 1.2. Zakresy typów zmiennoprzecinkowych w Javie8
Typ
Liczba bitów
Zakres
float
32
Od –3,4e38 do 3,4e38
double
64
Od –1,8e308 do 1,8e308
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 warunkó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.
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 komentarz 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.");
}
}
Umiejscowienie komentarza blokowego jest praktycznie dowolne. Co ciekawe, może
on nawet znaleźć się w środku instrukcji (pod warunkiem że nie zostanie przedzielone
żadne słowo); zobrazowano to na listingu 1.4. Okazuje się to możliwe, ponieważ —
zgodnie z tym, co zostało napisane wcześniej — wszystko, co znajduje się między
znakami /* i */ (oraz same te znaki), jest ignorowane przez kompilator. Oczywiście
należy potraktować to raczej jako ciekawostkę niż praktyczne zastosowanie. Komentarze
mają przecież przyczyniać się do zwiększenia czytelności kodu, a nie jego zaciemniania.
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
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.
Komentarz liniowy (wierszowy)
Komentarz liniowy zaczyna się od znaków // i obowiązuje do końca danej linii programu. To znaczy, że wszystko, co występuje po tych dwóch znakach aż do końca
bieżącej linii, jest ignorowane przez kompilator. Przykład wykorzystania takiego komentarza przedstawiono na listingu 1.6.
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 przydatne. Komentarz liniowy może natomiast znaleźć się w środku komentarza blokowego:
/*
//ta konstrukcja jest poprawna
*/
22
Java. Praktyczny kurs
Ćwiczenia do samodzielnego wykonania
Ćwiczenie 3.1.
Na początku programu z listingu 1.1 dołącz komentarz blokowy opisujący jego działanie.
Wykonaj kompilację kodu.
Rozdział 2.
Instrukcje języka
Java, tak jak każdy język programowania, jest wyposażona w szereg podstawowych instrukcji, 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).
Następnie omówimy występujące w Javie instrukcje, które sterują wykonywaniem programu. Będą to instrukcje warunkowe pozwalające wykonywać różny kod w zależności
od tego, czy zadany warunek jest prawdziwy, czy fałszywy, oraz pętle pozwalające na
łatwe wykonywanie powtarzających się instrukcji. Ostatnie dwie lekcje rozdziału 2.
zostaną poświęcone tablicom, i to zarówno jedno-, jak i wielowymiarowym. Osoby,
które znają dobrze języki C lub C++, mogą jedynie przejrzeć ten rozdział, gdyż większość
podstawowych instrukcji sterujących w Javie została zaczerpnięta właśnie z tych języków.
Bliżej powinny zapoznać się tylko z materiałem lekcji 5. omawiającej wyprowadzanie
danych na ekran oraz lekcji dotyczących tablic.
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, natomiast 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.
24
Java. Praktyczny kurs
Lekcja 4. Deklaracje i przypisania
Lekcja 4. jest poświęcona deklaracjom zmiennych oraz przypisywaniu danych zadeklarowanym zmiennym. Znajdują się w niej informacje o tym, jak tworzy się zmienne
i jaki ma to związek z typami danych omówionymi w lekcji 2., jak zadeklarować wiele
zmiennych w jednej instrukcji oraz jak spowodować, aby przechowywały one dane, a także jakie zasady obowiązują w ich nazewnictwie. Na zakończenie słowo o deklaracjach
typów odnośnikowych — więcej o nich dopiero w rozdziale 3.
Proste deklaracje
Każda zmienna, zanim zostanie wykorzystana w kodzie programu, musi zostać zadeklarowana. Deklaracja polega na podaniu typu oraz nazwy zmiennej. Ogólnie taka konstrukcja wygląda następująco:
typ_zmiennej nazwa_zmiennej;
Należy zwrócić uwagę na średnik kończący deklarację. Jest on niezbędny, informuje
bowiem kompilator o zakończeniu instrukcji programu (a deklaracja zmiennej jest instrukcją).
Jeśli w programie zechcemy przechowywać liczby całkowite, zadeklarujemy zmienną
typu int. To, jak taka deklaracja będzie wyglądała w strukturze programu, widać na
listingu 2.1.
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 pozwala 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;
Pierwsze przypisanie wartości zmiennej nazywamy inicjacją lub inicjalizacją1 zmiennej
(listing 2.2). A ponieważ przypisanie wartości zmiennej jest instrukcją programu, nie
można również zapomnieć o kończącym je znaku średnika.
1
Z formalnego punktu widzenia prawidłowym terminem jest inicjacja. Wynika to jednak wyłącznie z tego, że
termin ten został zapożyczony do języka polskiego znacząco wcześniej niż kojarzona chyba wyłącznie
z informatyką inicjalizacja (kalka ang. initialization). Co więcej, np. w języku angielskim funkcjonują
oba terminy (initiation oraz initialization) i mają nieco inne znaczenia. Nie wnikając jednak w niuanse
językowe, można powiedzieć, że w przedstawionym znaczeniu oba te terminy mogą być (i są) używane
zamiennie.
Rozdział 2.  Instrukcje języka
25
Listing 2.2.
class Main {
public static void main(String args[]) {
int liczba;
liczba = 100;
}
}
Inicjalizacja zmiennej może odbywać się w dowolnym miejscu programu po deklaracji
(jak na listingu 2.2), ale może być również równoczesna z deklaracją. W tym drugim
przypadku jednocześnie deklaruje się i inicjuje zmienną, co schematycznie wygląda
następująco:
typ_zmiennej nazwa_zmiennej = wartość_zmiennej;
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;
Deklaracje wielu zmiennych
Zmienne, inaczej niż w Pascalu, ale podobnie jak w C i C++, można deklarować w momencie, kiedy są potrzebne w kodzie programu (w Pascalu zmienne wykorzystywane
w procedurze lub funkcji muszą być zadeklarowane na ich początku). W jednej linii
da się również zadeklarować kilka zmiennych, ale uwaga: o ile są one tego samego
typu. Struktura takiej deklaracji wygląda następująco:
typ_zmiennej nazwa1, nazwa2, nazwa3;
W praktyce mogłoby to wyglądać tak:
int pierwszaLiczba, drugaLiczba, trzeciaLiczba;
Przy takiej deklaracji można również część zmiennych od razu zainicjować:
int pierwszaLiczba = 100, drugaLiczba, trzeciaLiczba= 200;
W tym ostatnim przypadku powstały trzy zmienne o nazwach: pierwszaLiczba, druga
Liczba oraz trzeciaLiczba. Zmiennej pierwszaLiczba została przypisana wartość 100,
zmiennej trzeciaLiczba wartość 200, natomiast drugaLiczba pozostała niezainicjowana.
Na listingu 2.3 zaprezentowany jest kod wykorzystujący różne sposoby deklaracji i inicjacji
zmiennych.
Listing 2.3.
class Main {
public static void main(String args[]) {
int liczba1;
byte liczba2, liczba3 = 100;
liczba1 = 10000;
liczba2 = 25;
}
}
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 zainicjowana 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ę bezwzględnie stosować, gdyż umieszczenie innych znaków w nazwie (np. dwukropka, wykrzyknika 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 łaciń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.
Nazwy powinny odzwierciedlać funkcje, które zmienna pełni w programie. Jeśli ma
ona określać szerokość ekranu, najlepiej nazwać ją po prostu szerokoscEkranu (szero
kośćEkranu) czy też screenWidth. Bardzo poprawia to czytelność kodu oraz ułatwia jego
późniejszą analizę. Przyjmuje się też, że nazwa zmiennej rozpoczyna się od małej litery,
natomiast poszczególne słowa wchodzące w skład tej nazwy rozpoczynają się wielkimi literami5. Dokładnie tak jak w zaprezentowanych w tym akapicie przykładach.
Zmienne typów odnośnikowych
Zmienne typów odnośnikowych, inaczej obiektowych lub referencyjnych (ang. reference types, object types), deklaruje się w sposób bardzo podobny do zmiennych zaprezentowanych już typów (tak zwanych typów prostych). Występuje tu jednak bardzo
ważna różnica. Otóż pisząc:
typ_zmiennej nazwa_zmiennej;
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.
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 odniesienie, inaczej referencja (ang. reference), któremu domyślnie zostanie przypisana
wartość pusta, nazywana w Javie null (w Pascalu stosowane jest słowo nil). Zmiennej referencyjnej po deklaracji należy przypisać odniesienie do utworzonego oddzielną 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ładniejsze wyjaśnienie znajdzie się w rozdziale 3. opisującym podstawy programowania
obiektowego.
Ćwiczenia do samodzielnego wykonania
Ćwiczenie 4.1.
Zadeklaruj i jednocześnie zainicjuj dwie zmienne typu short. Nazwy zmiennych i przypisywane wartości możesz wybrać dowolnie. Pamiętaj o zasadach nazewnictwa zmiennych
oraz zakresie wartości, jakie mogą być reprezentowane przez typ short.
Lekcja 5. Wyprowadzanie danych na ekran
Aby zobaczyć wyniki działania programu, można wyświetlić je na ekranie. W lekcji 5.
zostanie pokazane, w jaki sposób wykonać to zadanie, jak wyświetlić zwykły napis oraz
wartości wybranych zmiennych. Ponadto okaże się, co to są znaki specjalne i co należy
zrobić, aby one również mogły pojawić się na ekranie. Zajmiemy się także problemem
wyświetlania polskich znaków diakrytycznych na konsoli w środowisku Windows, wymaga to bowiem wykonania kilku dodatkowych czynności niezwiązanych bezpośrednio
z samym językiem programowania.
Wyświetlanie wartości zmiennych
W lekcji 4. zostały przedstawione sposoby deklarowania zmiennych oraz przypisywania
im różnych wartości. W jaki sposób przekonać się jednak, że dane przypisanie rzeczywiście odniosło skutek? Jak zobaczyć efekty działania programu? Najlepiej wyświetlić je
na ekranie. Sposób wyświetlenia określonego napisu został omówiony już w lekcji 1.,
była to instrukcja: System.out.println("napis"); (por. listing 1.1, rysunek 1.8). Ta
instrukcja będzie wykorzystywana w dalszych przykładach.
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.
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 zaprezentowano 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 +
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 nazwach 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 linii. 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).
Polskie znaki na konsoli Windows
Jeśli środowiskiem, z którego korzystamy, jest system Windows, przy próbie uzyskania na konsoli polskich znaków zapewne spotka nas niemiła niespodzianka. Można
się o tym przekonać, wykonując prosty test: wystarczy skompilować i uruchomić testowy program przedstawiony na listingu 2.7. Efekt jego wykonania prawdopodobnie
będzie podobny do widocznego na rysunku 2.4 (zależy od wersji i konfiguracji systemu wraz z JDK i JRE oraz sposobu kompilacji).
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: ĄĆĘŁŃÓŚŹŻ");
}
}
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 strona 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 wyprowadzanie 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 niestandardowego sposobu wyprowadzania znaków na ekran (za pomocą dodatkowych obiektów). Ponieważ wymaga to wykonania szeregu instrukcji, które byłyby w tej chwili niezrozumiał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 realizacji, 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 systemowego 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 korzystał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 kodowaniu. Dla Windowsa kompilator przyjmuje standardowo, że ma do czynienia z kodowaniem 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.
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
W takiej sytuacji najlepiej wskazać kompilatorowi, z jakim standardem ma do czynienia, stosując opcję –encoding. Kompilację wykonuje się wtedy za pomocą polecenia
w postaci:
javac –encoding kodowanie nazwa_pliku.java
np.:
javac –encoding utf8 Main.java
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 cudzysł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 poprzednim 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.
Tabela 2.1. Wybrane sekwencje znaków specjalnych
Sekwencja
Znaczenie
\b
Cofnięcie o jeden znak (ang. backspace)
\t
Znak tabulacji
\n
Nowa linia (ang. new line)
\r
Powrót karetki (przesunięcie na początek wiersza, ang. carriage return)
\"
Znak cudzysłowu
\'
Znak apostrofu
\\
Lewy ukośnik (ang. backslash)
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.
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 dwóch wierszy tekstu, z których pierwszy będzie zawierał
tekst napis1, a drugi napis2, natomiast wykonanie instrukcji w postaci:
System.out.print("napis1");
System.out.print("napis2");
spowoduje wyświetlenie na ekranie tylko jednego wiersza, który będzie zawierał połączony tekst w postaci: napis1napis2.
Ćwiczenia do samodzielnego wykonania
Ćwiczenie 5.1.
Zadeklaruj dwie zmienne typu double. Przypisz im dwie różne wartości zmiennoprzecinkowe, 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 napis widoczny na rysunku 2.8. Pamiętaj o wykorzystaniu sekwencji znaków specjalnych.
Rysunek 2.8.
Efekt wykonania
ćwiczenia 5.2
Lekcja 6. Operacje na zmiennych
Na zmiennych typów prostych (czyli tych poznanych w lekcji 2., z wyjątkiem typów obiektowych) można wykonywać różnorodne operacje, na przykład dodawanie, odejmowanie
itp. Operacji tych dokonuje się przy użyciu operatorów, np. operacji dodawania — za pomocą operatora plus zapisywanego jako +, a odejmowania — za pomocą operatora minus
zapisywanego jako -. W tej lekcji zostanie pokazane, w jaki sposób są wykonywane
34
Java. Praktyczny kurs
operacje na zmiennych i jakie rządzą nimi prawa. Przedstawione zostaną operatory arytmetyczne, 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 arytmetycznych, czyli znanego wszystkim dobrze mnożenia, dodawania itp. W tej grupie występują jednak również mniej znane operatory, takie jak operator inkrementacji i dekrementacji. Wszystkie one są zebrane w tabeli 2.2.
Tabela 2.2. Operatory arytmetyczne w Javie
Operator
Wykonywane działanie
*
Mnożenie
/
Dzielenie
+
Dodawanie
-
Odejmowanie
%
Dzielenie modulo (reszta z dzielenia)
++
Inkrementacja (zwiększanie)
--
Dekrementacja (zmniejszanie)
Podstawowe działania
W praktyce korzystanie z większości z tych operatorów sprowadza się do wykonywania 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);
}
}
Rozdział 2.  Instrukcje języka
35
Linie od 1. do 4. to deklaracje zmiennych a, b i c, przypisanie zmiennej a wartości 10,
zmiennej b wartości 20 oraz wyświetlenie tych wartości na ekranie. W linii 5. do zmiennej c
trafia wynik odejmowania b – a, czyli wartość 10 (20 – 10 = 10). W linii 7. zmienna c
otrzymuje wartość wynikającą z działania a / 2, czyli 5 (10 / 2 = 5). Podobnie jest
w linii 9. i 11., gdzie wykonywane są działania mnożenia (c = a * b) oraz dodawania
(c = a + b). W liniach 6., 8., 10. i 12. do wyświetlania wyników poszczególnych
działań używana jest dobrze nam już znana instrukcja System.out.println. Efekt
działania całego programu jest widoczny na rysunku 2.9.
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 wspomniane 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 zmniejszania (--), 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++.
Obie te postacie powodują zwiększenie wartości zapisanej w zmiennej liczba o 1, ale
w przypadku formy przedrostkowej (++x) odbywa się to przed wykorzystaniem zmiennej,
a w przypadku formy przyrostkowej (x++) dopiero po jej wykorzystaniu. Osobom początkującym może się to wydawać zupełnie niezrozumiałe, ale wszelkie wątpliwości
powinien rozwiać praktyczny przykład. Spójrzmy na listing 2.10. Jakie będą wyniki
działania takiego programu?
Listing 2.10.
class Main {
public static void main(String args[]) {
/*1*/ int x = 3, y;
/*2*/ System.out.println (x++);
36
Java. Praktyczny kurs
/*3*/
/*4*/
/*5*/
/*6*/
/*7*/
/*8*/
/*9*/
}
}
System.out.println
System.out.println
y = x++;
System.out.println
y = ++x;
System.out.println
System.out.println
(++x);
(x);
(y);
(y);
(++y);
Wynikiem jego działania będzie ciąg liczb 3, 5, 5, 5, 7, 8 (rysunek 2.10). Dlaczego?
Otóż w linii 1. deklarowane są zmienne x i y, a zmiennej x przypisywana jest wartość 3.
W linii 2. zastosowano formę przyrostkową operatora ++, zatem najpierw następuje wyświetlenie wartości zmiennej x (x = 3) na ekranie, a dopiero potem zwiększenie jej
wartości o 1 (x = 4). W linii 3. postępowanie jest dokładnie odwrotnie, to znaczy przez
zastosowanie formy przedrostkowej najpierw wartość zmiennej x zwiększa się o 1 (x = 5),
a dopiero potem jest ona wyświetlana na ekranie. W linii 4. jedyną operacją jest ponowne
wyświetlenie wartości x (x = 5).
Rysunek 2.10.
Efekt wykonania
programu ilustrującego
działanie operatora ++
W linii 5. najpierw aktualna wartość x (x = 5) trafia do zmiennej y (y = 5) i dopiero potem
wartość x jest zwiększana o 1 (x = 6). Linia 6. to wyświetlenie wartości y. W linii 7.
najpierw wartość x zwiększa się o 1 (x = 7), a następnie jest przypisywana zmiennej y
(y = 7). W ostatniej linii, 9., najpierw wartość y jest zwiększana o 1 (y = 8), a dopiero po
tym wyświetlana na ekranie. Wynik działania programu jest widoczny na rysunku 2.10.
Operator dekrementacji (--) działa analogicznie do ++, z tą różnicą, że zmniejsza
wartość zmiennej o 1. Zmodyfikujmy zatem program z listingu 2.10 w taki sposób,
aby wszystkie wystąpienia ++ zastąpić operatorem --. Otrzymamy wtedy kod widoczny
na listingu 2.11. Tym razem wynikiem będzie ciąg liczb: 3, 1, 1, 1, -1, -2 (rysunek 2.11).
Jak zatem działa ten program?
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);
Rozdział 2.  Instrukcje języka
37
Rysunek 2.11.
Ilustracja działania
operatora dekrementacji
/*5*/
/*6*/
/*7*/
/*8*/
/*9*/
}
}
y = x--;
System.out.println (y);
y = --x;
System.out.println (y);
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 zastosowanie 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).
W linii 5. najpierw aktualna wartość x (x = 1) trafia do zmiennej y (y = 1) i dopiero
potem wartość x jest zmniejszana o jeden (x = 0). Linia 6. to wyświetlenie wartości y
(y = 1). W 7. linii najpierw wartość x zmniejsza się o 1 (x = –1), a następnie ta wartość trafia do y (y = –1). W linii 8. wyświetlana jest wartość y (y = –1). W ostatniej
linii, 9., najpierw następuje zmniejszenie wartości y o 1 (y = –2), a dopiero potem wyświetlenie tej wartości na ekranie.
Kiedy napotkamy problemy…
Zrozumienie sposobu działania operatorów arytmetycznych nikomu z pewnością nie
przysporzyło większych problemów. Jednak i tutaj czekają pewne pułapki. Wróćmy na
chwilę do listingu 2.9. Wykonywane było tam m.in. działanie c = a / 2;. Zmienna a
miała wartość 10, zmiennej c został zatem przypisany wynik działania 10 / 2, czyli 5.
To nie budzi żadnych wątpliwości.
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 widoczny na listingu 2.12.
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);
}
}
Zmienne a i b są typu int i mogą przechowywać liczby całkowite. Zmiennej a przypisano wartość 9, zmiennej b wynik działania a / 2. Z matematyki wiadomo, że wynikiem
działania 9/2 jest 4,5. Nie jest to zatem liczba całkowita. Co się stanie w programie?
Otóż wynik zostanie zaokrąglony w dół do najbliższej liczby całkowitej (dokładniej:
odrzucona zostanie część ułamkowa). Zatem zmienna b otrzyma wartość 4. Jest to
widoczne na rysunku 2.12.
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ąpienia 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 programu, 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 programowania 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.
Rozdział 2.  Instrukcje języka
39
Nie oznacza to jednak, że można bezpośrednio przypisać wartość zmiennoprzecinkową
zmiennej typu całkowitoliczbowego. Kompilator zaprotestuje wtedy z całą stanowczością. Aby się o tym przekonać, wystarczy spróbować wykonać kompilację kodu z listingu 2.13.
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 razem 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
Podobna sytuacja wystąpi przy próbie przekroczenia dopuszczalnego zakresu wartości, który może być reprezentowany przez określony typ danych. Warto to sprawdzić.
Zmienna typu int może przechowywać wartości z zakresu od –2 147 483 648 (to jest
–231) do 2 147 483 647 (to jest 231–1). Co się zatem stanie, jeśli zmiennej typu int spróbujemy przypisać wartość przekraczającą 2 147 483 647 choćby o 1? Można się spodziewać, że kompilator zgłosi błąd. I tak jest w istocie. Na listingu 2.14 widoczny jest
kod realizujący taką instrukcję. Próba kompilacji da efekt widoczny na rysunku 2.14.
Błąd tego typu zostanie od razu zasygnalizowany — nie trzeba się takiej pomyłki
obawiać. Da się ją naprawić praktycznie od ręki, tym bardziej że kompilator wskazuje
konkretne miejsce jej wystąpienia.
Listing 2.14.
class Main {
public static void main(String args[]) {
int a = 2147483648;
System.out.println("Zmienna a ma wartość = " + a);
}
}
40
Java. Praktyczny kurs
Rysunek 2.14. Próba przypisania do zmiennej liczby przekraczającej dopuszczalny zakres wartości
Niestety najczęściej przypisanie wartości przekraczającej zakres danego typu odbywa
się już w trakcie działania programu. Jest to sytuacja podobna do przykładu z automatyczną konwersją liczby zmiennoprzecinkowej, tak aby mogła zostać przypisana
zmiennej typu int (listing 2.12). Jeśli przypisanie wartości zmiennej jest wynikiem obliczeń wykonanych w trakcie działania programu, kompilator nie będzie w stanie ostrzec
programisty, że przekracza dopuszczalny zakres wartości. Taka sytuacja została zobrazowana na listingu 2.15.
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
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.
Rysunek 2.16. Przekroczenie zakresu wartości dla typu int
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 podstawowe 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). Kolejne liczby są budowane z tych dwóch cyfr dokładnie tak samo jak w systemie dziesiętnym; 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.
Operatory bitowe pozwalają na wykonywanie operacji właśnie na poszczególnych
bitach liczb. Są to znane nam ze szkoły operacje AND (iloczyn), OR (suma), NOT (negacja)
oraz XOR (alternatywa wykluczająca) oraz mniej może znane operacje przesunięć bitów.
Zebrano je w tabeli 2.4.
42
Java. Praktyczny kurs
Tabela 2.3. Reprezentacja liczb w różnych systemach liczbowych
System dwójkowy
System ósemkowy
System dziesiętny
System szesnastkowy
0
0
0
0
1
1
1
1
10
2
2
2
11
3
3
3
100
4
4
4
101
5
5
5
110
6
6
6
111
7
7
7
1000
10
8
8
1001
11
9
9
1010
12
10
A
1011
13
11
B
1100
14
12
C
1101
15
13
D
1110
16
14
E
1111
17
15
F
Tabela 2.4. Operatory bitowe w Javie
Operator
Symbol
AND
&
OR
|
NOT
~
XOR
^
Przesunięcie bitowe w prawo
>>
Przesunięcie bitowe w lewo
<<
Przesunięcie bitowe w prawo z wypełnieniem zerami
>>>
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 zatem 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 tabeli 2.5.
Rozdział 2.  Instrukcje języka
43
Tabela 2.5. Operatory logiczne w Javie
Operator
Symbol
AND
&&
OR
||
NOT
!
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.
Tabela 2.6. Iloczyn logiczny
Argument 1
Argument 2
Wynik
true
true
true
true
false
false
false
true
false
false
false
false
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.
Tabela 2.7. Suma logiczna
Argument 1
Argument 2
Wynik
true
true
true
true
false
true
false
true
true
false
false
false
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.
Tabela 2.8. Negacja logiczna
Argument
Wynik
true
false
false
true
44
Java. Praktyczny kurs
Operatory przypisania
Operacje przypisania są dwuargumentowe i powodują przypisanie argumentu prawostronnego 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ładowo istnieje operator +=, który oznacza: przypisz argumentowi z lewej strony wartość 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
tłumaczy się po prostu jako:
a = a + b
Zatem wykonanie kodu widocznego na listingu 2.16 spowoduje przypisanie zmiennej
a wartości 15, następnie dodanie do niej wartości 10 oraz wyświetlenie ostatecznego
stanu zmiennej (a = 25) na ekranie.
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). Schematycznie można przedstawić ich znaczenie następująco:
arg1 op= arg2
co oznacza działanie:
arg1 = arg1 op arg2
czyli a += b oznacza a = a + b, a = b oznacza a = a  b, a %= b oznacza a = a % b itd.
Operatory porównywania (relacyjne)
Operatory porównywania służą oczywiście do porównywania argumentów. Wynikiem ich
działania jest wartość logiczna true lub false, czyli prawda lub fałsz. Operatory te są
zebrane w tabeli 2.10. Wynikiem operacji argument1 == argument2 będzie true, jeżeli
argumenty są sobie równe, oraz false, jeżeli są różne. Czyli 4 == 5 ma wartość false
(fałsz), a 2 == 2 ma wartość true (prawda). Podobnie 2 < 3 ma wartość true (2 jest
mniejsze od 3), ale 4 < 1 ma wartość false (4 jest większe, a nie mniejsze od 1).
Rozdział 2.  Instrukcje języka
45
Tabela 2.9. Operatory przypisania i ich znaczenie w Javie
Argument 1
Operator
Argument 2
Znaczenie
x
=
y
x=y
x
+=
y
x=x+y
x
-=
y
x=x–y
x
*=
y
x=xy
x
/=
y
x=x/y
x
%=
y
x=x%y
x
<<=
y
x = x << y
x
>>=
y
x = x >> y
x
>>>=
y
x = x >>> y
x
&=
y
x=x&y
x
|=
y
x=x|y
x
^=
y
x=x^y
Tabela 2.10. Operatory porównywania w Javie
Operator
Opis
==
Wynikiem jest true, jeśli argumenty są sobie równe.
!=
Wynikiem jest true, jeśli argumenty są różne.
>
Wynikiem jest true, jeśli argument prawostronny jest mniejszy od lewostronnego.
<
Wynikiem jest true, jeśli argument prawostronny jest większy od lewostronnego.
>=
Wynikiem jest true, jeśli argument prawostronny jest mniejszy od lewostronnego
lub jest mu równy.
<=
Wynikiem jest true, jeśli argument prawostronny jest większy od lewostronnego
lub jest mu równy.
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 przedstawione 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.
46
Java. Praktyczny kurs
Tabela 2.11. Priorytety operatorów w Javie
Grupa operatorów
Symbole
Inkrementacja przyrostkowa
++, --
Inkrementacja przedrostkowa, negacja
++, --, ~, !
Mnożenie, dzielenie
*, /, %
Przesunięcia bitowe
<<, >>, >>>
Porównywania 1
<, >, <=, >=
Porównywania 2
==, !=
Bitowe AND
&
Bitowe XOR
^
Bitowe OR
|
Logiczne AND
&&
Logiczne OR
||
Warunkowe
?
Przypisania
=, +=, -=, *=, /=, %=, >>=, <<=, >>>=, &=, ^=, |=
Ćwiczenia do samodzielnego wykonania
Ćwiczenie 6.1.
Zadeklaruj trzy zmienne typu int: a, b i c. Przypisz im dowolne wartości całkowite. Wykonaj 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ć.
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);
Wynik powyższej operacji wyświetl na ekranie. Spróbuj przewidzieć, jaka wartość
zostanie wyświetlona, a następnie uruchom program i sprawdź swoje przypuszczenia.
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
}
Zatem po słowie kluczowym if w nawiasie okrągłym umieszcza się warunek do sprawdzenia, a za nim w nawiasie klamrowym blok instrukcji do wykonania, kiedy warunek
jest prawdziwy. Dalej następuje słowo kluczowe else, a za nim, również w nawiasie
klamrowym10, blok instrukcji, które zostaną wykonane, kiedy warunek będzie fałszywy.
Blok else jest opcjonalny, zatem prawidłowa jest również konstrukcja:
if (warunek){
//instrukcje do wykonania, kiedy warunek jest prawdziwy
}
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.
48
Java. Praktyczny kurs
System.out.println("Zmienna liczba nie jest większa od zera.");
}
}
}
Na początku deklarujemy zmienną liczba typu int i przypisujemy jej wartość 15. Następnie za pomocą instrukcji if sprawdzamy, czy jest ona większa od 0. Wykorzystujemy 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.
Ponieważ w nawiasach klamrowych występujących po if i po else mogą znaleźć się
dowolne instrukcje, można tam również umieścić kolejne instrukcje if…else. Innymi
słowy, instrukcje te można zagnieżdżać. Schematycznie wygląda to następująco:
if (warunek1) {
if(warunek2) {
}
else {
}
}
else {
if (warunek3) {
}
else {
}
}
Spróbujmy wykorzystać taką konstrukcję do wykonania bardziej skomplikowanego
przykładu. Napiszemy program rozwiązujący klasyczne równanie kwadratowe. Jak
wiadomo ze szkoły, ma ono postać: A  x 2  B  x  C  0 , gdzie A, B i C to parametry
równania. Równanie ma rozwiązanie w zbiorze liczb rzeczywistych, jeśli parametr
 (delta) równy B 2  4  A  C jest większy lub równy 0. Jeśli  jest równa 0, mamy
B
jedno rozwiązanie równe
, jeśli  jest większa od 0, mamy dwa rozwiązania:
2 A
B  
B  
i x2 
. Taka liczba warunków sprawia, że to zadanie świetnie
x1 
2 A
2 A
nadaje się do przećwiczenia działania instrukcji if…else. Jedyną niedogodnością programu będzie to, że parametry A, B i C będą musiały być wprowadzone bezpośrednio
w kodzie programu, ponieważ nie zostały jeszcze przedstawione sposoby na wczytywanie danych z klawiatury (będzie to omówione dopiero w rozdziale 6.). Cały program
jest zaprezentowany na listingu 2.18.
Listing 2.18.
class Main {
public static void main (String args[]) {
//deklaracja zmiennych
int A = 1, B = 1, C = -2;
//wyświetlenie parametrów równania
Rozdział 2.  Instrukcje języka
49
System.out.println ("Parametry równania:\n");
System.out.println ("A: " + A + " B: " + B + " C: " + C + "\n");
//sprawdzenie, czy jest to równanie kwadratowe
//A jest równe zero, równanie nie jest kwadratowe
if (A == 0) {
System.out.println ("To nie jest równanie kwadratowe: A = 0!");
}
//A jest różne od zera, równanie jest kwadratowe
else {
//obliczenie delty
double delta = B * B - 4 * A * C;
//jeśli delta mniejsza od zera
if (delta < 0) {
System.out.println ("Delta < 0.");
System.out.println ("To równanie nie ma rozwiązania w zbiorze liczb
rzeczywistych");
}
//jeśli delta większa lub równa zero
else{
//deklaracja zmiennej pomocniczej
double wynik;
//jeśli delta równa zero
if (delta == 0){
//obliczenie wyniku
wynik = -B / (2 * A);
System.out.println ("Rozwiązanie: x = " + wynik);
}
//jeśli delta większa od zera
else{
//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);
}
}
}
}
}
Zaczynamy od zadeklarowania i zainicjowania trzech zmiennych, A, B i C, odzwierciedlających parametry równania. Następnie wyświetlamy je na ekranie. Za pomocą
instrukcji if sprawdzamy, czy zmienna A jest równa 0. Jeśli tak, oznacza to, że równanie nie jest kwadratowe. Jeśli jednak A jest różne od 0, możemy przystąpić do obliczenia delty. Wynik obliczeń przypisujemy zmiennej o nazwie delta. Zmienna ta jest
typu double, to znaczy może przechowywać liczby zmiennoprzecinkowe. To konieczne, jako że delta nie musi być liczbą całkowitą.
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 stosowny 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-
50
Java. Praktyczny kurs
czyć rozwiązanie równania ze wzoru -B / (2 * A). Wynik obliczeń przypisujemy
zmiennej pomocniczej o nazwie wynik i wyświetlamy komunikat z rozwiązaniem na
ekranie.
W przypadku gdy delta jest większa od 0, są dwa pierwiastki (rozwiązania) równania.
Obliczane są w liniach:
wynik = (-B + Math.sqrt(delta)) / (2 * A);
oraz:
wynik = (-B - Math.sqrt(delta)) / (2 * A);
Rezultat obliczeń wyświetlamy rzecz jasna na ekranie, tak jak na rysunku 2.17. Nieznana 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 powyższy, jednak z każdym kolejnym poziomem staje się coraz bardziej nieczytelne.
Nadmiernemu zagnieżdżaniu można zapobiec przez zastosowanie nieco zmodyfikowanej 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 zależności od tego, które są prawdziwe, wykonać instrukcje instrukcje1, instrukcje2,
instrukcje3 lub instrukcje4. Zatem instrukcje1 są wykonywane, kiedy warunek1
Rozdział 2.  Instrukcje języka
51
jest prawdziwy; instrukcje2 — kiedy warunek1 jest fałszywy, a warunek2 prawdziwy; instrukcje3 — kiedy prawdziwy jest warunek3, natomiast fałszywe są warunek1
i warunek2; instrukcje4 — kiedy wszystkie warunki są fałszywe. Jest to zobrazowane
w tabeli 2.12.
Tabela 2.12. Wykonanie instrukcji w zależności od stanu warunków
warunek1
warunek2
warunek3
Wykonaj instrukcje
Prawdziwy
Bez znaczenia
Bez znaczenia
instrukcje1
Fałszywy
Prawdziwy
Bez znaczenia
instrukcje2
Fałszywy
Fałszywy
Prawdziwy
instrukcje3
Fałszywy
Fałszywy
Fałszywy
instrukcje4
Taką konstrukcję można zamienić na identyczną znaczeniowo (semantycznie), ale
prostszą w zapisie instrukcję if…else if w postaci:
if(warunek1){
//instrukcje1
}
else if (warunek2){
instrukcje2
}
else if(warunek3){
instrukcje3
}
else{
instrukcje4
}
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 przeciwnym 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ększa, 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;
//wyświetlenie parametrów równania
System.out.println ("Parametry równania:\n");
System.out.println ("A: " + A + " B: " + B + " C: " + C + "\n");
//sprawdzenie, czy jest to równanie kwadratowe
//A jest równe zero, równanie nie jest kwadratowe
if (A == 0) {
52
Java. Praktyczny kurs
System.out.println ("To nie jest równanie kwadratowe: A = 0!");
}
//A jest różne od zera, równanie jest kwadratowe
else{
//obliczenie delty
double delta = B * B - 4 * A * C;
//jeśli delta mniejsza od zera
if (delta < 0) {
System.out.println ("Delta < 0.");
System.out.println ("To równanie nie ma rozwiązania w zbiorze liczb
rzeczywistych.");
}
//jeśli delta równa zero
else if(delta == 0) {
//obliczenie wyniku
double wynik = - B / (2 * A);
System.out.println ("Rozwiązanie: x = " + wynik);
}
//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);
}
}
}
}
Warto zauważyć, że można również zrezygnować z badania ostatniego warunku.
Obecnie mamy następującą konstrukcję:
if(delta < 0){
//instrukcje
}
else if(delta == 0){
//instrukcje
}
else if(delta > 0){
//instrukcje
}
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 fragmentu w postaci:
if(delta < 0){
//instrukcje
}
else if(delta == 0){
//instrukcje
Rozdział 2.  Instrukcje języka
53
}
else{
//instrukcje
}
Ćwiczenia do samodzielnego wykonania
Ć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.
Lekcja 8. Instrukcja switch i operator warunkowy
W lekcji 7. została przedstawiona instrukcja warunkowa if w kilku różnych postaciach. W praktyce wystarczyłaby ona do obsługi wszelkich zadań programistycznych
związanych ze sprawdzaniem warunków. Okazuje się jednak, że czasami wygodniejsze
są inne konstrukcje warunkowe i właśnie im jest poświęcona lekcja 8. Omówione tu
zostaną dokładnie instrukcja switch, nazywana instrukcją wyboru, oraz tak zwany operator warunkowy. Czytelnicy znający C bądź C++ mogą tę lekcję pominąć, gdyż działanie
tych instrukcji jest im znane, w Javie mają one bowiem taką samą postać jak w wymienionych językach. Z kolei osoby dopiero rozpoczynające naukę czy też znające np.
Pascala powinny wnikliwie zapoznać się z przedstawionym materiałem.
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
}
można przedstawić za pomocą instrukcji switch, która będzie wyglądać następująco:
switch(liczba){
case 1 :
instrukcje1;
break;
case 2 :
instrukcje2;
break;
case 3 :
instrukcje3;
break;
default :
instrukcje4;
}
W rzeczywistości w nawiasie okrągłym po switch nie musi występować nazwa zmiennej,
ale może się tam znaleźć dowolne wyrażenie, którego wynikiem będzie wartość arytmetyczna (czyli wartość typu byte, short, int albo long), lub też, począwszy od Java
7, ciąg znaków (wartość typu String). W postaci ogólnej cała konstrukcja wygląda
zatem następująco:
switch(wyrażenie){
case wartość1 :
instrukcje1;
break;
case wartość2 :
instrukcje2;
break;
case wartość3 :
instrukcje3;
break;
default :
instrukcje4;
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.
Zobaczmy, jak to działa na konkretnym przykładzie, który został zaprezentowany na
listingu 2.20.
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 dynamicznie 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.");
}
56
Java. Praktyczny kurs
}
}
Zatem instrukcja switch najpierw oblicza wartość wyrażenia występującego w nawiasie
okrągłym (jeśli jest to zmienna, w to miejsce jest podstawiana jej wartość), a następnie
próbuje ją dopasować do jednej z wartości występujących po słowach case. Jeśli zgodność zostanie stwierdzona, zostaną wykonane instrukcje występujące w danym bloku
case. Jeżeli nie uda się dopasować wartości wyrażenia do żadnej z wartości występujących po słowach case, zostaje wykonany blok default. Blok default nie jest jednak obligatoryjny i jeśli nie jest w programie potrzebny, można go pominąć.
Szczególną uwagę należy jednak zwrócić na instrukcję break, która przerywa wykonywanie danego bloku case i tym samym powoduje opuszczenie instrukcji switch. Jej przypadkowe pominięcie może doprowadzić do nieoczekiwanych wyników i błędów w programie. 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, prawda? 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ą wykonane 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.
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ęzyku C#11? Chodzi o to, że takie właśnie zachowanie bloku switch dla wprawnego programisty 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 wykorzystać 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.
58
Java. Praktyczny kurs
Najważniejsza jest tu oczywiście linia liczba2 = liczba1 < 0 ? -1 : 1;. Po lewej
stronie operatora przypisania = znajduje się zmienna (liczba2), natomiast po stronie
prawej wyrażenie warunkowe, czyli linia ta oznacza: przypisz zmiennej liczba2
wartość wyrażenia warunkowego. Jaka jest ta wartość? Trzeba przeanalizować samo
wyrażenie: liczba1 < 0 ? -1 : 1. Oznacza ono, zgodnie z tym, co zostało napisane
w poprzednim akapicie: jeżeli wartość zmiennej liczba1 jest mniejsza od 0, przypisz wyrażeniu wartość –1, w przeciwnym razie (zmienna liczba1 większa lub równa 0) przypisz wartość 1. Ponieważ zmiennej liczba1 zostało przypisane wcześniej 10, wartością
całego wyrażenia będzie 1 i ta właśnie wartość zostanie przypisana zmiennej liczba2.
Łatwo zauważyć, że operator warunkowy wykonuje podobne zadanie co instrukcja
if…else. Kod z listingu 2.23 można zatem w prosty sposób przerobić tak, by ten sam
efekt został osiągnięty za pomocą instrukcji warunkowej (ćwiczenie 8.3).
Ćwiczenia do samodzielnego wykonania
Ć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ść począ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 matematyce). 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.
Rozdział 2.  Instrukcje języka
59
Lekcja 9. Pętle
Pętle są konstrukcjami programistycznymi, które pozwalają na wykonywanie powtarzających się czynności. Przykładowo aby wyświetlić na ekranie dziesięć razy dowolny 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 występujące w Javie postacie pętli, czyli pętle for, while oraz do…while, a także wprowadzona w Java 5.0 (1.5) pętla typu foreach. Omówione zostaną także różnice wystę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
}
wyrażenie początkowe jest stosowane do zainicjalizowania zmiennej używanej jako
licznik liczby wykonań pętli, wyrażenie warunkowe określa warunek, jaki musi być
spełniony, aby dokonać kolejnego przejścia w pętli, natomiast wyrażenie modyfikujące
jest zwykle używane do modyfikacji zmiennej będącej licznikiem. Najłatwiej wyjaśnić to wszystko na praktycznym przykładzie. Znajduje się on na listingu 2.24.
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 wartość 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 (iteracje) 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");
}
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
Wyrażenie początkowe to w powyższym przykładzie int i = 0, wyrażenie warunkowe to i < 10, a wyrażenie modyfikujące to i++. Okazuje się, że mamy dużą dowolność
umiejscawiania tych wyrażeń. Na przykład wyrażenie modyfikujące, które najczęściej
jest wykorzystywane do modyfikacji zmiennej iteracyjnej, można umieścić wewnątrz
samej pętli, to znaczy zastosować konstrukcję o następującej schematycznej postaci:
for (wyrażenie początkowe; wyrażenie warunkowe;){
instrukcje do wykonania
wyrażenie modyfikujące
}
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");
Rozdział 2.  Instrukcje języka
61
i++;
}
}
}
Program ten jest funkcjonalnym odpowiednikiem poprzedniego przykładu. Szczególną
uwagę należy zwrócić natomiast na znak średnika, występujący po wyrażeniu warunkowym. Mimo że wyrażenie modyfikujące znalazło się teraz wewnątrz bloku pętli,
ten średnik jest niezbędny. Jeśli go zabraknie, kompilacja z pewnością się nie uda.
Skoro udało nam się usunąć wyrażenie modyfikujące z nawiasu okrągłego pętli, spróbujmy 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ątkowe 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 umiejscowienie ś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++;
}
}
}
Kolejną ciekawą możliwością jest połączenie wyrażenia warunkowego i modyfikującego. Pozostawimy wyrażenie początkowe przed pętlą, natomiast wyrażenie modyfikujące ponownie wprowadzimy do konstrukcji pętli, łącząc je jednak z wyrażeniem
warunkowym. Taka konstrukcja jest przedstawiona na listingu 2.27.
Listing 2.27.
class Main {
public static void main (String args[]) {
int i = 0;
for(; i++ < 10;){
System.out.println("Pętle w Javie");
}
}
}
62
Java. Praktyczny kurs
Istnieje również możliwość przeniesienia wyrażenia warunkowego do wnętrza pętli,
jednak wymaga to zastosowania instrukcji break, która pojawi się w lekcji 10. Warto też
zwrócić uwagę, że przedstawiony wyżej kod nie jest w pełni funkcjonalnym odpowiednikiem pętli z listingów 2.24 – 2.26, choć w pierwszej chwili wyniki działania wydają
się identyczne. Odkrycie odpowiedzi na pytanie, dlaczego tak jest, oraz wprowadzenie
odpowiednich poprawek (tak aby program z listingu 2.24 był dokładnym odpowiednikiem
wcześniejszych) pozostanie jednak ćwiczeniem do samodzielnego wykonania (wskazówka znajduje się w przykładach dotyczących pętli while).
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 odpowiednikami. Ogólna postać pętli while wygląda następująco:
while (wyrażenie warunkowe){
instrukcje;
}
Instrukcje są wykonywane dopóty, dopóki wyrażenie warunkowe jest prawdziwe. Zobaczmy zatem, jak za pomocą pętli while wyświetlić na ekranie dziesięć razy dowolny
napis. To zadanie jest wykonywane przez kod widoczny na listingu 2.28. Pętlę taką
rozumiemy następująco: dopóki i jest mniejsze od 10 (i < 10), wyświetlaj napis na ekranie (System.out.println("Pętle w Javie");), za każdym razem zwiększając i o 1 (i++).
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");
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 listingu 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
Widać wyraźnie, że w pierwszym przypadku wartości zmiennej zmieniają się od 0 do
9, natomiast w drugiej sytuacji od 1 do 10. Nie ma to znaczenia, kiedy wyświetla się
jedynie serię napisów, jednak gdyby wykorzystywać zmienną i do jakichś celów, np.
dostępu do komórek tablicy (podrozdział „Tablice”), ta drobna z pozoru różnica spowodowałaby poważne konsekwencje w działaniu programu. Dobrym ćwiczeniem do samodzielnego wykonania będzie natomiast poprawienie programu z listingu 2.28 tak,
aby działał dokładnie tak samo jak ten z listingu 2.29 (podpunkt „Ćwiczenia do samodzielnego wykonania”).
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);
64
Java. Praktyczny kurs
Konstrukcję tę należy rozumieć następująco: wykonuj instrukcje, dopóki warunek
jest prawdziwy12. Zobaczmy zatem, jak wygląda znane nam już dobrze zadanie wyświetlenia dziesięciu napisów, jeśli do jego realizacji zostanie wykorzystana pętla do…while.
Zobrazowano to w kodzie przedstawionym na listingu 2.30.
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 warunek, a dopiero potem są wykonywane instrukcje. W przypadku pętli do…while jest dokładnie odwrotnie — najpierw są wykonywane instrukcje, a dopiero potem jest sprawdzany 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.
Takiemu zachowaniu można zapobiec, wprowadzając wyrażenie modyfikujące zmienną
i do wnętrza pętli; sama pętla miałaby wtedy postać:
int i = 0;
do{
System.out.println("[i = " + i + "] Pętle w Javie");
i++;
}
while(i < 10);
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.
Ta cecha (czyli wykonywanie instrukcji przed sprawdzeniem warunku) pętli while
jest bardzo ważna, oznacza bowiem, że pętla tego typu jest wykonywana zawsze co najmniej raz, nawet jeśli warunek jest fałszywy. Można się o tym przekonać w bardzo prosty
sposób — wprowadzając fałszywy warunek i obserwując zachowanie programu, np.:
int i = 0;
do{
System.out.println("Pętle w Javie");
i++;
12
Osoby znające język programowania Pascal z pewnością zauważą w tej konstrukcji podobieństwo
do pętli repeat…until.
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 wykonaniu 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.
Pętla typu foreach
Począwszy od Javy w wersji 5.0 (1.5), dostępny jest nowy rodzaj pętli. To pętla nazywana pętlą typu foreach lub rozszerzoną pętlą for (ang. enhanced for). Pozwala
ona na automatyczną iterację po kolekcji obiektów lub też po tablicy13. Jej działanie
zostanie pokazane właśnie w tym drugim przypadku (osoby nieznające pojęcia tablic
powinny najpierw zapoznać się z materiałem zawartym w lekcji 11.). Jeśli bowiem
mamy tablicę tab zawierającą wartości pewnego typu, to do przejrzenia wszystkich jej
elementów możemy użyć konstrukcji o postaci:
for(typ nazwa: tab){
//instrukcje
}
W takim przypadku w kolejnych przebiegach pętli pod nazwa będzie podstawiana wartość kolejnej komórki. Pętla będzie działała tak długo, aż zostaną przejrzane wszystkie 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);
}
}
}
Ćwiczenia do samodzielnego wykonania
Ć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.
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.
Lekcja 10. Instrukcje break i continue
W lekcji 9. zostały omówione cztery rodzaje pętli, czyli konstrukcji programistycznych
pozwalających na łatwe wykonywanie powtarzających się czynności. Były to pętle
for, while, do…while i foreach. Teraz zostaną przedstawione dwie dodatkowe współpracujące z pętlami instrukcje: break i continue. Pierwsza z nich powoduje przerwanie
wykonywania pętli i opuszczenie jej bloku, natomiast druga — przerwanie bieżącej
iteracji i przejście do kolejnej. W lekcji znajdą się informacje, jak stosować te instrukcje w przypadku prostych pętli pojedynczych, a także w przypadku pętli dwu- lub wielokrotnie zagnieżdżonych. Omówiony zostanie również temat etykiet, które dodatkowo zmieniają zachowanie instrukcji break i continue, a tym samym umożliwiają
tworzenie bardziej zaawansowanych konstrukcji pętli.
Instrukcja break
Instrukcja break pojawiła się już w lekcji 8. przy omawianiu instrukcji switch. Znaczenie 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 instrukcji 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 warunkowe znalazło się wewnątrz niej. Kod realizujący takie zadanie został przedstawiony
na listingu 2.32.
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;
}
}
}
}
Ponownie szczególną uwagę należy zwrócić na wyrażenia znajdujące się w nawiasie
okrągłym pętli. Mimo że usunięte zostało wyrażenie warunkowe, umieszczony po nim
średnik musiał pozostać na swoim miejscu, inaczej nie udałaby się kompilacja programu.
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łego pętli. Wyrażenie początkowe przenosi się przed pętlę, a modyfikujące i warunkowe do jej wnętrza. Zostało to przedstawione na listingu 2.33. Tego typu konstrukcja
niegdyś była tylko ciekawostką programistyczną, raczej niestosowaną w praktycznych 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ętrznej, 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.
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 zmiennej 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
W pętli wewnętrznej umieśćmy teraz instrukcję warunkową if(i == 2) break;, tak
aby cała konstrukcja wyglądała następująco:
for(int i = 0; i < 3; i++){
for(int j = 0; j < 3; j++){
if(i == 2) break;
System.out.println(i + " " + j);
}
}
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 zewnę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, podobnie jak instrukcja continue, która zostanie przedstawiona już za chwilę.
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 wewną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ą). Zobaczmy, 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 skonstruowana 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.
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 „Ćwiczenia 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 umieszczona 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
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:
Teoretycznie taką etykietę można umieścić w dowolnym miejscu między instrukcjami, choć aby mogła zostać użyta, wolno wstawić ją jedynie przed instrukcją rozpoczynającą pętlę. Inaczej mówiąc, przykładowy fragment kodu:
int liczba = 0;
etykieta1:
if(liczba == 0){
System.out.println("tekst");
}
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 instrukcją iteracji (pętlą). Widać to na listingu 2.36 — jest to modyfikacja przykładu z zagnież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).
Powtórzmy jeszcze raz: break etykieta powoduje przerwanie wszystkich pętli aż do
miejsca oznaczonego etykietą, czyli wszystkich pętli znajdujących się za etykietą. To
znaczy, że jeżeli będziemy mieli potrójnie zagnieżdżone pętle w postaci:
72
Java. Praktyczny kurs
Rysunek 2.28.
Efekt działania
etykietowanej
instrukcji break
for(int i = 0; i < 3; i++){
etykieta1:
for(int j = 0; j < 3; j++){
for(int k = 0; k < 3; k++){
break etykieta1;
}
}
}
to po wykonaniu break zostanie przerwane działanie dwóch pętli wewnętrznych (gdzie
zmiennymi iteracyjnymi są j oraz k), a sterowanie zostanie przekazane pętli zewnętrznej (gdzie zmienną iteracyjną jest i). Ogólny schemat działania etykietowanej instrukcji break jest widoczny na rysunku 2.29. Widać wyraźnie, że po napotkaniu break następuje wyjście z pętli i przejście do wykonania kolejnej instrukcji, umieszczonej za
pętlą.
Rysunek 2.29.
Schemat działania
etykietowanej
instrukcji break
w przypadku pętli for
Etykietowana instrukcja continue działa nieco inaczej. Również powoduje przerwanie
bieżącej iteracji (przebiegu) pętli i przejście do miejsca oznaczonego etykietą. Potem
jednak następuje ponowne wejście do pętli znajdującej się tuż za etykietą. Zostało to
zilustrowane na rysunku 2.30.
Rysunek 2.30.
Schemat działania
etykietowanej
instrukcji continue
w przypadku pętli for
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
Ćwiczenia do samodzielnego wykonania
Ćwiczenie 10.1.
Napisz program, który wyświetli na ekranie nieparzyste liczby z zakresu 1 – 20. Wykorzystaj pętlę for i instrukcję continue.
Ćwiczenie 10.2.
Napisz program, który wyświetli na ekranie nieparzyste liczby z zakresu 1 – 20. Wykorzystaj 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:
74
Java. Praktyczny kurs
a) bez używania instrukcji continue,
b) z użyciem instrukcji continue,
c) z użyciem instrukcji break po wykryciu podzielności liczby przez 3
(osoby początkujące mogą potraktować tę wersję jako zadanie „z gwiazdką”).
Ć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 pozostał bez zmian, ale nie było potrzeby używania instrukcji continue.
Tablice
Tablica to stosunkowo prosta struktura danych pozwalająca na przechowanie uporządkowanego zbioru elementów danego typu. Obrazowo zostało to przedstawione na rysunku 2.32. Jak widać, struktura ta składa się z ponumerowanych kolejno komórek (numeracja 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
Lekcja 11. Podstawowe operacje na tablicach
Tablice to struktury danych występujące w większości popularnych języków programowania. Nie mogło ich zatem zabraknąć również w Javie. W tej lekcji zostaną przedstawione podstawowe typy tablic jednowymiarowych, sposoby ich deklarowania oraz
używania. Omówiona będzie również bardzo ważna dla tej struktury właściwość
length. Nie pominiemy też kwestii sposobu numeracji komórek każdej tablicy, która,
jak to zostało zaprezentowane na rysunku 2.32, zawsze zaczyna się od 0. Wiedzą o tym
osoby programujące w C i C++, może być to jednak nowością dla programujących
w Pascalu.
Rozdział 2.  Instrukcje języka
75
Proste tablice w Javie
Tablice w Javie są obiektami (więcej o obiektach w rozdziale 3.). Aby móc skorzystać
z tablicy, trzeba najpierw zadeklarować zmienną tablicową, a następnie utworzyć samą tablicę (obiekt tablicy). Schemat deklaracji wygląda następująco:
typ_tablicy nazwa_tablicy[];
lub też (co ma identyczne znaczenie):
typ_tablicy[] nazwa_tablicy;
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 deklaracja 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];
Można jednocześnie zadeklarować i utworzyć tablicę, korzystając z konstrukcji:
typ_tablicy nazwa_tablicy[] = 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 instrukcje. 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]);
}
}
76
Java. Praktyczny kurs
W pierwszym kroku nastąpiła deklaracja zmiennej tablicowej tab i przypisanie jej
nowo utworzonej tablicy typu int o rozmiarze 1 (int tab[] = new int[1]). Oznacza to, że
tablica ta ma tylko jedną komórkę i może przechowywać naraz tylko jedną liczbę całkowitą. W drugim kroku jedynemu elementowi tej tablicy przypisano wartość 10. Zwróćmy
uwagę na sposób odwołania się do tego elementu: tab[0] = 10. Ponieważ w Javie (podobnie jak w C i C++) elementy tablic są numerowane od 0 (por. rysunek 2.32), pierwszy
z nich ma indeks 0! To bardzo ważne: pierwszy element to indeks 0, drugi to indeks 1,
trzeci to indeks 2 itd. Jeśli zatem chcemy odwołać się do pierwszego elementu tablicy
tab, napiszemy tab[0] (indeks żądanego elementu umieszcza się w nawiasie kwadratowym za nazwą tablicy). W trzecim kroku zawartość wskazanego elementu jest po
prostu wyświetlana na ekranie przy użyciu dobrze nam znanej instrukcji System.out.
println.
Sprawdźmy teraz, co się stanie, jeśli się pomylimy i spróbujemy odwołać się do nieistnieją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 został 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 przyniesie rezultat widoczny na rysunku 2.33. Choć wygląda to bardzo groźnie, tak naprawdę 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.
Rysunek 2.33. Próba odwołania się do nieistniejącego elementu tablicy
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 począ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órkach 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 przypisać 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.
78
Java. Praktyczny kurs
Właściwość length
Kiedy rozmiar tablicy jest większy, zamiast stosować przedstawione wyżej przypisanie 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 programista 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
otrzymuje się rozmiar dowolnej tablicy. Należy oczywiście pamiętać o numerowaniu
poszczególnych komórek tablicy od 0. Aby odwołać się do ostatniego elementu tablicy,
należy w związku z tym odwołać się do indeksu o wartości length – 1. W praktyce
program wypełniający prostą tablicę liczb typu short kolejnymi wartościami całkowitymi oraz wyświetlający jej zawartość na ekranie będzie wyglądał tak jak na listingu 2.41.
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]);
}
}
}
Ćwiczenia do samodzielnego wykonania
Ć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.
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.
Lekcja 12. Tablice wielowymiarowe
Tablice jednowymiarowe prezentowane w lekcji 11. to nie jedyny rodzaj tablic, jakie
można tworzyć w Javie. Istnieją również tablice wielowymiarowe, którymi zajmiemy
się właśnie w tej lekcji. Przedstawiony zostanie sposób ich deklarowania i tworzenia
oraz odwoływania się do poszczególnych elementów. Omówione zostaną przy tym
zarówno tablice o regularnym, jak i nieregularnym układzie komórek. Okaże się też, jak
ważna w przypadku tego rodzaju struktur jest znana nam z poprzedniej lekcji właściwość
length.
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. Tablice nie muszą być jednak jednowymiarowe, wymiarów może być więcej, np. dwa. Wtedy 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 przypadku tablic jednowymiarowych. Jeśli schematyczna deklaracja tablicy jednowymiarowej wyglądała:
typ nazwa_tablicy[]
to w przypadku tablicy dwuwymiarowej będzie ona następująca:
typ nazwa_tablicy[][]
Jak widać, dodaje się po prostu kolejny nawias kwadratowy15.
Kiedy mamy już zmienną tablicową, możemy utworzyć i jednocześnie zainicjować
samą tablicę. Tu również konstrukcja jest analogiczna do konstrukcji tablic jednowymiarowych:
typ_tablicy nazwa_tablicy[][] = {{wartość1, wartość2, ... , wartośćN}, {wartość1,
wartość2, ... , wartośćN }}
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 widoczny 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.
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]
Wynik działania programu z listingu 2.42 jest widoczny na rysunku 2.37.
Rysunek 2.37.
Wynik działania
programu
wypełniającego tablicę
dwuwymiarową
Drugi sposób utworzenia tablicy dwuwymiarowej to wykorzystanie operatora new
przedstawionego w lekcji 11. Tym razem będzie on miał następującą postać:
82
Java. Praktyczny kurs
new typ_tablicy[liczba_elementów][liczba_elementów];
Można jednocześnie zadeklarować i utworzyć tablicę, korzystając z konstrukcji:
typ_tablicy nazwa_tablicy[][] =
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];
Jak widać, oba sposoby są analogiczne do metod tworzenia tablic jednowymiarowych.
Oczywiście również nawiasy kwadratowe mogą znaleźć się przed nazwą, a za typem
tablicy:
typ_tablicy[][] nazwa_tablicy
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 zastosować 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]);
}
}
}
}
Za wypełnienie tablicy danymi odpowiadają dwie pierwsze zagnieżdżone pętle for.
Kolejnym komórkom jest przypisywany stan zmiennej licznik. Zmienna ta ma wartość
początkową równą 1 i w każdej iteracji jej wartość jest zwiększana o 1. Stosuje się
w tym celu operator ++ (lekcja 6.). Po wypełnieniu danymi zawartość tablicy jest wyświetlana na ekranie za pomocą dwóch kolejnych zagnieżdżonych pętli for, dokładnie
tak jak w przypadku programu z listingu 2.42.
Rozdział 2.  Instrukcje języka
83
Właściwość length
Zaprezentowany na listingu 2.43 sposób wypełniania tablicy (oraz wyświetlania jej zawartości na ekranie) ma jeden mankament. W przypadku obu pętli zakładamy, że rozmiar 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?
Odpowiedź na pierwsze pytanie brzmi: oczywiście tak, tablica wielowymiarowa również ma właściwość length. Aby sprawnie się nią posługiwać, trzeba jednak dobrze
zrozumieć, czym tak naprawdę jest tablica wielowymiarowa. Można to odkryć po dokładnym przyjrzeniu się jej deklaracji. Da się zauważyć, że jeśli struktura:
int[]
oznacza tablicę liczb typu int, to:
int[][]
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]
określa w rzeczywistości n-elementową tablicę, której elementami są m-elementowe
tablice liczb typu int. Odwołanie:
tablica.length
da więc w wyniku liczbę tablic m-elementowych (czyli n), natomiast wywołanie:
tablica[indeks].length
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]
W tym przypadku zachodzą następujące zależności:
tablica.length = 2;
tablica[0].length = 4;
tablica[1].length = 4.
Budowę tej tablicy zobrazowano na rysunku 2.38.
Aby lepiej zrozumieć te zależności, wykonajmy jeszcze pełny praktyczny przykład.
Zmodyfikujemy kod z listingu 2.43, tak aby pętle zajmujące się zarówno wypełnianiem
tablicy, jak i wyświetlaniem jej zawartości na ekranie nie były zależne od jej rozmiaru
i pracowały, nawet jeśli wielkość tablicy ulegnie modyfikacji. Zadanie to można zrealizować w sposób przedstawiony na listingu 2.44.
84
Java. Praktyczny kurs
Rysunek 2.38. Budowa dwuwymiarowej tablicy o postaci int[2][4]
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ójką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 naprawdę tablice tablic jednowymiarowych (tablica dwuwymiarowa to tablica jednowymiarowa zawierająca szereg tablic jednowymiarowych, tablica trójwymiarowa to tablica
16
Zależy to od kształtu tablicy, który chce się uzyskać.
Rozdział 2.  Instrukcje języka
85
Rysunek 2.39.
Przykłady
nieregularnych tablic
wielowymiarowych
jednowymiarowa zawierająca w sobie tablice dwuwymiarowe itd.). Spróbujmy zatem
stworzyć strukturę widoczną na rysunku 2.39b. Zaczniemy od samej deklaracji — nie
przysporzy ona z pewnością żadnego kłopotu, była prezentowana już kilkakrotnie:
int[][] tab
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 rysunku 2.39b, należy teraz utworzyć 4-elementową tablicę, która będzie mogła przechowywać tablice jednowymiarowe liczb typu int. Trzeba więc użyć operatora new w postaci:
new int[4][]
Deklaracja i jednoczesna inicjalizacja zmiennej tab będzie wyglądać następująco:
int[][] tab = 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] znalazł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]
tab[1]
tab[2]
tab[3]
=
=
=
=
new
new
new
new
int[4];
int[2];
int[1];
int[3];
To wszystko — tablica jest gotowa. Można zacząć wypełniać ją danymi.
Załóżmy, że kolejne komórki mają zawierać liczby od 1 do 10, to znaczy w pierwszym
wierszu tablicy znajdą się liczby 1, 2, 3, 4, w drugim — 5, 6, w trzecim — 7, a w czwartym — 8, 9, 10, tak jak zostało to zobrazowane na rysunku 2.41. Zatem do wypełnienia tablicy można użyć zagnieżdżonej pętli for, analogicznie do przypadku tablicy regularnej w przykładzie z listingu 2.44. Podobnie zagnieżdżonych pętli for, choć w nieco
zmienionej postaci, da się użyć do wyświetlenia zawartości tablicy na ekranie. Pełny kod
programu realizującego postawione zadania jest widoczny na listingu 2.45.
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++;
}
}
Rozdział 2.  Instrukcje języka
87
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("");
}
}
}
W pierwszej części kodu powstała dwuwymiarowa tablica o strukturze takiej jak na
rysunkach 2.39b i 2.41 (wszystkie użyte konstrukcje zostały wyjaśnione w poprzednich akapitach). Jest ona wypełniana danymi tak, aby uzyskać w poszczególnych komórkach wartości widoczne na rysunku 2.41. W tym celu użyte zostały zagnieżdżone
pętle for i zmienna licznik. To taki sam sposób jak zastosowany w programie z listingu 2.44. Pętla zewnętrzna, ze zmienną iteracyjną i, odpowiada za przebieg po kolejnych wierszach tablicy, a wewnętrzna, ze zmienną iteracyjną j, za przebieg po kolejnych
komórkach w każdym wierszu.
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 zewnę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 danym 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 problemó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][];
Kolejne wiersze tablicy można utworzyć za pomocą serii instrukcji:
tab[0]
tab[1]
tab[2]
tab[3]
=
=
=
=
new
new
new
new
int[4];
int[3];
int[2];
int[1];
88
Java. Praktyczny kurs
Wypełnienie takiej tablicy danymi (zgodnie z podanymi zasadami) oraz wyświetlenie
tych danych na ekranie będzie się odbywać podobnie jak w przypadku kodu z listingu
2.45. Zatem pełny program będzie wyglądał tak, jak przedstawiono na listingu 2.46.
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]
tab[1]
tab[2]
tab[3]
=
=
=
=
new
new
new
new
int[4];
int[3];
int[2];
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];
}
Zmienna i zmienia się tu w zakresie 0 – 3, zatem tab[i] przyjmuje kolejne wartości
tab[0], tab[1], tab[2], tab[3], natomiast wyrażenie new int[4 – i] wartości: new
int[4], new int[2], new int[3], new int[1]. Tym samym otrzymamy dokładny odpowiednik czterech ręcznie napisanych instrukcji.
Rozdział 2.  Instrukcje języka
89
Ćwiczenia do samodzielnego wykonania
Ć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 wykorzystywane pętle typu while.
Ćwiczenie 12.3.
Utwórz tablicę liczb typu int zaprezentowaną na rysunku 2.43. Wypełnij kolejne komó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 danym 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.
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.).
Rozdział 3.
Programowanie
obiektowe. Część I
Każdy program w Javie składa się z jednej lub wielu klas. W przykładach przedstawionych 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");
}
}
Założyliśmy wtedy, że szkielet kolejnych aplikacji ilustrujących różne struktury języka
programowania ma właśnie tak wyglądać. Teraz nadszedł czas, aby wyjaśnić, dlaczego
właśnie tak. Wszystko stanie się jasne po przeczytaniu tego rozdziału.
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.
Lekcja 13. Klasy, pola i metody
Lekcja 13. rozpoczyna rozdział przedstawiający podstawy programowania obiektowego
w Javie. Podstawowe pojęcia z tego zakresu pojawiały się już w prezentowanym wcześniej materiale, teraz zostaną jednak wyjaśnione dokładniej na praktycznych przykładach.
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ą obiektową (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 nieobeznanych z programowaniem obiektowym to wszystko może brzmieć zawile, zobaczmy
od razu, jak to wygląda w praktyce.
Załóżmy, że pisany przez nas program wymaga przechowywania danych odnośnie do
punktów na płaszczyźnie. Każdy taki punkt jest charakteryzowany przez dwie wartości:
współrzędną x oraz współrzędną y. Stwórzmy więc klasę opisującą obiekty tego typu.
Schematyczny szkielet takiej klasy wygląda następująco:
class nazwa_klasy {
//treść klasy
}
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ładowymi 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 zmiennych (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, szczegó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.
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;
Ponieważ typem zmiennej jest nazwa klasy, deklaracja ta przyjmie postać:
Punkt przykladowyPunkt;
W ten sposób powstała zmienna odnośnikowa (referencyjna, obiektowa), która domyślnie jest pusta, tzn. nie zawiera żadnych danych. Dokładniej rzecz ujmując, po
deklaracji zmienna taka zawiera wartość specjalną null, która określa, że zmienna nie
zawiera odniesienia do żadnego obiektu. Trzeba więc samodzielnie utworzyć obiekt klasy
Punkt i powiązać go z tą zmienną4. Ta pierwsza czynność wykonywana jest za pomocą
operatora new w postaci:
new nazwa_klasy();
Zatem cała konstrukcja schematycznie będzie wyglądać następująco:
nazwa_klasy nazwa_zmiennej = new nazwa_klasy():
w przypadku klasy Punkt:
Punkt przykladowyPunkt = new Punkt();
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ównie 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.
94
Java. Praktyczny kurs
Należy sobie uzmysłowić, że po wykonaniu tych instrukcji w pamięci powstają dwie
różne struktury. Pierwszą z nich jest powstała na tak zwanym stosie (ang. stack) zmienna
referencyjna przykladowyPunkt, drugą jest powstały na tak zwanej stercie (ang. heap)
obiekt klasy (typu) Punkt. Zmienna przykladowyPunkt zawiera odniesienie do przypisanego jej obiektu klasy Punkt i tylko poprzez nią można się do tego obiektu odwoływać.
Zostało to schematycznie zobrazowane na rysunku 3.1.
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;
Napiszmy przykładowy program wykorzystujący klasy Main i Punkt, w którym utworzymy zmienną odnośnikową klasy Punkt oraz obiekt klasy Punkt, przypiszemy polom x i y tego obiektu wartości 100 oraz wyświetlimy te wartości na ekranie. Klasa
Punkt pozostanie bez zmian, tak jak na listingu 3.1, natomiast Main będzie miała postać przedstawioną na listingu 3.2.
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);
}
}
Na początku deklarujemy zmienną klasy Punkt o nazwie punkt (zauważmy, że jest to
możliwe dzięki temu, że w Javie małe i duże litery są rozróżniane) i przypisujemy jej
nowo utworzony obiekt tej klasy. Jest to zatem jednoczesna deklaracja i inicjalizacja.
Rozdział 3.  Programowanie obiektowe. Część I
95
Od tej chwili zmienna punkt wskazuje na obiekt klasy Punkt, można się zatem posługiwać 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 kompilację zarówno pliku Main.java, jak i Punkt.java. Obie klasy muszą natomiast znajdować się w tym samym katalogu.
Metody klas
Klasy oprócz pól przechowujących dane zawierają także metody, które wykonują zapisane 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
}
Po umieszczeniu w ciele klasy ta deklaracja będzie natomiast wyglądała tak:
class nazwa_klasy {
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);
}
96
Java. Praktyczny kurs
Słowo void oznacza, że metoda nie zwraca żadnego wyniku, a brak argumentów pomiędzy znakami nawiasu okrągłego mówi, że metoda ta nie przyjmuje żadnych argumentó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 referencję do obiektu klasy Punkt, prawidłowym wywołaniem metody wyswietlWspolrzedne
będzie:
punkt.wyswietlWspolrzedne();
Ogólnie wywołanie metody wygląda następująco:
nazwa_zmiennej.nazwa_metody(argumenty_metody);
Oczywiście jeśli dana metoda nie ma argumentów, po prostu się je pomija. Wykorzystajmy teraz klasę Main do przetestowania nowej konstrukcji. Zmodyfikujemy program
z listingu 3.2 tak, aby korzystał z metody wyswietlWspolrzedne. Odpowiedni kod jest zaprezentowany 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
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 zobaczyć 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 zapisanej 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 listingu 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;
98
Java. Praktyczny kurs
System.out.println("współrzędna x = " + punkt.pobierzX());
System.out.println("współrzędna y = " + punkt.pobierzY());
}
}
To, że metoda zwraca jakąś wartość, jest równoznaczne z tym, że ta wartość jest podstawiana 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()
(pole y również ma wartość 100).
Wartości domyślne pól
Na listingu 3.6 znajduje się klasa Main, w której tworzony jest obiekt klasy Punkt. Obu
polom tego obiektu zostaje przypisana wartość 100. Pytanie, które warto sobie zadać,
brzmi: jakie wartości miały te pola po utworzeniu obiektu, ale przed przypisaniem do
nich wartości? Otóż w Javie wartość, jaką przyjmuje niezainicjowane pole obiektu,
jest ściśle określona, zatem prawidłowa odpowiedź na to pytanie to: oba pola przyjmą
wartość 0. Można się o tym przekonać, kompilując i uruchamiając program przedstawiony
na listingu 3.7. Wynik jego działania jest widoczny na rysunku 3.4. Wartości domyślne
dla typów danych występujących w Javie zostały z kolei zaprezentowane w tabeli 3.1.
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
Rozdział 3.  Programowanie obiektowe. Część I
99
Tabela 3.1. Domyślne wartości dla poszczególnych typów danych
Typ
Wartość domyślna
byte
0
short
0
int
0
long
0
float
0.0
double
0.0
char
\0
boolean
false
odnośnikowy
null
Ćwiczenia do samodzielnego wykonania
Ć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ą przechowywaną 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 poszczegó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 ćwiczenia 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łaszczyźnie). Dodaj metody podające współrzędne każdego rogu.
100
Java. Praktyczny kurs
Ćwiczenie 13.6.
Do klasy Prostokat z ćwiczenia 13.2 lub 13.3 dopisz metodę sprawdzającą, czy wprowadzone 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).
Lekcja 14. Argumenty i przeciążanie metod
O tym, że metody mogą mieć argumenty, wiadomo z lekcji 13. Czas się dowiedzieć,
jak się nimi posługiwać. Właśnie temu tematowi została poświęcona cała lekcja 14.
Znajdują się w niej informacje o sposobach przekazywania metodom argumentów typów
prostych oraz referencyjnych, a także o przeciążaniu metod, czyli technice umożliwiającej umieszczenie w jednej klasie kilku metod o tej samej nazwie. Nieco miejsca
poświęcono także metodzie main, od której zaczyna się wykonywanie aplikacji. Opisany zostanie również sposób, w jaki przekazuje się aplikacji parametry z wiersza poleceń.
Argumenty metod
Dzięki lekcji 13. wiadomo, że każda metoda może mieć argumenty. Argumenty metody 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 argumenty 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*/
}
Przykładowo w klasie Punkt przydałyby się metody umożliwiające ustawianie
współrzędnych. Jest tu możliwych kilka wariantów — zacznijmy od najprostszych: napiszemy dwie metody, ustawX i ustawY. Pierwsza będzie odpowiedzialna za przypisanie przekazanej jej wartości polu x, a druga — polu y. Zgodnie z podanym powyżej
schematem pierwsza z nich powinna wyglądać następująco:
void ustawX(int wspX) {
x = wspX;
}
natomiast druga:
void ustawY(int wspY) {
y = wspY;
}
Metody te nie zwracają żadnych wyników, co sygnalizuje słowo void, przyjmują natomiast jeden argument typu int. W ciele każdej z metod następuje z kolei przypisanie 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:
Rozdział 3.  Programowanie obiektowe. Część I
101
void ustawXY(int wspX, int wspY) {
x = wspX;
y = wspY;
}
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);
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 przypisane 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 terminologia, czyli utożsamianie zmiennej ze wskazywanym przez nią obiektem. Zatem jeśli zostanie napisane na przykład, że zmiennej jest przypisywany obiekt, w rzeczywistości będzie chodziło o to, że jest jej przypisywana referencja do tego obiektu.
Następnie wykorzystujemy metody ustawX i ustawY do przypisania polom obiektu5
punkt wartości 100. W kolejnym kroku za pomocą metody wyswietlWspolrzedne wyświetlamy te wartości na ekranie. Dalej wykorzystujemy metodę ustawXY, aby przypisać
polom obiektu nowy_punkt wartości 200, oraz wyświetlamy je na ekranie, również za
pomocą metody wyswietlWspolrzedne. Po wykonaniu tego programu uzyskany efekt
będzie taki jak na rysunku 3.5.
Rysunek 3.5.
Wykonanie programu
z listingu 3.9
Obiekt jako argument
Argumentem przekazanym metodzie może być również obiekt, nie musimy ograniczać
się jedynie do typów prostych. Podobnie metoda może zwracać obiekt w wyniku swojego
wykonania. W obu wymienionych sytuacjach postępowanie jest dokładnie takie samo
jak w przypadku typów prostych. Przykładowo metoda ustawXY w klasie Punkt mogłaby
przyjmować jako argument obiekt tej klasy (ściślej: referencję do obiektu), a nie dwie
liczby typu int, tak jak zostało to zrobione w przedstawionych wcześniej przykładach
(listing 3.8). Metoda taka wyglądałaby następująco:
void ustawXY(Punkt punkt) {
x = punkt.x;
y = punkt.y;
}
5
Dokładniej: polom obiektu wskazywanego przez zmienną punkt. Stosowana jest tu jednak już
wspomniana uproszczona, bardziej potoczna i łatwiejsza w komunikacji terminologia.
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.
Podobnie można dopisać do klasy Punkt metodę o nazwie pobierzWspolrzedne, która
zwróci w wyniku nowy obiekt klasy Punkt o takich współrzędnych, jakie zostały zapisane
w polach obiektu bieżącego. Metoda ta będzie miała postać:
Punkt pobierzWspolrzedne() {
Punkt punkt = new Punkt();
punkt.x = x;
punkt.y = y;
return punkt;
}
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);
}
}
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);
System.out.println("Obiekt punkt ma współrzędne:");
punkt.wyswietlWspolrzedne();
System.out.print("\n");
drugiPunkt = punkt.pobierzWspolrzedne();
System.out.println("Obiekt drugiPunkt ma współrzędne:");
drugiPunkt.wyswietlWspolrzedne();
System.out.print("\n");
Punkt trzeciPunkt = new Punkt();
trzeciPunkt.ustawXY(drugiPunkt);
System.out.println("Obiekt trzeciPunkt ma współrzędne:");
trzeciPunkt.wyswietlWspolrzedne();
}
}
Na początku deklarujemy zmienne punkt oraz drugiPunkt. Zmiennej punkt przypisujemy nowo utworzony obiekt klasy Punkt (rysunek 3.7a). Następnie wykorzystujemy
znane nam już dobrze metody ustawX i ustawY do przypisania polom x i y wartości 100
oraz wyświetlamy te dane na ekranie, używając metody wyswietlWspolrzedne.
W kolejnym kroku zmiennej drugiPunkt, która nie została wcześniej zainicjowana,
przypisujemy obiekt zwrócony przez metodę pobierzXY wywołaną na rzecz obiektu
punkt. A zatem zapis:
drugiPunkt = punkt.pobierzWspolrzedne();
oznacza, że wywoływana jest metoda pobierzWspolrzedne obiektu punkt, a zwrócony
przez nią wynik jest przypisywany zmiennej drugiPunkt. Jak wiadomo, wynikiem
działania tej metody będzie obiekt (referencja do obiektu) klasy Punkt będący kopią
obiektu punkt, czyli zawierający w polach x i y takie same wartości, jakie są zapisane
w polach obiektu punkt. To znaczy, że po wykonaniu tej instrukcji zmienna drugiPunkt
zawiera referencję do obiektu, w którym pola x i y mają wartość 100 (rysunek 3.7b).
Obie wartości wyświetlamy na ekranie za pomocą metody wyswietlWspolrzedne.
W trzeciej części programu tworzymy obiekt trzeciPunkt (Punkt trzeciPunkt = new
Punkt();) i wywołujemy jego metodę ustawXY, aby wypełnić pola x i y danymi. Metoda
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 rysunku 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
W fazie pierwszej, na samym początku programu, są jedynie dwie zmienne: punkt
i drugiPunkt. Tylko pierwszej z nich jest przypisany obiekt, druga jest po prostu pusta
(zawiera wartość null). Zostało to przedstawione na rysunku 3.7a. W części drugiej
przypisujemy zmiennej drugiPunkt odniesienie do obiektu, który jest kopią obiektu
punkt (rysunek 3.7b), a w trzeciej tworzymy obiekt trzeciPunkt i wypełniamy go danymi
pochodzącymi z obiektu drugiPunkt. Tym samym ostatecznie otrzymujemy trzy zmienne
i trzy obiekty (rysunek 3.7c).
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 znaków, czyli napisami. Innymi słowy, args to tablica ciągów znaków.
Tablica ta zawiera parametry wywołania programu, czyli parametry przekazane z wiersza
poleceń. O tym, że tak jest w istocie, można się przekonać, uruchamiając program
widoczny na listingu 3.12. Wykorzystuje on pętlę for do przejrzenia i wyświetlenia na
ekranie zawartości wszystkich komórek tablicy args. Przykładowy wynik jego działania jest widoczny na rysunku 3.8.
Listing 3.12.
class Main {
public static void main (String args[]) {
System.out.println("Parametry wywołania:");
for(int i = 0; i < args.length; i++){
System.out.println(args[i]);
}
}
}
Metodę main można umieścić w dowolnej klasie, po prostu tam, gdzie jest to najwygodniejsze. 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 wykonana, 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
Rozdział 3.  Programowanie obiektowe. Część I
107
Rysunek 3.8.
Program wyświetlający
parametry jego
wywołania
klasą uruchomieniową jest klasa Main, jeśli natomiast piszemy:
java Punkt
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 umieszczenie 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, jednak 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 sposób, aby metoda main znalazła się w klasie Punkt, bez zmieniania samej treści tej metody. Rozwiązanie tego zadania jest przedstawione na listingu 3.13.
Listing 3.13.
class Punkt {
int x;
int y;
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);
}
}
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
tak, jak jest to przedstawione na rysunku 3.9.
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ążaniem 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;
void ustawXY(int wspX, int wspY) {
x = wspX;
y = wspY;
}
void ustawXY(Punkt punkt) {
x = punkt.x;
y = punkt.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. ustawiają 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);
Rozdział 3.  Programowanie obiektowe. Część I
109
System.out.println("Po pierwszym ustawieniu współrzędnych:");
System.out.println("x = " + punkt.x);
System.out.println("y = " + punkt.y);
System.out.println("");
punkt.ustawXY(drugiPunkt);
System.out.println("Po drugim ustawieniu współrzędnych:");
System.out.println("x = " + punkt.x);
System.out.println("y = " + punkt.y);
}
Działanie tej metody jest proste i nie wymaga wielu wyjaśnień. Na początku tworzymy dwa obiekty klasy Punkt i przypisujemy je zmiennym punkt oraz drugiPunkt. Nastę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 drugą 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.
Ćwiczenia do samodzielnego wykonania
Ć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 poprzednią 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 natomiast zwrócić obiekt klasy Punkt zawierający stare wartości x i y.
110
Java. Praktyczny kurs
Lekcja 15. Konstruktory
Lekcja 15. jest poświęcona konstruktorom, czyli specjalnym metodom wykonywanym
podczas tworzenia obiektów. Będzie się można z niej dowiedzieć, jak powstaje konstruktor, jak umieścić go w klasie, a także czy może on przyjmować argumenty. Znajdą się tu
również informacje o sposobach przeciążania konstruktorów oraz o wykorzystaniu słowa
kluczowego this. Nie pominiemy techniki wywoływania konstruktora z innego konstruktora, która w pewnych sytuacjach upraszcza budowę kodu. Na zakończenie pojawi
się specjalna metoda finalize wykonywana, kiedy obiekt jest usuwany z pamięci.
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 przedstawione 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;
}
i wywoływać ją po każdym utworzeniu obiektu. Widać jednak od razu, że żadna z tych
koncepcji nie jest wygodna. Przede wszystkim wymagają one, aby programista zawsze pamiętał o ich stosowaniu, a jak pokazuje praktyka, jest to zwykle zbyt optymistyczne
założenie. Na szczęście obiektowe języki programowania udostępniają dużo wygodniejszy mechanizm konstruktorów. Otóż konstruktor jest to specjalna metoda, która zostaje wywołana zawsze w trakcie tworzenia obiektu w pamięci. Nadaje się więc doskonale
do jego zainicjowania.
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.
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);
}
}
Klasa ta ma wyjątkowo prostą konstrukcję, jedyne jej zadania to utworzenie obiektu
klasy Punkt i przypisanie odniesienia do niego zmiennej punkt oraz wyświetlenie zawartości jego pól na ekranie. Dzięki temu przekonamy się, że konstruktor rzeczywiście
został wykonany, zobaczymy bowiem widok zaprezentowany na rysunku 3.10.
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 (lekcja 14.), budowa takiego konstruktora byłaby więc następująca:
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()
trzeba wykorzystać wywołanie:
nazwa_klasy zmienna = new nazwa_klasy(argumenty_konstruktora)
W przypadku przykładowej klasy Punkt przydatny byłby np. konstruktor przyjmujący
dwa argumenty, które oznaczałyby współrzędne punktu. Jego definicja, co nie jest z pewnością żadnym zaskoczeniem, będzie wyglądać następująco:
Punkt(int wspX, int wspY) {
x = wspX;
y = wspY;
}
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 począ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 przyjmują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;
}
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 konstruktoró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 widoczna 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("");
Punkt punkt2 = new Punkt(100, 100);
System.out.println("punkt2:");
System.out.println("x = " + punkt2.x);
System.out.println("y = " + punkt2.y);
System.out.println("");
Punkt punkt3 = new Punkt(punkt1);
System.out.println("punkt3:");
System.out.println("x = " + punkt3.x);
System.out.println("y = " + punkt3.y);
}
}
114
Java. Praktyczny kurs
Pierwszy obiekt — punkt1 — jest tworzony za pomocą konstruktora bezargumentowego, który przypisuje polom x i y wartość 1. Obiekt drugi — punkt2 — jest tworzony
poprzez wywołanie drugiego z konstruktorów, który przyjmuje dwa argumenty odzwierciedlające wartości x i y. Oba pola otrzymują wartość 100. Konstruktor trzeci, zastosowany wobec obiektu punkt3, to nasza najnowsza konstrukcja. Jako argument przyjmuje
on obiekt klasy Punkt, w omawianym przypadku — obiekt wskazywany przez punkt1.
Ponieważ w tym obiekcie oba pola mają wartość 1, również pola obiektu punkt3
przyjmą wartość 1. W efekcie działania programu zobaczymy widok zaprezentowany
na rysunku 3.11.
Rysunek 3.11.
Wykorzystanie trzech
różnych konstruktorów
klasy Punkt
Słowo kluczowe this
Słowo kluczowe this to nic innego niż odwołanie do obiektu bieżącego. Można je traktować jako referencję do aktualnego obiektu. Najłatwiej pokazać to na przykładzie.
Załóżmy, że mamy konstruktor klasy Punkt taki jak na listingu 3.18, czyli przyjmujący
dwa argumenty, którymi są liczby typu int. Nazwami tych parametrów były wspX i wspY.
A co by się stało, gdyby nazwy tych parametrów brzmiały x i y, czyli gdyby deklaracja
tego konstruktora wyglądała tak jak poniżej?
Punkt(int x, int y) {
}
Co należy wpisać w jego treści, aby spełniał swoje zadanie? Gdyby postępować podobnie jak w przypadku klasy z listingu 3.18, powstałaby konstrukcja:
Punkt(int x, int y) {
x = x;
y = x;
}
Oczywiście nie ma to najmniejszego sensu6. W jaki bowiem sposób kompilator ma
rozróżnić, kiedy chodzi o argument konstruktora, a kiedy o pole klasy, jeśli ich nazwy
są takie same? Sam sobie nie poradzi i tu właśnie z pomocą przychodzi słowo this.
6
Chociaż formalnie taki zapis jest w pełni poprawny.
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
Wynika z tego, że poprawna postać opisanego konstruktora powinna wyglądać następująco:
Punkt(int x, int y) {
this.x = x;
this.y = y;
}
Instrukcję this.x = x rozumiemy jako: przypisz polu x wartość przekazaną jako argument o nazwie x, a instrukcję this.y = y odpowiednio jako: przypisz polu y wartość
przekazaną jako argument o nazwie y.
Wywoływanie metod w konstruktorach
Z wnętrza konstruktora (tak jak z wnętrza każdej innej metody) można wywoływać inne
metody. W trakcie prac nad klasą Punkt pojawiły się m.in. metody ustawX, ustawY czy
ustawXY. Doskonale nadają się one do ustalania wartości pól tej klasy. Skoro tak,
można je wykorzystać w każdym z konstruktorów, zamiast bezpośrednio przypisywać
wartości polom klasy. Najbardziej odpowiednia do tego celu będzie metoda ustawXY,
która ustawia jednocześnie oba pola. Jeśli chcemy zmienić kod konstruktorów przedstawionych na listingu 3.19, dobrze będzie zastosować dwie wersje metody ustawXY —
jedną przyjmującą jako argument dwie wartości typu int i drugą przyjmującą jako argument obiekt klasy Punkt. Same konstruktory wyglądałyby wówczas następująco:
Punkt() {
ustawXY(1, 1);
}
Punkt(int x, int y) {
ustawXY(x, y);
}
Punkt(Punkt punkt) {
ustawXY(punkt);
}
Pierwszy z nich ustawi współrzędne punktu na x = 1 i y = 1, drugi zrobi to zgodnie z przekazanymi wartościami w argumentach x i y, z kolei trzeci skopiuje wartości argumentów x i y z przekazanego obiektu punkt. Działanie tych konstruktorów będzie zatem takie
samo jak tych z listingu 3.19, mimo że została zastosowana zupełnie inna konstrukcja
kodu. Jeśli umieścimy je w klasie Punkt wraz z niezbędnymi wersjami metody ustawXY,
otrzymamy kod klasy widoczny na listingu 3.21.
Listing 3.21.
class Punkt {
int x;
int y;
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 przekonać, 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.
Przyjrzyjmy się teraz dokładniej drugiemu konstruktorowi, który jako argumenty
przyjmuje dwie liczby typu int. Ma on postać:
Punkt(int x, int y) {
ustawXY(x, y);
}
Po cofnięciu się do opisu słowa kluczowego this można sobie przypomnieć, że w sytuacji, 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 poprawny? 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 jednoznaczna i zawsze wiadomo, o jakie odwołanie chodzi.
Z wnętrza konstruktora można również wywołać… inny konstruktor. Bywa to przydatne w sytuacji, kiedy w klasie znajduje się kilka przeciążonych konstruktorów, a zakres
wykonywanego przez nie kodu się pokrywa — dokładniej, gdy kod jednego z konstruktorów jest podzbiorem kodu innego. Nie zawsze takie wywołanie jest możliwe i niezbędne,
niemniej taka możliwość istnieje, trzeba więc wiedzieć, jak zrealizować zadanie tego
typu.
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)
Rozdział 3.  Programowanie obiektowe. Część I
117
to zostanie wywołany konstruktor, którego argumenty pasują do wymienionych. Przykład
kodu wykorzystującego taką technikę jest widoczny na listingu 3.22.
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 wartości argumentu liczba2 polu liczba2. Rzecz jasna, ponieważ argument ma taką samą
nazwę jak pole, wykorzystujemy znaną nam już dobrze konstrukcję ze słowem kluczowym 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 jeden argument typu int oraz drugi przyjmujący jeden argument typu double, w konstruktorze 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.
118
Java. Praktyczny kurs
Do przetestowania klasy Dane z listingu 3.22 można wykorzystać zawartą w niej metodę 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
Dane dane1 = new
Dane dane2 = new
Dane dane3 = new
main (String args[]) {
Dane(100);
Dane(99.9);
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ć pamięć 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 takie 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.
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 wyjątkowo wygodne podejście dla programisty, zwalnia go bowiem z obowiązku zarządzania 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 wykorzystywał mechanizmy alokacji pamięci specyficzne dla danej platformy systemowej czy
też odwoływał się do modułów napisanych w innych językach programowania, o posprzą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++){
120
Java. Praktyczny kurs
test = new Test();
}
System.gc();
for(int i = 0; i < 100000; i++){
for(int j = 0; j < 1000; j++){
}
}
}
}
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 przypisujemy 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 zawierała referencję do ostatnio przypisanego, dziesiątego obiektu, natomiast wszystkie poprzednie obiekty zostaną utracone i będą mogły zostać poddane procesowi odśmiecania.
Aby zobaczyć efekty tego odśmiecania, wywołujemy na końcu instrukcję System.gc(),
która powoduje uruchomienie odśmiecacza8. Za tą instrukcją znajdują się dwie zagnieżdżone pętle for, które opóźniają zakończenie wykonywania programu. Ostatecznie na
ekranie pojawi się obraz widoczny na rysunku 3.13. Jest na nim dziewięć napisów
Niszczenie obiektu odpowiadających dziewięciu utraconym obiektom. Ostatni, dziesiąty
obiekt został zniszczony przez maszynę wirtualną już po zakończeniu pracy programu,
dlatego też na ekranie nie mógł się pojawić dziesiąty napis. Warto samodzielnie sprawdzić, co się stanie, jeśli pętle opóźniające zostaną usunięte, i jak będzie się wtedy zachowywała aplikacja przy kolejnych uruchomieniach (uwaga: będzie to zależało od
użytej maszyny wirtualnej i wersji Javy).
Ćwiczenia do samodzielnego wykonania
Ćwiczenie 15.1.
Napisz klasę, której zadaniem będzie przechowywanie liczb typu int. Dołącz jednoargumentowy 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.
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 dwuargumentowych 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 powinna 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 dziedziczenia. W lekcji 16. zaprezentowane są podstawy, czyli sposoby tworzenia klas
potomnych oraz zachowania konstruktorów klasy bazowej i potomnej. W lekcji 17.
122
Java. Praktyczny kurs
zostały omówione specyfikatory dostępu pozwalające na ustalanie praw dostępu do
składowych klas, a także temat tworzenia i wykorzystywania pakietów. W lekcji 18.
przedstawiono techniki przesłaniania pól i metod w klasach potomnych oraz składowe
statyczne. W lekcji ostatniej, 19., zostały opisane klasy finalne oraz składowe finalne klas.
Lekcja 16. Klasy potomne
Lekcja 16. jest poświęcona podstawom dziedziczenia, czyli budowania nowych klas
na bazie już istniejących. Każda taka nowa klasa przejmuje zachowanie i właściwości
klasy bazowej. Zostanie tu przedstawione, jak tworzy się klasy potomne, jakie podstawowe zależności występują między klasą bazową a potomną oraz jak zachowują się
konstruktory w przypadku dziedziczenia.
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
int
int
int
}
Punkt3D {
x;
y;
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 operują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 schematycznie wygląda następująco:
9
Często spotyka się również potoczną formę „dziedziczyć z”, np. „Klasa Punkt3D dziedziczy z klasy Punkt”.
Rozdział 3.  Programowanie obiektowe. Część I
123
class klasa_potomna extends klasa_bazowa {
//wnętrze klasy
}
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 dodatkowo 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);
}
}
class Punkt3D extends Punkt {
int z;
}
Klasa Punkt ma tu postać znaną nam z wcześniej prezentowanych przykładów. Zawiera
dwa pola, x i y, oraz sześć metod: pobierzX i pobierzY (zwracające współrzędne x i y),
124
Java. Praktyczny kurs
ustawX, ustawY i ustawXY (ustawiające współrzędne) oraz wyswietlWspolrzedne (wyświetlającą wartości pól x i y na ekranie). Ponieważ klasa Punkt3D dziedziczy po klasie
Punkt, również zawiera wymienione pola i metody oraz dodatkowo pole o nazwie z.
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();
System.out.println("x = " + punkt.x);
System.out.println("y = " + punkt.y);
System.out.println("z = " + punkt.z);
System.out.println("");
punkt.ustawX(100);
punkt.ustawY(200);
System.out.println("x = " + punkt.x);
System.out.println("y = " + punkt.y);
System.out.println("z = " + punkt.z);
System.out.println("");
punkt.ustawXY(300, 400);
System.out.println("x = " + punkt.x);
System.out.println("y = " + punkt.y);
System.out.println("z = " + punkt.z);
}
}
Na początku definiujemy zmienną klasy Punkt3D o nazwie punkt i przypisujemy jej
nowo utworzony obiekt typu Punkt3D. Wykorzystujemy oczywiście dobrze nam znany
operator new. Następnie wyświetlamy na ekranie wartości wszystkich pól tego obiektu.
Wiadomo, że są to trzy pola, x, y, z, oraz że powinny one otrzymać wartości domyślne równe 0 (tabela 3.1). Następnie wykorzystujemy metody ustawX oraz ustawY, aby
przypisać polom x i y wartości 100 oraz 200. W kolejnym kroku ponownie wyświetlamy zawartość wszystkich pól na ekranie. W dalszej części kodu wykorzystujemy
metodę ustawXY do przypisania polu x wartości 300, a polu y wartości 400 i jeszcze raz
wyświetlamy zawartość wszystkich pól na ekranie. Otrzymamy tym samym widok
zaprezentowany na rysunku 3.14. Jest to też najlepszy dowód, że klasa Punkt3D rzeczywiście odziedziczyła wszystkie pola i metody zawarte w klasie Punkt.
Klasa Punkt3D nie jest jednak w takiej postaci w pełni funkcjonalna, należałoby przecież 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ść
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 samodzielnego 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;
}
}
Konstruktory klasy bazowej i potomnej
Ostatnia wersja klasy Punkt3D, widoczna na listingu 3.27, ma już metody operujące na
polu z, tzn. ustawiające oraz pobierające jego wartość, brakuje jej jednak konstruktorów. Przypomnijmy, że we wcześniejszych wersjach klasy Punkt zostały napisane aż
trzy konstruktory:
 bezargumentowy, ustawiający wartość wszystkich pól na 1;
 dwuargumentowy, przyjmujący dwie wartości typu int;
 jednoargumentowy, przyjmujący obiekt klasy Punkt.
126
Java. Praktyczny kurs
Co się stanie, jeśli dopiszemy je do kodu z listingu 3.25 i użyjemy razem z programem 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 skomplikowane 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 wszystkim 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ą natomiast uwzględnione w listingach dostępnych na serwerze FTP).
Rozdział 3.  Programowanie obiektowe. Część I
127
Przyglądając się dokładnie napisanym przed chwilą konstruktorom, nie da się nie zauważ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;
}
są prawie identyczne! Jedyna różnica to dodatkowy argument i dodatkowa instrukcja
przypisująca jego wartość polu z. Czy nie lepiej byłoby zatem wykorzystać konstruktor klasy Punkt w klasie Punkt3D lub ogólniej — konstruktor klasy bazowej w konstruktorze klasy potomnej? Oczywiście, że tak. Nie można jednak wywołać konstruktora tak
jak zwyczajnej metody, do tego celu służy specjalna konstrukcja. Dokładniej, w konstruktorze klasy potomnej należy wywołać metodę super() w postaci:
class klasa_potomna extends klasa_bazowa {
klasa_potomna(argumenty) {
super();
/*
…dalszy kod konstruktora…
*/
}
}
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
Oczywiście jeśli metodzie super zostaną przekazane argumenty, zostanie wywołany
konstruktor klasy bazowej, który tym argumentom odpowiada. Do praktycznego zobrazowania takiej konstrukcji doskonale nadaje się klasa Punkt3D. Spójrzmy na listing 3.29.
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…
*/
}
W pierwszym konstruktorze wywołujemy bezargumentową metodę super, która powoduje wywołanie bezargumentowego konstruktora klasy bazowej. Taki konstruktor (bezargumentowy) istnieje w klasie Punkt, konstrukcja ta nie budzi więc żadnych wątpliwości.
W konstruktorze drugim wywołujemy metodę super, przekazując jej dwa parametry typu
int. Ponieważ w klasie Punkt istnieje konstruktor dwuargumentowy przyjmujący dwie
wartości typu int, i ta konstrukcja jest jasna. Trzeci konstruktor przyjmuje jeden argument typu (klasy) Punkt3D, w klasie Punkt istnieje konstruktor przyjmujący jeden argument klasy… no właśnie, w klasie Punkt przecież wcale nie ma konstruktora, który
przyjmowałby argument tego typu! Jest co prawda konstruktor:
Punkt(Punkt punkt) {
//instrukcje konstruktora
}
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 sytuacji 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 powinien 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.
Rozdział 3.  Programowanie obiektowe. Część I
129
Ćwiczenia do samodzielnego wykonania
Ć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 kolorze. Kolor ma być określany dodatkowym polem o nazwie kolor i typie int (lub innym). 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 przyjmował obiekt klasy Punkt. Wykorzystaj w tym konstruktorze metodę super.
Lekcja 17. Specyfikatory dostępu i pakiety
Specyfikatory dostępu pełnią ważną rolę w Javie, pozwalają bowiem na określenie
praw dostępu do składowych klas, a także do samych klas. Występują one w czterech rodzajach, które zostaną przedstawione właśnie w tej lekcji. W jej drugiej części będzie
natomiast mowa o pakietach, które można traktować jako biblioteki klas. Okaże się,
co to znaczy, że klasa jest publiczna lub pakietowa, oraz jak tworzyć własne pakiety.
130
Java. Praktyczny kurs
Publiczne, prywatne czy chronione?
W Javie przed każdym polem i metodą może, a często wręcz powinien pojawiać się tak
zwany specyfikator dostępu (używa się również terminu „modyfikator dostępu”, ang.
access modifier). Dostęp do każdego pola i każdej metody może być:
 publiczny,
 prywatny,
 chroniony,
 pakietowy.
Domyślnie, jeżeli przed składową klasy nie występuje żadne określenie, dostęp jest pakietowy, 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.
Dostęp publiczny — public
Jeżeli dana składowa (pole lub metoda) klasy jest publiczna, oznacza to, że mają do niej
dostęp wszystkie inne klasy, czyli dostęp ten nie jest w żaden sposób ograniczony.
Weźmy np. pierwotną wersję klasy Punkt z listingu 3.1 (lekcja 13.). Gdyby pola x i y
tej klasy miały być publiczne, musiałaby ona wyglądać tak jak na listingu 3.30.
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
Podobnie jest z metodami — specyfikator dostępu powinien być pierwszym elementem
deklaracji, czyli ogólnie należy napisać:
specyfikator_dostępu typ_zwracany nazwa_metody(argumenty)
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() {
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;
}
}
Dostęp prywatny — private
Składowe oznaczone słowem private to takie, które są dostępne jedynie z wnętrza
danej klasy. To znaczy, że wszystkie metody danej klasy mogą je dowolnie odczytywać i zapisywać, natomiast dostęp z zewnątrz jest zabroniony zarówno dla zapisu, jak
i odczytu. Jeżeli zatem w klasie Punkt z listingu 3.30 oba pola zostaną ustawione jako
prywatne, będzie ona miała postać widoczną na listingu 3.32.
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);
}
}
Po utworzeniu nowego obiektu klasy Punkt przypisujemy go zmiennej punkt. Następnie
próbujemy przypisać polom x i y tego obiektu wartości całkowite oraz wyświetlić je
na ekranie. Już próba kompilacji udowodni, że nie można w klasie Main odwoływać się
do pól x i y obiektu klasy Punkt, gdyż są one oznaczone jako prywatne. Wynikiem kompilacji będzie jedynie seria komunikatów o nieprawidłowych odwołaniach widoczna
na rysunku 3.17.
132
Java. Praktyczny kurs
Rysunek 3.17. Próba odwołania się do składowych prywatnych klasy Punkt
W jaki zatem sposób odwołać się do pola prywatnego? Przypomnijmy sobie opis prywatnej 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;
}
}
Te metody pozwolą już na bezproblemowe odwoływanie się do obu prywatnych pól.
Teraz program z listingu 3.33 trzeba poprawić tak, aby wykorzystywał nowe metody,
a zatem zamiast:
punkt.x = 100;
należy napisać:
punkt.ustawX(100);
Zamiast:
System.out.println("punkt.x = " + punkt.x);
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 potomnych. Jeżeli z klasy Punkt z listingu 3.32 wyprowadzimy klasę Punkt3D w postaci:
class Punkt3D extends Punkt {
private int z;
}
klasa ta nie będzie miała żadnego dostępu do pól x i y.
Dostęp chroniony — protected
Składowe klasy oznaczone słowem protected to składowe chronione. Są one dostępne jedynie dla metod danej klasy, klas potomnych oraz klas z tego samego pakietu (patrz
podrozdział „Pakiety”). Oznacza to, że jeśli mamy przykładową klasę Punkt, w której
znajdzie się chronione pole o nazwie x, to w klasie Punkt3D, o ile jest ona klasą pochodną
od Punkt, również będzie można odwoływać się do pola x. Jednak dla każdej innej
klasy, która nie dziedziczy po Punkt, pole x będzie niedostępne11. W praktyce klasa
Punkt — z polami x i y zadeklarowanymi jako chronione — będzie wyglądała tak jak
na listingu 3.35.
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;
}
Czemu ukrywamy wnętrze klasy?
W tym miejscu pojawi się zapewne pytanie: czemu zabraniać przy użyciu modyfikatorów
private i protected bezpośredniego dostępu do niektórych składowych klas? Otóż chodzi
o ukrycie implementacji wnętrza klasy. Programista, projektując daną klasę, udostępnia na zewnątrz (innym programistom) pewien interfejs służący do posługiwania się
jej obiektami. Określa więc sposób, w jaki można korzystać z danego obiektu. To, co
11
Chyba że jest to klasa dostępna w ramach tego samego pakietu co klasa Punkt.
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 listingu 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 przedstawiony 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());
}
}
Załóżmy teraz, że zaistniała konieczność (obojętnie z jakiego powodu) zmiany sposobu
reprezentacji współrzędnych punktu na tak zwany układ biegunowy, w którym położenie punktu jest opisywane za pomocą dwóch parametrów: kąta α oraz odległości od
początku układu współrzędnych (rysunek 3.18). W klasie Punkt nie będzie już pól x i y,
więc przestaną mieć sens wszelkie odwołania do nich. Gdyby pola te były zadeklarowane
jako publiczne, byłby to spory problem. Nie dość, że we wszystkich programach wykorzystujących klasę Punkt trzeba byłoby zmieniać odwołania, to dodatkowo w każdym
takim miejscu należałoby dokonywać przeliczania współrzędnych. Jednak dzięki temu, że
pola x i y są prywatne, a dostęp do nich odbywa się przez publiczne metody, wystarczy
tylko odpowiednio zmienić interfejs klasy Punkt. Jak się za chwilę okaże, można całkowicie tę klasę przebudować, a korzystający z niej program z listingu 3.37 nie będzie wymagał
nawet najmniejszej poprawki.
Rysunek 3.18.
Położenie punktu
reprezentowane za
pomocą współrzędnych
biegunowych
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 skomplikowane, wystarczy znajomość podstawowych funkcji trygonometrycznych oraz twierdzenia
Pitagorasa. Ponieważ ta książka to kurs programowania w Javie, a nie lekcja matematyki, zostaną podane w gotowej postaci12. I tak (dla oznaczeń jak na rysunku 3.18, bez
używania polskich liter):
y  modul  sin()
x  modul  1  sin2 ()
oraz:
modul  x 2  y 2
sin() 
y
modul
Mając te dane, można przystąpić do napisania odpowiednich metod. W ramach treningu
przeanalizujmy metodę pobierzY. Jej postać będzie następująca:
public int pobierzY() {
double y = modul * sinusalfa;
return (int) y;
}
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 dokonujemy 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.
136
Java. Praktyczny kurs
public int pobierzX() {
double x = modul * Math.sqrt(1 - sinusalfa * sinusalfa);
return (int) x;
}
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 obliczają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. Wykonujemy 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();
modul = Math.sqrt(x * x + y * y);
sinusalfa = y / modul;
}
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, natomiast 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ątkowe 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;
}
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
public int pobierzX() {
double x = modul * Math.sqrt(1 - sinusalfa * sinusalfa);
return (int) x;
}
public void ustawX(int wspX) {
int x = wspX;
int y = pobierzY();
modul = Math.sqrt(x * x + y * y);
sinusalfa = y / modul;
}
public void ustawY(int wspY) {
int x = pobierzX();
int y = wspY;
modul = Math.sqrt(x * x + y * y);
sinusalfa = y / modul;
}
}
Pakiety
Klasy w Javie są grupowane w jednostki nazywane pakietami. Pakiet to inaczej biblioteka, 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 pakietu 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 {
/*
138
Java. Praktyczny kurs
…treść klasy Punkt…
*/
}
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;
przykładowy plik z klasą Main mógłby więc mieć następującą strukturę:
import grafika.Punkt;
class Main {
/*
…treść klasy Main…
*/
}
Polecenie import grafika.Punkt mówi kompilatorowi, że będziemy korzystać z klasy
Punkt znajdującej się w pakiecie grafika. Po jego zastosowaniu w klasie Main można
odwoływać się bezpośrednio do klasy Punkt. Jeżeli jednak instrukcji import zabraknie, to
w przypadku odwoływania się do klasy Punkt trzeba będzie podać pełną nazwę pakietową,
tak więc utworzenie obiektu punkt klasy Punkt musiałoby wyglądać wtedy następująco:
grafika.Punkt punkt = new grafika.Punkt();
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.
Jak nazywać pakiety?
Pakiety są przydatne nie tylko ze względu na możliwość zgrupowania powiązanych
tematycznie klas w jednostki biblioteczne. Pozwalają również na uniknięcie konfliktu
nazw. Bardzo prawdopodobna jest przecież sytuacja, kiedy dwóch programistów
utworzy klasy o tej samej nazwie. Bez wykorzystania techniki pakietów nie można
byłoby skorzystać z obu tych klas naraz w programie. Wystąpiłby przecież konflikt nazw.
Jeśli jednak zostaną one umieszczone w dwóch różnych pakietach, konfliktu nie będzie.
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 prawdopodobień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ą.
Rozdział 3.  Programowanie obiektowe. Część I
139
Rysunek 3.20.
Struktura katalogów
dla pakietu o nazwie
com.marcinlis.grafika
Należy więc zbudować drzewo katalogów o nazwach kolejnych poziomów: com,
marcinlis, grafika.
To jednak nie wszystko — przecież kompilator musi „wiedzieć”, w którym miejscu to
drzewo się zaczyna. Na rysunku 3.20 widać, że wcześniej znajduje się jeszcze jeden
katalog o nazwie java. Informację o tym należy przekazać, ustawiając zmienną środowiskową CLASSPATH. Musi ona zawierać oddzielone średnikami nazwy wszystkich
katalogów, w których zaczynają się struktury pakietów. W omawianym przypadku,
przy założeniu, że pracujemy w systemie Windows, a katalog java z rysunku 3.20
znajduje się na dysku C:, zmiennej CLASSPATH należy przypisać wartość: c:\java\ (na
zasadach opisanych w rozdziale 1.).
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ą przechowywał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;
public class Punkt {
public int x;
public int y;
}
package grafika3d;
public class
public int
public int
public int
}
Punkt {
x;
y;
z;
Oczywiście klasę Punkt z pakietu grafika2d umieszczamy w pliku o nazwie Punkt w katalogu grafika2d, a klasę Punkt z pakietu grafika3d — także w pliku Punkt, ale w katalogu o nazwie grafika3d. Ścieżka dostępu do obu katalogów musi być określona w zmiennej
140
Java. Praktyczny kurs
środowiskowej CLASSPATH (zgodnie z podanymi wcześniej informacjami)14. Aby teraz
w jednym programie skorzystać z obu klas, przy deklaracji niezbędne będzie podanie
pełnej nazwy pakietowej dla klasy, zatem deklaracja przykładowej zmiennej i utworzenie
obiektu dla klasy z pakietu grafika2d będą wyglądały tak:
grafika2d.Punkt punkt2d = new grafika2d.Punkt();
natomiast dla pakietu grafika3d tak:
grafika3d.Punkt punkt3d = new grafika3d.Punkt();
Przykładowa klasa Main wykorzystująca obie deklaracje jest zaprezentowana na listingu 3.40, a efekt jej działania widać na rysunku 3.21.
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;
System.out.println("punkt2d.x = " + punkt2d.x);
System.out.println("punkt2d.y = " + punkt2d.y);
System.out.println("");
punkt3d.x = 110;
punkt3d.y = 120;
punkt3d.z = 130;
System.out.println("punkt3d.x = " + punkt3d.x);
System.out.println("punkt3d.y = " + punkt3d.y);
System.out.println("punkt3d.y = " + punkt3d.z);
}
}
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.
Rozdział 3.  Programowanie obiektowe. Część I
141
Klasy publiczne i pakietowe
Klasy w Javie można podzielić na trzy typy:
 publiczne,
 pakietowe,
 wewnętrzne.
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 pakietu. Jeśli natomiast przed nazwą klasy znajduje się modyfikator public, jest to klasa
publiczna, czyli dostępna dla wszystkich innych klas.
Deklaracja klasy pakietowej ma więc postać:
class nazwa_klasy {
//pola i metody klasy
}
natomiast klasy publicznej:
public class nazwa_klasy {
//pola i metody klasy
}
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 pakietowy, 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.
Po co zdefiniowano taką zasadę? Po to, aby umożliwić wprowadzenie klas pomocniczych,
usługowych wobec klasy głównej. Takich klas pomocniczych nie udostępnia się publicznie, mają one jedynie współpracować z klasą główną. Umieszcza się je wtedy jako klasy pakietowe w tym samym pliku co klasę główną. Jeśli na przykład dopiszemy do
klasy Punkt klasę usługową o nazwie Pomocnicza, plik Punkt.java będzie miał postać
taką jak przedstawiona na listingu 3.41.
Nie należy zapomnieć, że tym razem, inaczej niż w większości dotychczas przedstawionych 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.
142
Java. Praktyczny kurs
Listing 3.41.
package grafika;
public class Punkt {
int x;
int y;
Pomocnicza obiektPomocniczy;
/*
…dalsza część klasy Punkt…
*/
}
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 rysunku 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.
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 klasy 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…
*/
}
natomiast w pliku Main.java kod klasy Main w postaci:
class Main {
/*
…treść klasy Main…
*/
}
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 pakietowymi. 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.
Ćwiczenia do samodzielnego wykonania
Ć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.
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 odwrotnie). 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ć.
Lekcja 18. Przesłanianie metod
i składowe statyczne
W lekcji 14. była mowa o przeciążaniu metod, teraz omówimy, w jaki sposób dziedziczenie wpływa na przeciążanie, oraz przeanalizujemy technikę przesłaniania pól i metod.
Technika ta pozwala na uzyskanie bardzo ciekawego efektu umieszczenia składowych
o identycznych nazwach zarówno w klasie bazowej, jak i potomnej. Drugim poruszanym
tematem będą składowe statyczne, czyli takie, które mogą istnieć nawet wtedy, kiedy
nie istnieją obiekty danej klasy.
Przeciążanie metod a dziedziczenie
Przeciążanie metod, czyli możliwość umieszczenia w jednej klasie kilku metod o tej samej
nazwie, różniących się argumentami, było omawiane w lekcji 14. Pytanie: jak ma się to
do dziedziczenia, czyli czy można przeciążać metody klasy bazowej w klasie potomnej?
Odpowiedź brzmi — tak. Jeśli mamy np. klasę o nazwie A, a w niej bezargumentową
metodę f (czyli klasę w postaci widocznej na listingu 3.43):
Rozdział 3.  Programowanie obiektowe. Część I
145
Listing 3.43.
public class A {
public void f() {
System.out.println("Metoda f() z klasy A");
}
}
i z niej wyprowadzimy klasę pochodną o nazwie B, to w klasie B da się zdefiniować metodę f np. przyjmującą jeden argument typu int, tak jak jest to widoczne na listingu 3.44.
Listing 3.44.
public class B extends A {
public void f(int liczba) {
System.out.println("Metoda f(int) z klasy B");
}
}
Zatem w klasie A została zdefiniowana bezargumentowa metoda o nazwie f. Jej zadaniem
jest wyświetlenie na ekranie nazwy klasy, w której została zdefiniowana. W klasie B
dziedziczącej po klasie A również została zdefiniowana metoda o nazwie f, ale przyjmująca jeden argument typu int. Jedynym zadaniem metody f z klasy B jest również wypisanie na ekranie nazwy klasy, w której została zdefiniowana. Po umieszczeniu obu
klas w jednym katalogu można je bez problemu skompilować, a prosty program testowy widoczny na listingu 3.45 pozwala przekonać się, że działanie będzie takie samo jak wtedy, gdy wszystko odbywa się w jednej klasie (przykłady z lekcji 14.).
Listing 3.45.
public class Main {
public static void main (String args[]) {
A a = new A();
B b = new B();
//prawidłowe wywołanie, w klasie A istnieje metoda f()
a.f();
//nieprawidłowe wywołanie, w klasie A nie ma metody f(int)
//a.f(0);
//oba wywołania prawidłowe, w klasie B istnieją metody
//f() i f(int)
b.f();
b.f(0);
}
}
Definiujemy tu dwa obiekty, a klasy A oraz b klasy B. Następnie wywołujemy w kilku
wariantach metodę f. Wywołanie pierwsze — a.f() — jest prawidłowe, obiekt a jest typu
(klasy) A, a więc zawiera bezargumentową metodę f. Na ekranie zostanie zatem wyświetlony tekst Metoda f() z klasy A. Wywołanie drugie — a.f(0) — zostało ujęte
146
Java. Praktyczny kurs
w komentarz, gdyż jest oczywiście nieprawidłowe. W obiekcie klasy A nie ma bowiem
metody o nazwie f, która przyjmowałaby argument typu int. Wywołania trzecie —
b.f() — oraz czwarte — b.f(0) — są prawidłowe. W obiekcie klasy B istnieje zarówno
bezargumentowa metoda f odziedziczona po klasie A (wywołanie b.f() wyświetli
zatem na ekranie napis Metoda f() z klasy A), jak i metoda o nazwie f (zdefiniowana
w klasie B), która przyjmuje argument typu int (czyli wywołanie b.f(0) wyświetli na
ekranie napis Metoda f(int) z klasy B). Ostatecznie w wyniku uruchomienia klasy
Main na ekranie pojawią się napisy widoczne na rysunku 3.23.
Rysunek 3.23.
Ilustracja mechanizmu
przeciążania metod
w klasach potomnych
Przesłanianie pól i metod
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? Albo 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");
}
}
public class B extends A {
public void f() {
System.out.println("Metoda f z klasy B");
}
}
W klasie A znajduje się bezargumentowa metoda o nazwie f wyświetlająca na ekranie
tekst z nazwą klasy, w której została zdefiniowana. Klasa B dziedziczy po klasie A
(jest klasą pochodną od A), zgodnie z zasadami dziedziczenia przejmuje więc metodę
f z klasy A. Tymczasem w klasie B została ponownie zadeklarowana bezargumentowa
metoda f (również wyświetlająca nazwę klasy, w której została zdefiniowana, czyli
tym razem klasy B). Wydawać by się mogło, że w takim wypadku wystąpi konflikt
Rozdział 3.  Programowanie obiektowe. Część I
147
nazw (dwukrotne zadeklarowanie metody f), jednak próba kompilacji wykaże, że kompilator 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 obiektach 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 metody 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ę wydawać zawiły. Spójrzmy na przykład klas z listingu 3.46 — chodzi o to, czy w obiekcie 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 upraszczają 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 wywołaniu w schematycznej postaci:
super.nazwa_metody(argumenty);
16
Akurat ten termin nie oddaje w pełni zasady działania omawianego mechanizmu.
148
Java. Praktyczny kurs
Wywołanie takie najczęściej stosuje się w metodzie przesłaniającej (np. metodzie f
klasy B), ale możliwe jest ono również w dowolnej innej metodzie klasy pochodnej.
Jeśli zatem przykładowe klasy A i B wyglądałyby jak na listingu 3.48, wykonanie kodu
klasy Main widocznego na listingu 3.49 dałoby wynik jak na rysunku 3.25.
Listing 3.48.
public class A {
public void f() {
System.out.println("Klasa A");
}
}
public class B extends A {
//ta metoda przesłania metodę f z klasy A,
//ale wywołuje również przesłoniętą metodę f
//z klasy A
public void f() {
super.f();
System.out.println("Klasa B");
}
//ta metoda wywołuje metodę f z klasy B
public void g() {
f();
}
//ta metoda wywołuje metodę f z klasy A
public void h() {
super.f();
}
}
Listing 3.49.
public class Main {
public static void main (String args[]) {
A a = new A();
B b = new B();
System.out.println("Wynik wywołania a.f():");
a.f();
System.out.println("\nWynik wywołania b.f():");
b.f();
System.out.println("\nWynik wywołania b.g():");
b.g();
System.out.println("\nWynik wywołania b.h():");
b.h();
}
}
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 korzysta 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 metoda 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, metoda 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 przypadku 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.
150
Java. Praktyczny kurs
Listing 3.50.
public class A {
public int liczba;
}
public class B extends A {
public int liczba;
}
W klasie A zostało zdefiniowane pole o nazwie liczba i typie int. W klasie B, która dziedziczy 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 (pochodzącego z klasy A). Taka sytuacja została przedstawiona na listingu 3.51.
Listing 3.51.
public class A {
public int liczba;
}
public class B extends A {
public int liczba;
public void ustawLiczbaA(int liczba) {
super.liczba = liczba;
}
public void ustawLiczbaB(int liczba) {
this.liczba = liczba;
}
public int pobierzLiczbaA() {
return super.liczba;
}
public int pobierzLiczbaB() {
return 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 ustawianie, 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
Rozdział 3.  Programowanie obiektowe. Część I
151
Możemy teraz napisać klasę testową Main, która udowodni, że obiekt klasy B naprawdę 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);
System.out.print("Wartość przesłoniętego pola liczba: ");
System.out.println(b.pobierzLiczbaA());
System.out.print("Wartość pola liczba z klasy B: ");
System.out.println(b.pobierzLiczbaB());
}
}
Tworzymy tu obiekt b klasy B. Następnie wywołujemy metodę ustawLiczbaA, która
zapisze przekazany jej argument w polu liczba pochodzącym z klasy A, oraz metodę
ustawLiczbaB, która zapisze przekazany jej argument w polu liczba pochodzącym z klasy B.
Dalej wyświetlamy zawartość obu pól na ekranie (wykorzystując metody pobierzLiczbaA i pobierzLiczbaB). Na rysunku 3.26 został przedstawiony wynik działania tego
programu. Widać wyraźnie, że obiekt b przechował obie wartości, zatem rzeczywiście
znajdują się w nim dwie wersje pola o nazwie liczba.
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 przedstawiona 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;
}
public class B extends A {
protected char pole1;
}
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ę wykonywanie 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 listingu 3.54.
Listing 3.54.
public class A {
public static void f() {
System.out.println("Metoda f klasy A");
}
}
Tak napisaną metodę można wywołać klasycznie, to znaczy po utworzeniu obiektu
klasy A, np. w postaci:
A a = new A();
a.f();
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);
W przypadku klasy A wywołanie tego typu miałoby następującą postać:
A.f();
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.
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 aplikacji 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 statycznych.
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 deklarujemy, 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.
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
bądź też poprzedzając je nazwą klasy:
nazwa_klasy.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 niezależ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;
System.out.println("-=- Po przypisaniu: A.liczba = 100 -=-");
System.out.println("A.liczba: " + A.liczba);
System.out.println("");
//krok 2
A obiekt1 = new A();
A obiekt2 = new A();
System.out.println("-=- Po utworzeniu obiektów -=-");
System.out.println("obiekt1.liczba: " + obiekt1.liczba);
System.out.println("obiekt1.liczba: " + obiekt2.liczba);
System.out.println("");
//krok 3
obiekt1.liczba = 200;
System.out.println("-=- Po przypisaniu: obiekt1.liczba = 200 -=-");
System.out.println("obiekt1.liczba: " + obiekt1.liczba);
System.out.println("obiekt1.liczba: " + obiekt2.liczba);
System.out.println("");
//krok 4
obiekt2.liczba = 300;
System.out.println("-=- Po przypisaniu: obiekt2.liczba = 300 -=-");
System.out.println("obiekt1.liczba: " + obiekt1.liczba);
System.out.println("obiekt1.liczba: " + obiekt2.liczba);
}
}
W pierwszym kroku przypisujemy polu statycznemu liczba z klasy A wartość 100. Stosujemy 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
Rozdział 3.  Programowanie obiektowe. Część I
155
liczba faktycznie została przypisana wartość 100 (wyświetlamy ją przecież na ekranie).
To najlepszy dowód na to, że pole statyczne istnieje niezależnie od obiektów danej klasy.
W drugim kroku tworzymy dwa obiekty klasy A i przypisujemy je zmiennym obiekt1
i obiekt2. Następnie wyświetlamy zawartość pola liczba za pomocą odwołań
obiekt1.liczba oraz obiekt2.liczba. To kolejny dowód na to, że pole to jest niezależne
od obiektów. Po ich utworzeniu nie przypisywaliśmy polu żadnej wartości, gdy tymczasem ma ono wartość 100, czyli przypisaną w kroku pierwszym.
Krok trzeci to wykonanie instrukcji obiekt1.liczba = 200, czyli przypisanie polu
liczba wartości 200. I tym razem pozwala to przekonać się, że jest tylko jedno takie pole
mimo istnienia dwóch obiektów klasy A. Zarówno odwołanie w postaci obiekt1.liczba,
jak i obiekt2.liczba daje bowiem w wyniku wartość 200.
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 odczytanie 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 prostu 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
Ćwiczenia do samodzielnego wykonania
Ć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.
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 argumentu 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 argumentów.
Ćwiczenie 18.5.
Napisz klasę Przechowalnia zawierającą metodę statyczną o nazwie przechowaj przyjmującą jeden argument typu int. Klasa ta ma zapamiętywać argument przekazany metodzie przechowaj w taki sposób, że każde wywołanie tej metody spowoduje zwrócenie
poprzednio zapisanej wartości i zapamiętanie aktualnie przekazanej.
Lekcja 19. Klasy i składowe finalne
W Javie klasa lub jej składowa może być finalną, czyli taką, której nie można zmieniać. Umożliwia to słowo kluczowe final. W tej lekcji zostanie pokazane, jak z niego
korzystać i co ono znaczy w odniesieniu do klasy jako takiej, a co w przypadku metod
i pól. Ponadto przedstawione zostanie to, jak zachowują się pola finalne typów prostych,
a jak pola finalne typów obiektowych. Okaże się również, że finalne mogą być także
argumenty metod.
Klasy finalne
Klasa finalna to taka, z której nie wolno wyprowadzać innych klas. Pozwala to programiś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
}
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 przedstawiony sposób zapisu.
Z klasy Ostateczna przedstawionej na listingu 3.58 nie można wyprowadzić żadnej
innej klasy. Tak więc widoczna na listingu 3.59 klasa RozszerzonaOstateczna dziedzicząca po Ostateczna jest niepoprawna. Java nie dopuści do kompilacji takiego kodu.
Kompilator zgłosi komunikat o błędzie zaprezentowany na rysunku 3.28.
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
158
Java. Praktyczny kurs
Tak więc poprawne są wszystkie poniższe deklaracje:
final int liczba;
public final double liczba;
public static final char znak;
W rzeczywistości specyfikator dostępu oraz słowa final i static mogą występować
w dowolnej kolejności, jednak dla zachowania spójnego stylu i przejrzystości kodu
będziemy konsekwentnie trzymać się przedstawionej zasady, że na pierwszym miejscu umieszcza się specyfikator dostępu, słowo final występuje zawsze tuż przed typem pola, natomiast słowo static zawsze tuż przed final.
Pola finalne typów prostych
Przykładowa klasa zawierająca pola finalne jest przedstawiona na listingu 3.60.
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;
//prawidłowo: odczyt pól liczba1 i liczba2, zapis pola liczba2
liczba2 = liczba2 + liczba1;
//nieprawidłowo: niedozwolony zapis pola liczba1
//liczba1 = liczba2 / 2;
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, wykorzystują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 zdecydowanie 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.
Rozdział 3.  Programowanie obiektowe. Część I
159
Rysunek 3.29. Próba przypisania wartości zmiennej finalnej
Pola finalne typów referencyjnych
Zachowanie pól finalnych w przypadku typów prostych jest jasne — nie wolno zmieniać
ich wartości. To znaczy wartość przypisana polu pozostaje niezmienna przez cały czas
działania programu. W przypadku typów referencyjnych jest oczywiście tak samo, trzeba
jednak uświadomić sobie, co to w takim przypadku oznacza. Otóż pisząc:
nazwa_klasy nazwa_pola = new nazwa_klasy(argumenty_konstruktora)
polu nazwa_pola przypisujemy referencję do nowo powstałego obiektu klasy nazwa_klasy.
Przykładowo w przypadku klasy Punkt poznanej w lekcji 13. deklaracja:
Punkt punkt = new Punkt()
oznacza przypisanie referencji do powstałego na stercie obiektu klasy Punkt zmiennej
punkt.
Gdyby pole to było finalne, tej referencji nie wolno byłoby zmieniać, jednak nic nie stałoby na przeszkodzie, aby modyfikować pola obiektu, na który ta referencja wskazuje.
Czyli po wykonaniu instrukcji:
final Punkt punkt = new Punkt();
możliwe byłoby odwołanie w postaci (zakładając publiczny dostęp do pola x):
punkt.x = 100;
Aby lepiej to zrozumieć, warto spojrzeć na kod przedstawiony na listingu 3.61.
Listing 3.61.
public class Punkt {
public int x;
public int y;
}
public class Main {
public static final Punkt punkt = new Punkt();
public static void main (String args[]) {
//prawidłowo, można modyfikować pola obiektu punkt
punkt.x = 100;
punkt.y = 200;
//nieprawidłowo, nie można zmieniać referencji finalnej
//punkt = new Punkt();
}
}
160
Java. Praktyczny kurs
Są tu widoczne dwie publiczne klasy: Main i Punkt (należy je oczywiście zapisać
w dwóch plikach: Main.java i Punkt.java). Klasa Punkt zawiera dwa publiczne pola
typu int o nazwach x i y. Klasa Main zawiera jedno publiczne, statyczne i finalne pole
o nazwie punkt, któremu została przypisana referencja do obiektu klasy Punkt. Ponieważ
pole jest publiczne, mają do niego dostęp wszystkie inne klasy. Ponieważ jest statyczne,
może istnieć nawet wtedy, kiedy nie istnieje żaden obiekt klasy Main (z taką sytuacją
mamy właśnie do czynienia, bo nie utworzyliśmy takiego obiektu). Natomiast ponieważ jest finalne, nie wolno zmienić jego wartości. Ale uwaga: zgodnie z tym, co zostało
napisane we wcześniejszych akapitach, nie wolno zmienić referencji, przy czym nic
nie stoi na przeszkodzie, aby modyfikować pola obiektu, na który ona wskazuje. Dlatego
też pierwsze dwa odwołania w metodzie main są poprawne. Wolno przypisać dowolne
wartości polom x i y obiektu wskazywanego przez pole punkt. Nie należy natomiast
zmieniać samej referencji, zatem ujęta w komentarz instrukcja punkt = new Punkt()
jest nieprawidłowa.
Inicjalizacja pól finalnych
W dotychczasowych przykładach pola finalne były inicjalizowane w momencie ich
deklaracji, zatem w przypadku pól typów prostych wyglądało to tak:
final nazwa_typu nazwa_pola = wartość;
a w przypadku typów referencyjnych:
final nazwa_klasy nazwa_pola = new nazwa_klasy();
Wydaje się to logiczne, jednak wcale nie musi tak być. Otóż pola finalne można deklarować 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;
//nieprawidłowo, nie ma jeszcze obiektu punkt2
punkt2.x = 200;
punkt2.y = 300;
//nieprawidłowo, nie można zmieniać referencji finalnej
punkt1 = new Punkt();
Rozdział 3.  Programowanie obiektowe. Część I
161
//prawidłowo, inicjalizacja pola finalnego
punkt2 = new Punkt();
//prawidłowo, można modyfikować pola obiektu punkt2
punkt2.x = 200;
punkt2.y = 300;
//nieprawidłowo, nie można modyfikować pola finalnego
liczba1 = 200;
//prawidłowo, inicjalizacja pola finalnego
liczba2 = 300;
//nieprawidłowo, pole liczba2 zostało już zainicjalizowane
liczba2 = 400;
}
}
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).
Prawidłowe będą więc wszystkie następujące przykładowe deklaracje:
final void metoda(){/*kod metody*/};
public final int metoda(){/*kod metody*/};
public static final void metoda(){/*kod metody*/};
public static final int metoda(int argument){/*kod metody*/};
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 przypadku dziedziczenia. Dokładniej, zgodnie z tym, co zostało napisane powyżej, metod finalnych 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
}
}
public class Pochodna extends Bazowa {
public void metoda() {
//kod metody
}
}
162
Java. Praktyczny kurs
Są na nim widoczne dwie klasy, pierwsza Bazowa i druga Pochodna, dziedzicząca po
Bazowa. W klasie Bazowa została zdefiniowana publiczna i finalna metoda o nazwie
metoda. W Pochodna metoda ta została przesłonięta (lekcja 14.). Zgodnie z powyższymi
wyjaśnieniami kod ten powinien być zatem nieprawidłowy (nie wolno przesłaniać
metody finalnej) i tak jest w istocie. Próba kompilacji spowoduje wyświetlenie komunikatu o błędzie widocznego na rysunku 3.30.
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ównież 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)
Przykładowo deklaracja publicznej metody o nazwie metoda1, niezwracającej żadnej
wartości, przyjmującej natomiast jeden argument finalny typu int o nazwie argument1,
będzie miała postać:
public void metoda1(final int argument1){/*treść metody*/}
Spójrzmy teraz na listing 3.64. Została na nim przedstawiona klasa z metodami ilustrują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;
//nieprawidłowo, nie można modyfikować argumentu finalnego
//liczba = 100;
}
public void metoda2(final Punkt punkt) {
//prawidłowo, można odczytywać wartości pól obiektu
//wskazywanego przez punkt
int x = punkt.x;
int y = punkt.y;
Rozdział 3.  Programowanie obiektowe. Część I
163
//nieprawidłowo, nie można modyfikować argumentu finalnego
//punkt = new Punkt();
//prawidłowo, można zapisywać pola obiektu
//wskazywanego przez argument finalny punkt
punkt.x = 100;
punkt.y = 100;
}
}
Ćwiczenia do samodzielnego wykonania
Ć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?
164
Java. Praktyczny kurs
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.
Lekcja 20. Blok try…catch
Lekcja 20. jest poświęcona wprowadzeniu do tematu wyjątków. Zostaną w niej omówione sposoby zapobiegania powstawaniu niektórych typów błędów w programach
oraz to, jak stosować przechwytujący błędy blok instrukcji try…catch. Przedstawiony
zostanie też bliżej wyjątek o nieco egzotycznej dla początkujących programistów nazwie ArrayIndexOutOfBoundsException, dzięki któremu można uniknąć błędów związanych z przekroczeniem dopuszczalnego zakresu indeksów tablic.
Sprawdzanie poprawności danych
Powróćmy na chwilę do rozdziału 2. i lekcji 11. Znalazł się tam przykład, w którym następowało odwołanie do nieistniejącego elementu tablicy (listing 2.39). Było to spowodowane następującą sekwencją instrukcji:
int tab[] = new int[10];
tab[10] = 5;
Doświadczony programista od razu zauważy, że te instrukcje są błędne, jako że zadeklarowana została tablica 10-elementowa, zatem — ponieważ indeksowanie tablicy zaczyna się od 0 — ostatni element tablicy ma indeks 9. Tak więc instrukcja tab[10] = 5
powoduje próbę odwołania się do nieistniejącego jedenastego elementu tablicy. Ten
błąd jest jednak stosunkowo prosty do wychwycenia, nawet wtedy, gdy pomiędzy deklaracją tablicy a nieprawidłowym odwołaniem są umieszczone inne instrukcje.
166
Java. Praktyczny kurs
Dużo więcej kłopotów mogłaby sprawić sytuacja, w której np. tablica byłaby deklarowana 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;
}
}
public class Main {
public static void main (String args[]) {
Tablica tablica = new Tablica();
tablica.ustawElement(5, 10);
int liczba = tablica.pobierzElement(10);
System.out.println(liczba);
}
}
Widoczne są dwie klasy: Tablica oraz Main. W klasie Tablica zostało zadeklarowane prywatne pole typu tablicowego o nazwie tablica, któremu została przypisana 10-elementowa 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, pobierzElement 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.
W klasie Main tworzymy obiekt klasy Tablica i wykorzystujemy metodę ustawElement
do zapisania w komórce o indeksie 5 wartości 10. W kolejnej linii popełniamy drobną
pomyłkę. W metodzie pobierzElement odwołujemy się do nieistniejącego elementu
o indeksie 10 (została pomylona wartość z indeksem). Musi to spowodować wystąpienie
błędu w trakcie działania aplikacji (rysunek 4.1). Błąd tego typu bardzo łatwo popełnić,
gdyż w klasie Main nie widać rozmiarów tablicy.
Rysunek 4.1. Odwołanie do nieistniejącego elementu w klasie Tablica
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 zasygnalizować 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 zasygnalizowania, 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 dodatkowego 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;
}
}
}
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 indeks nie przekracza maksymalnej dopuszczalnej wartości1. Jeśli tak, ustawiamy pole
wystapilBlad na true oraz zwracamy wartość 0. Oczywiście w tym przypadku zwrócona 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 konieczne, 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.
W metodzie ustawElement postępujemy podobnie. Sprawdzamy, czy przekazany indeks
nie przekracza maksymalnej dopuszczalnej wartości. Jeśli przekracza, pole wystapilBlad
ustawiamy na true, w przeciwnym razie dokonujemy przypisania wartości z argumentu
wartosc wskazanej komórce tablicy i ustawiamy wystapilBlad na false. Po takiej modyfikacji obu metod w klasie Main można już bez problemu stwierdzić, czy operacje
wykonywane na klasie Tablica zakończyły się sukcesem. Przykład wykorzystania
możliwości, jakie daje nowe pole, został przedstawiony na listingu 4.3.
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... (rysunek 4.2).
Sposobów radzenia sobie z problemem przekroczenia indeksu tablicy można byłoby
zapewne wymyślić jeszcze kilka. Metody tego typu mają jednak poważną wadę: programiści mają tendencję do ich… niestosowania. Często możliwy błąd wydaje się zbyt banalny,
aby się nim zajmować, czasami po prostu zapomina się o sprawdzaniu pewnych warunków. Dodatkowo przedstawione wyżej sposoby, czyli zwracanie wartości sygnalizacyjnej
1
Nie jest natomiast badana dopuszczalna wartość minimalna. Będzie to ćwiczenie do samodzielnego
wykonania.
Rozdział 4.  Wyjątki
169
Rysunek 4.2.
Efekt wykonania
programu z listingu 4.3
czy korzystanie z dodatkowych zmiennych, powodują niepotrzebne rozbudowywanie
kodu aplikacji, co paradoksalnie może prowadzić do powstawania kolejnych błędów,
a więc błędów wynikających z napisania kodu zajmującego się obsługą błędów…
W Javie zastosowano w związku z tym mechanizm tak zwanych wyjątków, znany na
pewno programistom C++ i Object Pascala2.
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 lekcji 11. Był to wyjątek spowodowany przekroczeniem dopuszczalnego zakresu tablicy.
Zobaczmy go raz jeszcze: na listingu 4.4 została zaprezentowana odpowiednio spreparowana 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 skompilujemy i uruchomimy taki program, na ekranie pojawi się obraz podobny do tego z rysunku 4.1 (tym razem w komunikacie wskazany będzie tylko jeden plik źródłowy — Main.java). Został tu wygenerowany wyjątek o nazwie ArrayIndexOutOfBoundsException,
oznaczający, że indeks tablicy znajduje się poza dopuszczalnymi granicami.
Oczywiście gdyby możliwości wyjątków kończyły się na wyświetlaniu informacji na
ekranie i przerywaniu działania programu, ich przydatność byłaby mocno ograniczona.
Na szczęście wygenerowany wyjątek można przechwycić i wykonać własny kod obsługi błędu. Do takiego przechwycenia służy blok instrukcji try…catch. W najprostszej
postaci wygląda on następująco:
try{
//instrukcje mogące spowodować wyjątek
}
2
Sama technika obsługi sytuacji wyjątkowych sięga jednak lat sześćdziesiątych ubiegłego stulecia
(czyli wieku XX).
170
Java. Praktyczny kurs
catch (TypWyjątku identyfikatorWyjątku){
//obsługa wyjątku
}
W nawiasie klamrowym występującym po słowie try została umieszczona instrukcja,
która może spowodować wygenerowanie wyjątku. W bloku występującym po catch
umieszcza się kod, który ma zostać wykonany, kiedy zostanie wygenerowany wyjątek.
W przypadku klasy Main z listingu 4.4 blok try…catch należałoby zastosować w sposób
przedstawiony na listingu 4.5.
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 wygenerowanie wyjątku. W nawiasie okrągłym występującym po instrukcji catch został wymieniony 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 komunikat 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 postać 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.
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 metodę z klasy Tablica. Tym samym w klasie Main można zastosować blok try…catch.
Identyczna sytuacja ma miejsce w przypadku hierarchicznego wywołania metod jednej
klasy, czyli kiedy metoda f wywołuje metodę g, która wywołuje metodę h generującą
z kolei wyjątek. W każdej z wymienionych metod można zastosować blok try…catch
do przechwycenia tego wyjątku. Dokładnie taki przykład jest widoczny na listingu 4.8.
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();
}
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 zostaną 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
Rozdział 4.  Wyjątki
173
Ćwiczenia do samodzielnego wykonania
Ć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 dopuszczalnej 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 zostanie 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.
Lekcja 21. Wyjątki to obiekty
W lekcji 20. został omówiony wyjątek sygnalizujący przekroczenie dopuszczalnego
zakresu tablicy. To nie jest oczywiście jedyny dostępny typ — czas poznać również
inne. W lekcji 21. okaże się, że wyjątki są tak naprawdę obiektami, a także że tworzą
one hierarchiczną strukturę. Omówimy przechwytywanie wielu wyjątków w jednym
bloku try, zagnieżdżanie bloków try…catch oraz przypadek, gdy jeden wyjątek ogólny
może obsłużyć wiele błędów.
Dzielenie przez zero
Rodzajów wyjątków jest bardzo wiele. Wiadomo już, jak reagować na przekroczenie
zakresu tablicy. Czas poznać inny typ wyjątku, generowanego, kiedy zostanie podjęta
próba wykonania dzielenia przez zero. W tym celu musimy spreparować odpowiedni
fragment kodu. Wystarczy w metodzie main umieścić przykładową instrukcję:
int liczba = 10 / 0;
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).
174
Java. Praktyczny kurs
Rysunek 4.4. Próba wykonania dzielenia przez zero
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 zdefiniowanych blisko 500. Aby sprawnie się nimi posługiwać, trzeba się dowiedzieć, czym tak
naprawdę są.
Wyjątek jest obiektem
Wyjątek, określany wcześniej jako byt programistyczny, to nic innego jak obiekt powstający, kiedy w programie wystąpi sytuacja wyjątkowa. Skoro wyjątek jest obiektem,
to wspomniany wcześniej typ wyjątku (ArrayIndexOutOfBoundsException, Arithmetic
Exception) nie jest niczym innym niż klasą opisującą ten obiekt. Jeśli teraz spojrzymy
ponownie na ogólną postać instrukcji try…catch:
try{
//instrukcje mogące spowodować wyjątek
}
catch(TypWyjątku identyfikatorWyjątku){
//obsługa wyjątku
}
stanie się jasne, że w takim razie identyfikatorWyjątku to zmienna obiektowa (referencyjna) wskazująca na obiekt wyjątku. Na tym obiekcie można wykonywać operacje
zdefiniowane w klasie wyjątku. Możliwe jest np. uzyskanie systemowego komunikatu
o błędzie. Wystarczy wywołać metodę getMessage(). Można to zilustrować na przykładzie wyjątku generowanego podczas próby wykonania dzielenia przez zero. Jest on
zaprezentowany na listingu 4.9.
Listing 4.9.
public class Main {
public static void main (String args[]) {
try{
int liczba = 10 / 0;
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());
}
}
}
Wykonujemy tutaj próbę niedozwolonego dzielenia przez zero oraz przechwytujemy
wyjątek typu ArithmeticException. W bloku catch najpierw wyświetlamy własne
komunikaty o błędzie, a następnie komunikat systemowy (pobrany za pomocą metody
getMessage()). Po uruchomieniu kodu na ekranie pojawi się widok zaprezentowany
na rysunku 4.5.
Rysunek 4.5.
Wyświetlenie
systemowego
komunikatu o błędzie
Istnieje jeszcze jedna możliwość uzyskania komunikatu o wyjątku — umieszczenie
w argumencie instrukcji System.out.println zmiennej wskazującej na obiekt wyjątku,
czyli w przypadku listingu 4.9:
System.out.println(e);
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.
Jeśli zatem w programie z listingu 4.9 zamienimy instrukcję:
System.out.println(e.getMessage());
na:
System.out.println(e);
otrzymamy nieco dokładniejszy komunikat określający dodatkowo klasę wyjątku, tak
jak jest to widoczne na rysunku 4.6.
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 dziedziczenia, 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 przykł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 bardzo 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 przechwytują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ą.
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.
Przechwytywanie wielu wyjątków
W jednym bloku try…catch można przechwytywać wiele wyjątków. Konstrukcja taka zawiera wtedy jeden blok try i wiele bloków catch. Schematycznie wygląda to następująco:
try{
//instrukcje mogące spowodować wyjątek
}
catch(KlasaWyjątku1 identyfikatorWyjątku1){
//obsługa wyjątku 1
}
catch(KlasaWyjątku2 identyfikatorWyjątku2){
//obsługa wyjątku 2
}
/*
…dalsze bloki catch…
*/
catch(KlasaWyjątkuN identyfikatorWyjątkuN){
//obsługa wyjątku n
}
Po wygenerowaniu wyjątku następuje sprawdzenie, czy jest on klasy KlasaWyjątku1
(inaczej: czy jego typem jest KlasaWyjątku1). Jeśli tak — są wykonywane instrukcje
obsługi tego wyjątku i blok try…catch jest opuszczany. Jeżeli jednak wyjątek nie jest
klasy KlasaWyjątku1, następuje sprawdzenie, czy jest on klasy KlasaWyjątku2 itd.
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.
178
Java. Praktyczny kurs
Nie można zatem najpierw przechwycić wyjątku RuntimeException, a dopiero po nim
ArithmeticException (por. rysunek 4.7), gdyż skończy się to błędem kompilacji. Jeśli
więc dokonamy próby kompilacji przykładowego programu przedstawionego na listingu
4.11, efektem będą komunikaty widoczne na rysunku 4.8.
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);
}
}
}
Rysunek 4.8. Błędna hierarchia wyjątków powoduje błąd kompilacji
Dzieje się tak dlatego, że (można powiedzieć) błąd bardziej ogólny zawiera już w sobie błąd bardziej szczegółowy. Jeśli zatem przechwyci się najpierw wyjątek RuntimeException, 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");
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 pierwszego błędu. Dzieje się tak dlatego, że po jego wystąpieniu blok try został przerwany, a sterowanie zostało przekazane blokowi catch. Jeśli więc w bloku try któraś z instrukcji 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, przekonamy 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
W Java 7 i nowszych dostępna jest możliwość przychwycenie kilku typów wyjątków
w jednym bloku catch. Poszczególne typy należy oddzielić od siebie pionową kreską |.
Wtedy niezależnie od tego, jaki wyjątek z wymienionych wystąpi, zostanie wykonany
taki sam kod (zawarty w bloku catch). Schemat takiej konstrukcji jest następujący:
try{
//instrukcje mogące spowodować wyjątek
}
catch (TypWyjątku1 | TypWyjątku2 |…TypWyjątkuN identyfikatorWyjątku){
//obsługa wyjątku
}
180
Java. Praktyczny kurs
A zatem gdybyśmy w programie z listingu 4.12 chcieli w identyczny sposób obsługiwać
błąd arytmetyczny oraz wynikający z niezainicjowania zmiennej punkt, a w inny sposób
reagować na wszystkie pozostałe wyjątki, kod mógłby wyglądać tak jak na listingu 4.13.
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żanie bloków try…catch
Bloki try…catch można zagnieżdżać. To znaczy, że w jednym bloku przechwytującym
wyjątek X może istnieć drugi blok, który będzie przechwytywał wyjątek Y. Schematycznie taka konstrukcja wygląda następująco:
try{
//instrukcje mogące spowodować wyjątek 1
try{
//instrukcje mogące spowodować wyjątek 2
}
catch (TypWyjątku2 identyfikatorWyjątku2){
//obsługa wyjątku 2
}
}
catch (TypWyjątku1 identyfikatorWyjątku1){
//obsługa wyjątku 1
}
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średniego 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, zastosujemy zagnieżdżenie, tak jak jest to przedstawione na listingu 4.14.
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ę przypisania 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 zatem 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).
Ćwiczenia do samodzielnego wykonania
Ćwiczenie 21.1.
Popraw kod z listingu 4.11 tak, aby przechwytywanie wyjątków odbywało się w prawidł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.
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 metody: 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 odpowiednio 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 powinien 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 instrukcji. W przeciwnym przypadku rozwiąż zadanie, korzystając z instrukcji if. Dodatkowo do klasy TablicaPunktow dopisz metodę usuwającą punkt znajdujący się pod
wskazanym indeksem tablicy.
Lekcja 22. Własne wyjątki
Wyjątki można przechwytywać, aby zapobiec niekontrolowanemu zakończeniu działania programu w przypadku wystąpienia błędu. Ta technika została omówiona w lekcjach 20. i 21. Okazuje się jednak, że wyjątki można również zgłaszać samemu, a także
że można tworzyć nowe, nieistniejące wcześniej typy wyjątków. Tej właśnie tematyce
jest poświęcona bieżąca lekcja.
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, wystarczy napisać:
throw new Exception();
5
Stosuje się również termin „wyrzucenie wyjątku”, od ang. throw — rzucać, wyrzucać.
Rozdział 4.  Wyjątki
183
Jeśli taki wyjątek zostanie obsłużony przez znajdującą się w danym bloku (danej metodzie) instrukcję try…catch, nie trzeba robić nic więcej. Jeśli jednak nie zostanie obsłużony (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ć). Robi 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 zaprezentowany 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();
można równie dobrze zastosować konstrukcję:
Exception exception = new Exception();
throw exception;
W obu przedstawionych przypadkach efekt będzie identyczny, najczęściej korzysta się
jednak z pierwszego zaprezentowanego sposobu.
184
Java. Praktyczny kurs
Aby zgłaszanemu wyjątkowi został przypisany komunikat, należy przekazać go jako
parametr konstruktora klasy Exception, a więc użyć instrukcji w postaci:
throw new Exception("komunikat");
lub:
Exception exception = new Exception("komunikat");
throw exception;
Oczywiście można tworzyć obiekty wyjątków klas dziedziczących po Exception. Przykładowo: jeśli wykryjemy próbę dzielenia przez zero, być może zechcemy samodzielnie
wygenerować wyjątek, nie czekając, aż zgłosi go maszyna wirtualna. Taka sytuacja
została przedstawiona na listingu 4.16.
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 wyjątek klasy ArithmeticException. W konstruktorze klasy przekazujemy komunikat informujący o dzieleniu przez zero. Podajemy w nim wartości argumentów metody podziel, 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ównież metoda main. Tworzymy w niej nowy obiekt klasy Dzielenie i odniesienie do niego 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.
Rozdział 4.  Wyjątki
185
Rysunek 4.11. Zgłoszenie własnego wyjątku klasy ArithmeticException
Ponowne zgłoszenie przechwyconego wyjątku
Wiadomo już, jak przechwytywać wyjątki oraz jak je samodzielnie zgłaszać. To pozwoli
omówić technikę ponownego zgłaszania (potocznie: wyrzucania) już przechwyconego
wyjątku. Jak pamiętamy, bloki try…catch można zagnieżdżać bezpośrednio, a także
stosować je w przypadku kaskadowo wywoływanych metod. Jeśli jednak na którymkolwiek poziomie przechwytywaliśmy wyjątek, jego obsługa ulegała zakończeniu.
Nie zawsze jest to korzystne zachowanie, czasami istnieje potrzeba, aby po wykonaniu
bloku obsługi wyjątek nie był niszczony, ale był przekazywany dalej. Aby tak się stało,
należy zastosować instrukcję throw. Schematycznie wygląda to następująco:
try{
//instrukcje mogące spowodować wyjątek
}
catch(TypWyjątku identyfikatorWyjątku){
//instrukcje obsługujące sytuację wyjątkową
throw indentyfikatorWyjątku
}
Na listingu 4.17 widać, jak taka sytuacja wygląda w praktyce.
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.
186
Java. Praktyczny kurs
Rysunek 4.12. Ponowne zgłoszenie raz przechwyconego wyjątku
W przypadku zagnieżdżonych bloków try sytuacja wygląda analogicznie. Wyjątek przechwycony w bloku wewnętrznym i ponownie zgłoszony może być obsłużony w bloku
zewnętrznym, w którym może być oczywiście zgłoszony kolejny raz itd. Jest to zobrazowane na listingu 4.18.
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 nieprawidł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 przechwycenie, 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.
Identycznie będą się zachowywały wyjątki w przypadku kaskadowego wywoływania
metod. Z sytuacją tego typu mieliśmy do czynienia w przykładzie z listingu 4.8. W klasie
Wyjatki były wtedy zadeklarowane cztery metody: main, f, g, h. Metoda main wywoływała
metodę f, ta metodę g, a ta z kolei metodę h. W każdej z metod znajdował się blok
try…catch, jednak zawsze działał tylko ten najbliższy miejsca wystąpienia wyjątku
(lekcja 20.). Zmodyfikujmy kod z listingu 4.8 tak, aby za każdym razem wyjątek był
Rozdział 4.  Wyjątki
187
Rysunek 4.13. Przechwytywanie i ponowne zgłaszanie wyjątków
po przechwyceniu ponownie zgłaszany. Kod realizujący to zadanie jest przedstawiony
na listingu 4.19.
Listing 4.19.
public class Wyjatki {
public void f() {
try{
g();
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Wyjątek: metoda f");
throw e;
}
}
public void g() {
try{
h();
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Wyjątek: metoda g");
throw e;
}
}
public void h() {
int[] tab = new int[0];
try{
tab[0] = 1;
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Wyjątek: metoda h");
throw e;
}
}
public static void main(String args[]) {
Wyjatki wyjatki = new Wyjatki();
try{
wyjatki.f();
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Wyjątek: metoda main");
throw e;
}
}
}
188
Java. Praktyczny kurs
Wszystkie wywołania metod pozostały niezmienione, w każdym bloku catch została
natomiast dodana instrukcja throw ponownie zgłaszająca przechwycony wyjątek. Na
rysunku 4.14 jest widoczny efekt uruchomienia przedstawionego kodu. Widać wyraźnie, jak wyjątek jest propagowany po wszystkich metodach, począwszy od metody h,
a skończywszy na main. Ponieważ w bloku try…catch metody main jest on ponownie
zgłaszany, na ekranie widać także reakcję maszyny wirtualnej.
Rysunek 4.14. Kaskadowe przechwytywanie wyjątków
Tworzenie własnych wyjątków
Programując w Javie, nie trzeba zdawać się wyłącznie na wyjątki systemowe, które
dostaje się wraz z JDK. Nic nie stoi na przeszkodzie, aby tworzyć własne klasy wyjątków.
Wystarczy napisać klasę pochodną pośrednio lub bezpośrednio z klasy Throwable, a będzie można wykorzystywać ją do zgłaszania własnych wyjątków. W praktyce jednak
wyjątki zwykle są wyprowadzane z Exception i klas od niej pochodnych. Klasa taka
w najprostszej postaci będzie miała postać:
public class nazwa_klasy extends Exception {
//treść klasy
}
Przykładowo można utworzyć bardzo prostą klasę o nazwie GeneralException (wyjątek ogólny) w postaci:
public class GeneralException extends Exception {
}
To w zupełności wystarczy. Nie musimy dodawać żadnych nowych pól i metod. Ta
klasa jest pełnoprawną klasą obsługującą wyjątki, z której możemy korzystać w taki
sam sposób jak ze wszystkich innych klas opisujących wyjątki. Na listingu 4.20 jest
widoczna przykładowa klasa Main, która generuje wyjątek GeneralException.
Listing 4.20.
public class Main {
public static void main (String args[]) throws GeneralException {
throw new GeneralException();
}
}
Rozdział 4.  Wyjątki
189
W metodzie main za pomocą instrukcji throws zaznaczamy, że metoda ta może zgłaszać wyjątek klasy GeneralException, sam wyjątek zgłaszamy natomiast przez zastosowanie instrukcji throw dokładnie w taki sam sposób jak we wcześniejszych przykładach. Na rysunku 4.15 jest widoczny efekt działania takiego programu: rzeczywiście
zgłoszony został wyjątek nowej klasy GeneralException (oczywiście aby przykład udało
się skompilować, kod klasy GeneralException należy zapisać w pliku GeneralException.java).
Rysunek 4.15.
Zgłaszanie własnych
wyjątków
Własne klasy wyjątków można wyprowadzać również z klas pochodnych od Exception.
Można by np. rozszerzyć klasę ArithmeticException o wyjątek zgłaszany wyłącznie
wtedy, kiedy wykryte zostanie dzielenie przez zero. Taką klasę można nazwać np.
DivideByZeroException. Miałaby ona postać widoczną na listingu 4.21.
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
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 argumentem 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 komunikatów, należy dopisać do klasy DivideByZeroException odpowiedni konstruktor (przyjmują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);
}
}
Teraz instrukcja throw z listingu 4.22 mogłaby wyglądać np. tak:
throw new DivideByZeroException("Dzielenie przez zero: " + liczba1 + "/" + liczba2);
Sekcja finally
Do bloku try można dołączyć sekcję finally, która będzie wykonywana zawsze, niezależ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
}
Rozdział 4.  Wyjątki
191
Zgodnie z tym, co zostało napisane wcześniej, instrukcje sekcji finally są wykonywane
zawsze, niezależnie od tego, czy w bloku try wystąpi wyjątek, czy nie. Zostało to zobrazowane w przykładzie z listingu 4.24, który jest oparty na kodzie z listingów 4.21 i 4.22.
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 przekazanych jej argumentów. Tym razem metoda podziel pozostała bez zmian w stosunku do wersji z listingu 4.22, czyli zgłasza błąd DivideByZeroException. Zmodyfikowana 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.
Drugie wywołanie metody podziel powoduje wygenerowanie wyjątku, zostaną zatem
wykonane zarówno instrukcje bloku catch, jak i finally. Na ekranie pojawią się więc
dwa napisy: Przechwycenie wyjątku 2 oraz Sekcja finally 2. Ostatecznie wynik działania
całego programu będzie taki, jak zaprezentowano na rysunku 4.17.
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ą wygenerowania 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();
}
}
Metoda f1 zawiera instrukcję return 0 kończącą wykonywanie metody i zwracającą
wartość 0. Ta instrukcja została ujęta w blok try…finally, w którym wyświetlana jest
informacja o nazwie metody. Dzięki temu będzie można się przekonać, że nawet prze-
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 wygenerowania wyjątku) zostanie wykonany blok finally. Zatem po skompilowaniu i uruchomieniu programu zobaczymy widok przedstawiony na rysunku 4.18.
Rysunek 4.18. Efekt działania kodu z listingu 4.25
Ćwiczenia do samodzielnego wykonania
Ćwiczenie 22.1.
Napisz klasę, w której zostaną zadeklarowane metody f i main. W metodzie f napisz dowolną 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łaszany 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 pierwszego 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.
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 generowany wyjątek nowej klasy o nazwie InvalidIndexException (lub podobnej). Przetestuj
działanie nowego mechanizmu klasy Tablica, zmieniając odpowiednio klasę Main.
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 pracować 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 opisana 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.
196
Java. Praktyczny kurs
Konwersje typów prostych
W lekcji 6. rozważaliśmy, co się wydarzy w programie, jeśli np. wynikiem dzielenia dwóch
liczb całkowitych będzie liczba ułamkowa, która zostanie przypisana zmiennej typu całkowitoliczbowego — a zatem w jaki sposób zostanie wykonana przykładowa instrukcja:
int liczba = 9 / 2;
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), zostanie 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żniejsza operacja to konwersja typów danych, a zaokrąglenie jest jedynie efektem ubocznym
tej konwersji.
Programista nie musi zdawać się na konwersje automatyczne, w wielu przypadkach
są one wręcz niemożliwe. Często niezbędne jest wykonanie konwersji jawnej. Takiej
operacji dokonuje się poprzez umieszczenie przed wyrażeniem nazwy typu docelowego
ujętej w nawias okrągły. Schematycznie taka konstrukcja wygląda następująco:
(typ_docelowy) wyrażenie;
Przykładowo: aby dokonać jawnej konwersji wyniku dzielenia 9 / 2, który jest wartoś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 sprawdzić, wystarczy użyć programu przedstawionego na listingu 5.1. Widać na nim przykł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;
}
}
Rozdział 5.  Programowanie obiektowe. Część II
197
Rysunek 5.1. Błąd kompilacji spowodowany brakiem jawnej konwersji
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ż wystę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;
Operacje takie zostaną wykonane poprawnie, warto jednak wiedzieć, że — formalnie
rzecz biorąc — kompilator potraktuje ten zapis tak, jakby miał on postać:
int liczba1 = 10;
double liczba2 = (double) liczba1;
Rzutowanie typów obiektowych
Typy obiektowe, tak jak typy proste, podlegają konwersjom. Wróćmy na chwilę do końcowej części lekcji 16. Pojawiło się tam stwierdzenie, ż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. Dzieje się tak dlatego, że w takiej sytuacji zostanie dokonane tak zwane automatyczne rzutowanie typu obiektu.
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;
}
public class Punkt3D extends Punkt {
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). Poprawne 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 wskazywanego przez zmienną punkt, można uzyskać dostęp jedynie do metod zdefiniowanych 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;
}
}
Rysunek 5.2. Błędne odwołanie do nieistniejącego w klasie Punkt pola z
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();
jest przez kompilator rozumiana jako:
punkt = (Punkt) new Punkt3D();
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.
Rzutowania można dokonać również w przypadku zmiennych już istniejących, np.:
Punkt3D punkt3D = new Punkt3D();
Punkt punkt = (Punkt) punkt3D;
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 typu 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 deklarujemy 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
W przypadku zmiennych obiektowych będzie zupełnie inaczej, co wynika z ich właściwości. Na listingu 5.5 widoczny jest przykład obrazujący wyniki rzutowania obiektów.
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("");
Punkt punkt = (Punkt) punkt3D1;
System.out.println("punkt");
System.out.println("x = " + punkt.x);
System.out.println("y = " + punkt.y);
System.out.println("");
Punkt3D punkt3D2 = (Punkt3D) punkt;
System.out.println("punkt3D2");
System.out.println("x = " + punkt3D2.x);
System.out.println("y = " + punkt3D2.y);
System.out.println("z = " + punkt3D2.z);
}
}
Tworzymy obiekt klasy Punkt3D i przypisujemy go zmiennej punkt3D1 (Punkt3D punkt3D1
= new Punkt3D();) oraz ustalamy wartości jego pól x, y, z na 10, 20 i 30. Następnie
wyświetlamy te informacje na ekranie. Dalej tworzymy zmienną punkt klasy Punkt i za
pomocą techniki rzutowania przypisujemy jej obiekt, na który wskazuje punkt3D1
(Punkt punkt = (Punkt) punkt3D1;). Zawartość pól wyświetlamy na ekranie. Oczywiście
ponieważ typem zmiennej punkt jest Punkt, mamy dostęp jedynie do pól x oraz y. Pole
z nie jest dostępne, nie można modyfikować ani odczytywać jego wartości.
To jednak nie wszystko. W kolejnym kroku dokonujemy jeszcze jednego rzutowania.
Obiekt wskazywany przez punkt przypisujemy zmiennej punkt3D2, która jest klasy
Punkt3D. Następnie wyświetlamy zawartość wszystkich pól. Te wartości to 10, 20 i 30.
Ostatecznie na ekranie pojawi się widok zaprezentowany na rysunku 5.4.
Jest już zatem jasne, że rzutowanie nie zmienia stanu obiektu. W przykładzie utworzony 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.
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;
Czy może on zostać wykonany? Pozostawmy ten przykład do przemyślenia, wrócimy
do niego na początku lekcji 24.
Rzutowanie na typ Object
W Javie wszystkie klasy dziedziczą bezpośrednio lub pośrednio po klasie Object. Nie ma
pod tym względem wyjątków. Nawet jeśli definicja nowej klasy bazowej wygląda tak
jak w dotychczas przedstawionych przykładach, czyli:
202
Java. Praktyczny kurs
public class nazwa_klasy {
}
to kompilator potraktuje ten fragment kodu jako:
public class nazwa_klasy extends Object {
}
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 przeciążonych wersji metody println, żadna jednak nie przyjmuje argumentu typu
ArithmeticException. Nie istnieje też domyślna konwersja żadnego typu obiektowego
na typ String.
Wytłumaczenie jest na szczęście bardzo proste. Istnieje przeciążona wersja metody
println, która przyjmuje jako argument obiekt klasy Object. W klasie Object istnieje
natomiast metoda o nazwie toString, która zwraca opis obiektu w postaci ciągu znaków2.
Metoda println wywołuje natomiast metodę toString w celu uzyskania ciągu znaków.
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.
Rozdział 5.  Programowanie obiektowe. Część II
203
Jest to klasa MojaKlasa zawierająca publiczną metodę toString. Metoda ta nadpisuje
metodę toString z klasy Object. Jedynym jej zadaniem jest wyświetlenie na ekranie napisu widocznego na rysunku 5.6. W metodzie main tworzymy nowy obiekt klasy MojaKlasa
i przekazujemy go jako argument dla instrukcji System.out.println. Dzięki temu
można się naocznie przekonać, że metoda toString zostanie rzeczywiście wykonana.
Rysunek 5.6.
Efekt działania
przeciążonej metody
toString z klasy
MojaKlasa
Metodę tę można również wywołać jawnie, stosując konstrukcję:
System.out.println(mk.toString());
Warto jednak zauważyć, że w tej chwili jest wykorzystywana inna wersja metody
println. W poprzednim wypadku została zastosowana wersja przyjmująca jako argument 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);
Jeśli w MojaKlasa nie zostanie zdefiniowana metoda toString, to również będzie
można zastosować obiekt tej klasy jako argument metody println. Zostanie wtedy
wywołana metoda toString z klasy Object. Komunikat będzie jednak wówczas mało
zrozumiały. Taka klasa jest przedstawiona na listingu 5.7, a efekt jej działania widać na
rysunku 5.7 (ciąg znaków występujący po @ może być inny).
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
Ćwiczenia do samodzielnego wykonania
Ćwiczenie 23.1.
Popraw kod z listingu 5.1 tak, aby jego kompilacja była możliwa.
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.
Lekcja 24. Późne wiązanie i wywoływanie metod
klas pochodnych
W lekcji 23. pojawiły się pojęcia konwersji oraz rzutowania typów danych. Wiadomo
zatem już, że obiekt danej klasy można potraktować jak obiekt klasy nadrzędnej lub
podrzędnej i nie powoduje to straty żadnych zapisanych w nim informacji. W lekcji 24.
zostanie pokazane, jak w Javie wywoływane są metody oraz kiedy dochodzi do rzutowania typów; pojawi się tu też pojęcie polimorfizmu, jedno z głównych pojęć w programowaniu obiektowym.
Rzeczywisty typ obiektu
Wiadomo, że możliwe jest rzutowanie obiektu na typ bazowy (tzw. rzutowanie w górę),
czyli np. wykorzystywane w lekcji 23. rzutowanie obiektu klasy Punkt3D na klasę Punkt.
Zostanie ono opisane dokładniej już za chwilę. Powróćmy tymczasem do tematu
rzutowania w dół, który również pojawił się w poprzedniej lekcji. Rzutowanie w dół
to rzutowanie typu klasy bazowej na typ klasy pochodnej, tak jak w poniższym fragmencie kodu:
Punkt3D punkt3D = (Punkt3D) new Punkt();
punkt3D.z = 10;
Taką konstrukcję można zapisać również przy użyciu dodatkowej zmiennej klasy Punkt:
Rozdział 5.  Programowanie obiektowe. Część II
205
Punkt punkt = new Punkt();
Punkt3D punkt3D = (Punkt3D) punkt;
punkt3D.z = 10;
Powstaje tu obiekt klasy Punkt. W pierwszym przypadku jest on bezpośrednio rzutowany na klasę Punkt3D i przypisywany zmiennej punkt3D, natomiast w drugim najpierw
jest przypisywany zmiennej punkt klasy Punkt, a dopiero potem zawartość tej zmiennej
jest rzutowana na klasę Punkt3D i przypisywana zmiennej punkt3D. W obu przypadkach
zmienna punkt3D, której typem jest Punkt3D, wskazuje zatem na obiekt klasy Punkt.
Schematycznie zostało to zobrazowane na rysunku 5.8.
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 przypisania. 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;
}
}
Ten program jest syntaktycznie (składniowo) poprawny. Wolno dokonywać rzutowania
klasy Punkt na klasę Punkt3D (por. listing 5.5), program w żadnym wypadku nie zostanie
jednak poprawnie wykonany. Kompilator nie może „zakładać” złej woli programisty
i „nie wie”, że w rzeczywistości punkt3D wskazuje na obiekt, w którym brakuje pola z,
czyli obiekt nieprawidłowej klasy. Jednak w trakcie wykonania programu takie sprawdzenie nastąpi i zostanie zgłoszony błąd (wyjątek) ClassCastException. Jest on widoczny
na rysunku 5.9. Oczywiście wyjątek ten można przechwycić (por. lekcje z rozdziału 4.).
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 wskazuje 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 generowany 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.
Typ obiektu a rzutowanie w górę
Zaprezentowane przed chwilą sprawdzanie rzeczywistego (a nie deklarowanego) typu
obiektu w trakcie działania programu to właśnie polimorfizm, który jest tematem
nadrzędnym bieżących lekcji. Polimorfizm jest nazywany także późnym wiązaniem
(ang. late binding), wiązaniem czasu wykonania (ang. runtime binding) czy wiązaniem dynamicznym (ang. dynamic binding). W przypadku rzutowania w dół, omawianego w poprzednim podpunkcie, polimorfizm uniemożliwia wykonanie niedozwolonych operacji. O wiele bardziej użyteczny jest jednak przy rzutowaniu w górę, czyli
na klasę nadrzędną.
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 listingu 5.9.
Rozdział 5.  Programowanie obiektowe. Część II
207
Listing 5.9.
public class MojaKlasa {
public String toString() {
return "MojaKlasa";
}
}
public class MojaDrugaKlasa extends MojaKlasa {
public String toString() {
return "MojaDrugaKlasa";
}
}
public class MojaTrzeciaKlasa extends MojaKlasa {
public String toString() {
return "MojaTrzeciaKlasa";
}
}
Konstrukcja tych klas nie powinna budzić żadnych wątpliwości. Wykorzystywane
jest tu typowe dziedziczenie oraz metoda toString w standardowej postaci. Pytanie
brzmi: co się stanie po utworzeniu obiektu klasy MojaDrugaKlasa lub MojaTrzeciaKlasa,
przypisaniu zmiennej typu MojaKlasa odniesienia do tego obiektu i wywołaniu metody
toString? Inaczej: co pojawi się na ekranie po wykonaniu przykładowego programu
widocznego na listingu 5.10?
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 referencję 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 ();
Wykonujemy następnie dwie instrukcje System.out.println, przekazując im w postaci
argumentów wartości zwrócone przez wywołanie metody toString obiektów mk1 i mk2.
Wydawać by się mogło, że skoro typem obu zmiennych jest MojaKlasa, to zostaną potraktowane tak, jakby były obiektami tej klasy, i na ekranie dwukrotnie pojawi się napis
MojaKlasa.
208
Java. Praktyczny kurs
Nic bardziej mylnego! W Javie wywołania metod są domyślnie polimorficzne, to znaczy, że skojarzenie obiektu z metodą jest wykonywane w trakcie działania programu. Tym
samym sprawdzeniu podlega rzeczywisty typ obiektu, którego metoda jest wywoływana. Ponieważ rzeczywistym typem obiektu dla mk1 jest MojaDrugaKlasa, a dla mk2 —
MojaTrzeciaKlasa, na ekranie pojawi się widok zaprezentowany na rysunku 5.10.
Rysunek 5.10.
Wynik polimorficznego
wywołania metody
toString z listingów 5.9
i 5.10
Widać wyraźnie, że zostały wywołane metody toString z klas MojaDrugaKlasa oraz
MojaTrzeciaKlasa. Warto zauważyć, że gdyby wiązanie, czyli skojarzenie wywołania
metody z obiektem, odbywało się już na etapie kompilacji (tak zwane wczesne wiązanie, ang. early binding), takie wywołanie nie mogłoby mieć miejsca. W trakcie kompilacji oba obiekty, mk1 i mk2, są dla kompilatora obiektami klasy MojaKlasa, więc przypisałby on im kod metody toString z klasy MojaKlasa. Dzięki wywołaniom polimorficznym
wiązanie następuje dopiero w trakcie działania programu i pod uwagę jest brany rzeczywisty typ obiektu. To jedna z podstawowych zasad programowania obiektowego,
bardzo użyteczna przy pisaniu rzeczywistych aplikacji.
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ę jedynie 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");
}
}
public class Circle extends Shape {
public void draw() {
System.out.println("Circle");
}
}
public class Triangle extends Shape {
public void draw() {
System.out.println("Triangle");
}
}
public class Rectangle extends Shape {
public void draw() {
System.out.println("Rectangle");
}
}
Rozdział 5.  Programowanie obiektowe. Część II
209
Struktura przedstawionych klas przypomina poprzednio omawiany przykład. Klasą
główną jest Shape (kształt), po niej dziedziczą klasy Triangle (trójkąt), Circle (okrąg)
i Rectangle (prostokąt). Każda z wymienionych klas ma zdefiniowaną własną metodę
draw, której zadaniem, zgodnie z tym, co zostało napisane powyżej, będzie wyświetlenie
na ekranie nazwy klasy.
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ą metodę 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();
Circle circle = new Circle();
Triangle triangle = new Triangle();
Rectangle rectangle = new Rectangle();
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 tworzymy bowiem obiekty klas Circle, Triangle oraz Rectangle i przekazujemy je jako
argumenty metodzie drawShape klasy Main. Wynik działania, zgodny z naszymi oczekiwaniami, 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 listingu 5.12, jeśli np. klasa Triangle z listingu 5.11 przyjmie pokazaną poniżej postać?
public class Triangle extends Shape {
}
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 zdefiniowaną 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 zdefiniowana 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 pochodnej. 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ę jednak 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");
}
}
public class B extends A {
public void f() {
System.out.println("Metoda f klasy B");
}
}
Rozdział 5.  Programowanie obiektowe. Część II
211
Na listingu są dwie klasy: A zawierająca prywatną metodę f oraz dziedzicząca po niej B
zawierająca publiczną metodę, również o nazwie f. Obie klasy uda się bez problemu skompilować. W tym miejscu pojawi się zapewne pytanie: czemu jest to możliwe? Otóż prawdą jest, że do metod prywatnych klasy bazowej nie można się odwoływać w klasach
potomnych (lekcja 17.). Aby się o tym szybko przekonać, wystarczy spróbować w metodzie f klasy B wywołać metodę f klasy nadrzędnej (A) za pomocą składni super, czyli
umieścić w niej instrukcję:
super.f();
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.
To z kolei ma poważne implikacje praktyczne, związane z polimorficznym wywoływaniem metod. Aby to sprawdzić, dopiszmy do klasy A metodę main zawierającą instrukcje:
A a = (A) new B();
a.f();
Deklarujemy tu zmienną a klasy A i przypisujemy jej odniesienie do nowo utworzonego
obiektu klasy B. Dokonujemy przy tym jawnego rzutowania na klasę A, chociaż — formalnie rzecz biorąc — nie jest to w tym miejscu konieczne. Identyczny skutek będzie
miało zastosowanie instrukcji w postaci:
A a = new B();
Klasa A przyjmie zatem postać widoczną na listingu 5.14.
Listing 5.14.
public class A {
private void f() {
System.out.println("Metoda f klasy A");
}
public static void main(String args[]) {
A a = (A) new B();
a.f();
}
}
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 widać, została wykonana metoda f klasy A.
Rysunek 5.13.
Niespodziewane
zachowanie klasy
z metodą prywatną
212
Java. Praktyczny kurs
Co się stało? Otóż gdyby obie metody f były publiczne, czyli metoda f z klasy B przesłaniałaby metodę f z klasy A, zachowanie programu okazałoby się zgodne z wcześniejszymi 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. Tymczasem 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 potomnych 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 konstrukcji kodu programów.
Ćwiczenia do samodzielnego wykonania
Ćwiczenie 24.1.
Popraw kod z listingu 5.8 tak, aby po wystąpieniu wyjątku nie następowało niekontrolowane 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 semigraficznych.
Ć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.
Lekcja 25. Konstruktory oraz klasy abstrakcyjne
Lekcja 25. jest poświęcona klasom abstrakcyjnym oraz problematyce zachowań konstruktorów w specyficznych sytuacjach związanych z wywołaniami polimorficznymi.
Okaże się zatem, co to są klasy i metody abstrakcyjne i jak je stosować w praktyce.
Znajdą się tu też informacje o kolejności wywoływania konstruktorów w hierarchii
klas, a także o problemach, jakie można napotkać podczas wywoływania w konstruktorach innych metod danej klasy.
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 tworzone obiekty klasy bazowej. W przypadku zwykłych klas nie można jednak nikomu zabronić 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 kluczowego 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 powodzeniem metodą abstrakcyjną. Zgodnie z powyższym opisem klasa Shape w takim wypadku 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();
skończy się komunikatem o błędzie: Shape is abstract; cannot be instantiated.
Co jednak ważniejsze, zadeklarowanie metody jako abstrakcyjnej wymusza jej redeklarację w klasie potomnej. Oznacza to, że każda klasa wyprowadzona (czyli dziedzicząca)
z klasy Shape musi zawierać metodę draw. Jeżeli w którejś z klas tej metody zabraknie,
programu nie uda się skompilować. Tak więc przykładowa klasa Triangle w postaci:
public class Triangle extends Shape {
}
spowoduje błąd kompilacji zaprezentowany na rysunku 5.14 (nie ma w niej bowiem definicji 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.
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ównież 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łaby 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ć zadeklarowana jako abstrakcyjna, jeżeli co najmniej jedna jej metoda jest abstrakcyjna. Wynika z tego, że nie ma żadnych przeciwwskazań, aby pozostałe metody nie były abstrakcyjne. 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";
}
}
public class Rectangle extends Shape {
public void draw() {
System.out.println("metoda draw z klasy Rectangle");
}
public String toString() {
return "metoda toString z klasy Rectangle";
}
}
public class Triangle extends Shape {
public void draw() {
System.out.println("metoda draw z klasy Triangle");
}
}
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
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 zestaw 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();
Triangle triangle = new Triangle();
Rectangle rectangle = new Rectangle();
System.out.println("Wywołanie metod draw:");
main.drawShape(triangle);
main.drawShape(rectangle);
System.out.println("");
System.out.println("Wywołanie metod toString:");
System.out.println(triangle.toString());
System.out.println(rectangle.toString());
}
}
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. Ponieważ 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.
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 zainicjowany. Sytuacja taka jest zilustrowana na listingu 5.17.
Listing 5.17.
public class A {
public A() {
System.out.println("Konstruktor klasy A");
}
}
public class B extends A {
public B() {
System.out.println("Konstruktor klasy B");
}
public static void main(String args[]) {
B obiekt = new B();
}
}
Klasa A zawiera jeden publiczny konstruktor, którego zadaniem jest wyświetlenie na ekranie 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 takiej 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 wywołany żaden konstruktor klasy bazowej, automatycznie zostanie wywołany domyślny
konstruktor klasy bazowej (konstruktorem domyślnym jest konstruktor bezargumentowy).
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 konstruktora, 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 Konstruktor 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ć wywoł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 umieszczony 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 zaprezentowana na listingu 5.18.
Listing 5.18.
public class A {
public A(int arg) {
System.out.println("Konstruktor klasy A");
}
}
public class B extends A {
public B() {
System.out.println("Konstruktor klasy B");
}
public static void main(String args[]) {
B obiekt = new B();
}
}
Klasa B nie zmieniła się w stosunku do wersji zaprezentowanej na listingu 5.17, zmodyfikowana została natomiast treść klasy A. Nie ma już konstruktora domyślnego, bezargumentowego, 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 zasygnalizowany błąd, który został zaprezentowany na rysunku 5.17.
218
Java. Praktyczny kurs
Rysunek 5.17. Brak konstruktora domyślnego w klasie bazowej
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 przypadku należałoby zastosować następującą konstrukcję:
public B() {
super(0);
System.out.println("Konstruktor klasy B");
}
Oczywiście wartość przekazana konstruktorowi klasy bazowej (0) jest przykładowa,
może to być dowolna wartość typu int.
Wykonajmy jeszcze przykład. Obiekty przedstawionych wcześniej klas A i B będą polami
dodatkowej klasy C i zostaną utworzone w konstruktorze klasy C. Będzie to sytuacja,
którą przedstawia listing 5.19.
Listing 5.19.
public class A {
public A() {
System.out.println("Konstruktor klasy A");
}
}
public class B extends A {
public B() {
System.out.println("Konstruktor klasy B");
}
}
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();
}
}
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 konstruktor tej klasy i zostanie wyświetlony napis Konstruktor klasy A. W kolejnym kroku powstaje obiekt klasy B. Klasa B dziedziczy jednak po A, więc w tym kroku zostaną wywołane 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
Wywoływanie metod w konstruktorach
Z lekcji 15. z rozdziału 3. wiadomo, że w konstruktorach można wywoływać metody
danej klasy. Trzeba jednak uważać, gdyż wywoływanie metod w konstruktorach w połączeniu z polimorfizmem może wprowadzić w pułapkę. Przyjrzyjmy się klasom przedstawionym na listingu 5.20.
Listing 5.20.
public class A {
public A() {
//System.out.println("Konstruktor klasy A");
f();
}
public void f() {
//System.out.println("A:f");
}
}
public class B extends A {
int dzielnik;
public B(int dzielnik) {
//System.out.println("Konstruktor klasy B");
this.dzielnik = dzielnik;
}
public void f() {
//System.out.println("B:f()");
double wynik = 1 / dzielnik;
System.out.println("1 / dzielnik to: " + wynik);
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.
Klasa B dziedziczy po klasie A. Ma ona jeden konstruktor przyjmujący argument typu
int. Wartość tego argumentu jest przypisywana polu typu int o nazwie dzielnik. Została
również zdefiniowana metoda f, która wykonuje dzielenie wartości 1 przez wartość
zapisaną w polu o nazwie dzielnik i wyświetla wynik tego dzielenia na ekranie. W metodzie main jest tworzony nowy obiekt klasy B. Konstruktorowi przekazywana jest wartość 1,
zatem pole dzielnik przyjmuje wartość 1. Następnie zostaje wywołana metoda f. Można
by się spodziewać, że metoda ta wykona dzielenie 1 / 1, zatem na ekranie pojawi się
napis 1 / dzielnik to: 1. Spróbujmy uruchomić taki program i sprawdźmy, co się stanie.
Zostało to przedstawione na rysunku 5.19.
Rysunek 5.19. Pułapka związana z polimorficznym wywoływaniem metod
To zapewne bardzo niemiła niespodzianka, ewidentnie powstał wyjątek klasy Arithmetic
Exception, co oznacza, że w metodzie f zostało wykonane dzielenie przez zero. Jak
to się jednak mogło stać, skoro — to nie ulega wątpliwości — konstruktor klasy B
otrzymał argument o wartości 1? Polu dzielnik musiała zostać przypisana wartość 1. Skąd
więc wyjątek? Aby sprawdzić, co się tak naprawdę stało, warto usunąć teraz z obu klas
komentarze występujące przy instrukcjach System.out.println oraz ponownie skompilować i uruchomić program. Pojawi się widok zaprezentowany na rysunku 5.20. Jak
widać, konstruktor klasy B wcale nie zdążył się wykonać, wyjątek nastąpił wcześniej.
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.
Rozdział 5.  Programowanie obiektowe. Część II
221
Rysunek 5.20. Konstruktor klasy B nie został do końca wykonany
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);
zostanie wykonany, ZANIM jeszcze polu dzielnik zostanie przypisana jakakolwiek
wartość. Pole dzielnik jest w tym momencie po prostu niezainicjowane. A jak wiadomo
z lekcji 13., niezainicjowane jawnie pole typu int ma wartość… 0. Dlatego w tym
przykładzie został wygenerowany wyjątek ArithmeticException — przecież przez zero
dzielić nie wolno.
Koniecznie należy więc pamiętać, że również w konstruktorach wywołania metod są
polimorficzne, czyli skojarzenie treści metody odbywa się w trakcie działania programu i brany jest pod uwagę rzeczywisty typ obiektu. To niestety może prowadzić do
trudnych do wykrycia błędów. Mechanizm powstawania takich błędów został przedstawiony w powyższym przykładzie.
Ćwiczenia do samodzielnego wykonania
Ć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ść wykonywania konstruktorów.
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 abstrakcyjne). W tej lekcji zostaną przedstawione interfejsy, które można traktować (w wersjach 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ślnych (o czym pod koniec lekcji)5. Interfejs deklaruje się za pomocą słowa kluczowego 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 interfejsu 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);
}
Przykładowy interfejs o nazwie Drawable zawierający deklarację tylko jednej metody
o nazwie draw został przedstawiony na listingu 5.21.
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).
Rozdział 5.  Programowanie obiektowe. Część II
223
pominięta, kompilator zgłosi błąd. To, że klasa ma implementować dany interfejs,
zaznacza się, wykorzystując słowo kluczowe implements, co schematycznie wygląda
następująco:
[specyfikator dostępu][abstract] class nazwa_klasy implements nazwa_interfejsu {
/*
…pola i metody klasy…
*/
}
Zatem aby przykładowa klasa MojaKlasa implementowała interfejs Drawable z listingu 5.21, trzeba napisać:
public class MojaKlasa implements Drawable {
}
Próba kompilacji takiej klasy skończy się… błędem kompilacji. Jest to widoczne na
rysunku 5.21.
Rysunek 5.21. Brak definicji metody zadeklarowanej w interfejsie
Zapomnieliśmy bowiem, że zgodnie z tym, co zostało napisane wcześniej, klasa, która
implementuje interfejs, musi zawierać implementację wszystkich jego metod6. W tym
przypadku jest to metoda draw. Zatem poprawna wersja klasy MojaKlasa będzie wyglądała tak, jak to zostało przedstawione na listingu 5.22. Oczywiście treść metody draw
może być dowolna.
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 musi 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ą.
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ść deklaracji 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");
}
}
public class Rectangle implements Drawable {
public void draw() {
System.out.println("metoda draw z klasy Rectangle");
}
}
public class Triangle implements Drawable {
public void draw() {
System.out.println("metoda draw z klasy Triangle");
}
}
Interfejsy a hierarchia klas
Przykład z listingu 5.23 pokazał, jak implementować jeden interfejs w wielu klasach,
jednak w stosunku do przykładów z poprzednich lekcji została zmieniona hierarchia
klas. Wcześniej wszystkie trzy klasy, Triangle, Rectangle oraz Circle, dziedziczyły
po klasie Shape, co pozwalało na stosowanie wywołań polimorficznych. Skorzystanie
z interfejsów na szczęście nic tu nie zmienia, czyli wszystkie trzy klasy nadal mogą
dziedziczyć po Shape i jednocześnie implementować interfejs Drawable. Taka sytuacja
jest przedstawiona na listingu 5.24.
Listing 5.24.
public class Shape {
public void draw() {
}
}
public class Circle extends Shape implements Drawable {
public void draw() {
System.out.println("metoda draw z klasy Circle");
}
}
public class Rectangle extends Shape implements Drawable {
public void draw() {
System.out.println("metoda draw z klasy Rectangle");
}
}
public class Triangle extends Shape implements Drawable {
Rozdział 5.  Programowanie obiektowe. Część II
225
public void draw() {
System.out.println("metoda draw z klasy Triangle");
}
}
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 implementacja metody draw, a w klasie bazowej nie. A więc nawet jeśli nie zapomnimy o napisaniu 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 zaprotestować.
Listing 5.25.
public class Main {
public void drawShape(Shape shape) {
shape.draw();
}
public static void main(String args[]) {
Main main = new Main();
Triangle triangle = new Triangle();
Rectangle rectangle = new Rectangle();
Circle circle = new Circle();
System.out.println("Wywołanie metod draw:");
main.drawShape(triangle);
main.drawShape(rectangle);
main.drawShape(circle);
}
}
Rysunek 5.22.
Próba wywołania
nieistniejącej metody
w klasie Shape
226
Java. Praktyczny kurs
Jakie jest rozwiązanie tego problemu? Otóż metoda bazowa również powinna implementować 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ą abstrakcyjną. 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 pochodne. Taka sytuacja została przedstawiona na listingu 5.26.
Listing 5.26.
public abstract class Shape implements Drawable {
}
public class Triangle extends Shape {
public void draw() {
System.out.println("metoda draw klasy Triangle");
}
}
public class Rectangle extends Shape {
public void draw() {
System.out.println("metoda draw klasy Rectangle");
}
}
public class Circle extends Shape {
public void draw() {
System.out.println("metoda draw klasy Circle");
}
}
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ą zawsze publiczne, statyczne oraz finalne, czyli trzeba im przypisać wartości już w momencie 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;
Rozdział 5.  Programowanie obiektowe. Część II
227
typ_pola2 nazwa_pola2 = wartość_pola2;
/*…*/
typ_polan nazwa_polan = wartość_polan;
}
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ę zapisu: ponieważ pola interfejsów są zawsze statyczne i finalne, przyjmuje się, że ich nazwy 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 definicja 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…*/
}
228
Java. Praktyczny kurs
Załóżmy, że w trakcie prac nad interfejsem Drawable (z listingu 5.21) wystąpiła konieczność jego unowocześnienia i dodania metody draw3D. Jej zadaniem ma być np. wyświetlanie 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 dokonać 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ącego 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");
}
}
Teraz bez najmniejszych problemów da się skompilować i uruchomić program z listingu 5.25 (w połączeniu z klasami z listingu 5.24, a także tymi z listingu 5.26) i będzie się
on zachowywał zupełnie tak samo jak w przypadku starej wersji interfejsu (z listingu
5.21). Jednocześnie nic też nie będzie stało na przeszkodzie, aby korzystać z nowych
możliwości. Zostało to zilustrowane w przykładzie widocznym na listingach 5.30 i 5.31.
Na pierwszym z nich znajduje się zmodyfikowana klasa Triangle zawierająca definicję
metody draw3D (klasy Shape, Rectangle i Circle należy pozostawić bez zmian, jak na
listingu 5.26), a na drugim program bazujący na rozwiązaniu z listingu 5.25, ale korzystający z możliwości, jakie daje najnowsza wersja interfejsu Drawable.
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[]) {
Rozdział 5.  Programowanie obiektowe. Część II
229
Main main = new Main();
Triangle triangle = new Triangle();
Rectangle rectangle = new Rectangle();
Circle circle = new Circle();
System.out.println("Wywołanie metod draw i draw3D:");
main.drawShape3D(triangle);
main.drawShape3D(rectangle);
main.drawShape(circle);
}
}
Klasa Triangle (listing 5.30) dziedziczy po Shape (listing 5.26), która z kolei jest abstrakcyjna 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 wersji 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 bazowej 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
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 listingu 5.25.
Ćwiczenia do samodzielnego wykonania
Ć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.
Lekcja 27. Wiele interfejsów
Wiadomo już, w jaki sposób tworzyć i implementować interfejsy w klasach. Zagadnienia te pojawiły się w lekcji 26. Kolejnym tematem, którym się zajmiemy, jest technika pozwalająca na implementację przez jedną klasę wielu interfejsów. Zostanie tu też
przedstawione, jak unikać niebezpieczeństw związanych z konfliktem nazw metod w interfejsach, a także to, że interfejsy, tak jak i inne klasy, podlegają regułom dziedziczenia. Okaże się również, że jeden interfejs może przejąć metody nawet z kilku innych.
Implementowanie wielu interfejsów
W Javie klasa potomna może dziedziczyć tylko po jednej klasie bazowej, nie ma więc
znanego m.in. z C++ wielodziedziczenia. Istnieje natomiast możliwość implementowania wielu interfejsów. Interfejsy, które mają być implementowane przez klasę, należy
wymienić po słowie implements i oddzielić je znakami przecinka. Schemat takiej konstrukcji wygląda następująco:
[public][abstract] class nazwa_klasy implements interfejs1, interfejs2, ... ,
interfejsN {
/*
Rozdział 5.  Programowanie obiektowe. Część II
231
…pola i metody klasy…
*/
}
Przykładowo: jeśli zostaną napisane dwa interfejsy, pierwszy o nazwie PierwszyInterfejs
definiujący metodę f i drugi o nazwie DrugiInterfejs definiujący metodę g, oraz klasa
MojaKlasa implementująca oba, wszystkie trzy klasy będą miały postać przedstawioną
na listingu 5.32.
Listing 5.32.
public interface PierwszyInterfejs {
public void f();
}
public interface DrugiInterfejs {
public void g();
}
public class MojaKlasa implements PierwszyInterfejs, DrugiInterfejs {
public void f(){
}
public void g(){
}
}
Klasa MojaKlasa musi oczywiście zawierać definicje wszystkich metod zadeklarowanych
w interfejsach PierwszyInterfejs i DrugiInterfejs. W tym przypadku są to metody f i g.
Pominięcie którejkolwiek z nich spowoduje rzecz jasna błąd kompilacji.
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();
}
public interface WyswietlaObraz {
public void wyswietl();
}
public class Radio implements WydajeDzwiek {
public void graj() {
//instrukcje metody graj
}
}
public class Telewizor implements WydajeDzwiek, WyswietlaObraz {
public void graj() {
//instrukcje metody graj
}
232
Java. Praktyczny kurs
public void wyswietl() {
//instrukcje metody wyswietl
}
}
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 jedynie 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ć bardziej uniwersalne. Gdyby taka możliwość nie istniała, dla telewizora musiałby powstać interfejs np. o nazwie WyswietlaObrazIWydajeDzwiek, w którym zostałyby zadeklarowane 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();
}
public interface DrugiInterfejs {
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 interfejsy? Z kodu pierwszego interfejsu wynika, że klasa MojaKlasa powinna mieć zdefiniowaną 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");
}
}
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);
}
public interface DrugiInterfejs {
public int f(double argument);
}
W pierwszym interfejsie została zadeklarowana metoda f przyjmująca jeden argument
typu int, natomiast w drugim metoda o takiej samej nazwie przyjmująca argument
typu double i zwracająca wartość typu int. Skoro tak, przykładowa klasa MojaKlasa
implementująca oba interfejsy będzie musiała mieć implementacje obu metod. Będzie
zatem wyposażona w przeciążone metody f (lekcja 14.). Może ona wyglądać np. tak,
jak to zostało przedstawione na listingu 5.37.
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;
}
}
Klasa ta implementuje dwa interfejsy o nazwach PierwszyInterfejs i DrugiInterfejs.
Zawiera również trzy przeciążone metody f. Pierwsza z nich nie zwraca żadnych wartości oraz nie przyjmuje żadnych argumentów. Druga metoda została wymuszona przez
interfejs PierwszyInterfejs, również nie zwraca żadnej wartości, przyjmuje natomiast argument typu int (tak jak zostało to zdefiniowane w interfejsie). Trzecia metoda została wymuszona przez interfejs DrugiInterfejs. Przyjmuje ona jeden argument
typu double oraz zwraca wartość typu int. Tak więc w tym przypadku kod jest również
w pełni poprawny i jednoznaczny (klasa MojaKlasa da się bez problemów skompilować).
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 {
234
Java. Praktyczny kurs
public void f();
}
public interface DrugiInterfejs {
public int f();
}
Odpowiedź na zadane powyżej pytanie brzmi: nie. Co by się bowiem stało, gdyby utworzona 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 rysunku 5.24.
Rysunek 5.24. Nieprawidłowa implementacja interfejsów w klasie MojaKlasa
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ć przeciwwskazań do implementacji metody f przyjmującej argument typu int i niezwracającej wyniku.
Rozdział 5.  Programowanie obiektowe. Część II
235
Listing 5.40.
public interface MojInterfejs {
public void f(int argument);
}
public class MojaDrugaKlasa extends MojaKlasa implements MojInterfejs {
//public void f(int argument) {
//}
}
public class MojaKlasa {
public int f(int argument) {
return 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 nieprawidł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 zawierał 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
}
Zatem z przykładowego interfejsu I1 zawierającego metodę f można wyprowadzić interfejs I2 zawierający metodę g. Taka sytuacja została przedstawiona na listingu 5.41.
Listing 5.41.
public interface I1 {
public void f();
}
public interface I2 extends I1 {
236
Java. Praktyczny kurs
public void g();
}
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
}
Jeśli zatem mamy dwa przykładowe interfejsy bazowe I1 i I2 zawierające metody f i g,
a chcemy utworzyć interfejs potomny I3, który odziedziczy wszystkie ich właściwości,
powinniśmy zastosować konstrukcję widoczną na listingu 5.43.
Listing 5.43.
public interface I1 {
public void f();
}
public interface I2 {
public void g();
}
public interface I3 extends I1, I2 {
public void h();
}
Oczywiście przykładowa klasa implementująca interfejs I3 będzie musiała mieć zdefiniowane wszystkie metody z interfejsów I1, I2 i I3, czyli metody f, g i h. Pominięcie
którejkolwiek z nich spowoduje błąd kompilacji. Przykładowa klasa MojaKlasa implementująca interfejs I3 została przedstawiona na listingu 5.44.
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
}
}
Przy korzystaniu z mechanizmu dziedziczenia w interfejsach należy pamiętać o możliwych konfliktach nazw, które były omawiane w poprzedniej części lekcji. Takie niebezpieczeństwo istnieje zarówno przy dziedziczeniu pojedynczym, jak i wielokrotnym. Na listingu 5.45 jest przedstawiony przykład nieprawidłowego dziedziczenia po
jednym interfejsie bazowym. Otóż w interfejsie I1 została zadeklarowana metoda f
niezwracająca wyniku, natomiast w interfejsie I2 metoda o nazwie f, ale zwracająca wynik
typu int. W takiej sytuacji interfejs I2 nie może dziedziczyć po I1, gdyż występuje konflikt nazw (próba kompilacji pliku z kodem interfejsu I2 zakończy się niepowodzeniem).
Listing 5.45.
public interface I1 {
public void f();
}
public interface I2 extends I1 {//Nieprawidłowo!
public int f();
}
Błąd pojawi się również w sytuacji przedstawionej na listingu 5.46 — tym razem dotyczy trzech interfejsów. Interfejs I1 zawiera bezargumentową metodę f o typie zwracanym 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();
}
public interface I3 extends I1, I2 {
public void h();
}
238
Java. Praktyczny kurs
Rysunek 5.25. Konflikt nazw przy dziedziczeniu interfejsów
Ćwiczenia do samodzielnego wykonania
Ć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 zapoznanie 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
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ą.
Tworzenie klas wewnętrznych
Klasa wewnętrzna (ang. inner class), jak sama nazwa wskazuje, to taka, która została
zdefiniowana we wnętrzu innej klasy7. Konstrukcja ta początkowo może wydawać się
nieco dziwna, w praktyce pozwala jednak na wygodne tworzenie różnych konstrukcji
programistycznych. Schematyczna deklaracja klasy wewnętrznej wygląda następująco:
[specyfikator dostępu] class klasa_zewnętrzna {
[specyfikator dostępu] class klasa_wewnętrzna {
/*
…pola i metody klasy wewnętrznej…
*/
}
/*
…pola i metody klasy zewnętrznej…
*/
}
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 nazwie 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 zawierają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).
240
Java. Praktyczny kurs
public void g() {
Inside ins = new Inside();
ins.f();
ins.liczba = 200;
ins.f();
}
public static void main(String args[]) {
new Outside().g();
}
}
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ę wykonywanie 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();
Ostatecznie w wyniku działania aplikacji na ekranie pojawi się widok przedstawiony
na rysunku 5.26. Widać więc wyraźnie, że w klasie zewnętrznej można bez problemu
odwoływać się do składowych klasy wewnętrznej.
Rysunek 5.26.
Odwołania do pól
i metod klasy
wewnętrznej
Kilka klas wewnętrznych
W jednej klasie zewnętrznej może istnieć dowolna liczba klas wewnętrznych, nie ma
w tym względzie ograniczeń. Dostęp do nich odbywa się w taki sam sposób, jaki zaprezentowano wyżej. Przykładowa klasa Outside, w której zostały zdefiniowane dwie klasy
wewnętrzne, została przedstawiona na listingu 5.49.
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();
}
}
Są tu trzy klasy, zewnętrzna Outside oraz dwie wewnętrzne: FirstInside i SecondInside.
W klasie FirstInside znajduje się jedno pole typu int o nazwie liczba oraz jedna
bezargumentowa metoda o nazwie f. Zadaniem tej metody jest wyświetlanie wartości
pola liczba. Klasa SecondInside jest zbudowana w sposób analogiczny do FirstInside,
zawiera jedno pole o nazwie liczba i typie double oraz jedną bezargumentową metodę
o nazwie f, której zadaniem jest wyświetlanie wartości pola liczba.
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 dyspozycji 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 rysunku 5.27.
242
Java. Praktyczny kurs
Rysunek 5.27.
Wykorzystanie dwóch
klas wewnętrznych
Składowe klas wewnętrznych
W dotychczasowych przykładach składowe (pola i metody) klas wewnętrznych były
oznaczone specyfikatorem dostępu public. Nie jest to jednak obligatoryjne, do klas wewnętrznych mają zastosowanie takie same zasady ustalania dostępu do składowych jak
w przypadku zwykłych klas. Dostęp może być więc publiczny, pakietowy, chroniony
bądź prywatny. Załóżmy, że istnieje klasa wewnętrzna Inside podobna do przedstawionej
na listingu 5.48, w której jednak pole liczba jest polem prywatnym, tak jak pokazano
na listingu 5.50.
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;
Rozdział 5.  Programowanie obiektowe. Część II
243
public void f() {
System.out.println("Inside:f liczba = " + liczba);
}
}
}
public class Main {
public static void main(String args[]) {
Outside out = new Outside();
out.ins.f();
out.ins.liczba = 200;
out.ins.f();
}
}
Klasa Outside ma konstrukcję bardzo podobną do wykorzystywanej we wcześniejszych
przykładach. Zawiera klasę wewnętrzną Inside oraz publiczne pole (klasy Inside) o nazwie ins. Pole to jest inicjowane już w momencie jego deklaracji. Klasa wewnętrzna
Inside zawiera jedno prywatne pole typu int o nazwie liczba, któremu podczas deklaracji jest przypisywana wartość 100, oraz jedną publiczną, bezargumentową metodę f
wyświetlającą wartość pola liczba na ekranie. Ważne jest podkreślenie, że pole liczba
jest prywatne. Obie klasy należy zapisać w pliku o nazwie Outside.java.
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 nowy 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.
Rysunek 5.28. Próba odwołania się do prywatnego pola klasy wewnętrznej
Umiejscowienie klasy wewnętrznej
W przedstawionych dotychczas przykładach klasa wewnętrzna była umieszczana zaraz na
początku kodu klasy zewnętrznej, ewentualnie za jej polami. Jest to najbardziej sensowne
i najczęściej spotykane umiejscowienie klas wewnętrznych, ale to nie jedyna możliwość. Klasa wewnętrzna może się również znaleźć w dowolnym miejscu między polami, między metodami, a także… wewnątrz metod (będzie to wtedy lokalna klasa
244
Java. Praktyczny kurs
wewnętrzna, ang. local inner class)8. Ta ostatnia sytuacja jest rzadziej spotykana, niemniej taka możliwość istnieje i warto wiedzieć, jak w praktyce traktować taki fragment 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 bezargumentową 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 kompilacji. 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 bezpo8
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.
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.
Dostęp do klasy zewnętrznej
Wiadomo już, że z klasy zewnętrznej jest pełny dostęp do składowych klasy wewnętrznej.
Okazuje się jednak, że ta relacja obowiązuje również w drugą stronę, to znaczy z klasy
wewnętrznej mamy dostęp do składowych klasy zewnętrznej. Co więcej, jest to dostęp
pełny, czyli można się odwoływać także do składowych prywatnych. Przykład ilustrujący to zagadnienie został przedstawiony na listingu 5.54.
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 =
ins.g(argument);
System.out.println("liczba =
}
public static void main(String
new Outside().f(200);
}
}
{
" + liczba);
" + liczba);
args[]) {
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ępnie 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.
246
Java. Praktyczny kurs
Rysunek 5.29.
Ilustracja odwołania
z klasy wewnętrznej
do prywatnego pola
klasy zewnętrznej
Ćwiczenia do samodzielnego wykonania
Ć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 dokonuj 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.
Lekcja 29. Rodzaje klas wewnętrznych
i dziedziczenie
Omówiliśmy już podstawy posługiwania się klasami wewnętrznymi, czyli takimi, które
są zdefiniowane wewnątrz innych klas. Lekcja 29. pozwoli na pogłębienie wiedzy na
ten temat. Przedstawione zostaną sposoby tworzenia i odwoływania się do obiektów klas
wewnętrznych w ciele klas od nich niezależnych, istniejące rodzaje klas wewnętrznych,
a także to, co się dzieje w przypadku dziedziczenia. Okaże się również, jak w tego typu
konstrukcjach stosować modyfikatory dostępu.
Obiekty klas wewnętrznych
Wiadomo już, że obiekty klas wewnętrznych można tworzyć w klasach zewnętrznych
— nic w tym dziwnego, skoro klasa zewnętrzna ma pełny dostęp do klasy wewnętrznej9.
9
Z wyjątkiem opisanej w poprzedniej lekcji sytuacji, kiedy dostęp do klasy wewnętrznej jest
ograniczony do zasięgu, w którym została ona zdefiniowana.
Rozdział 5.  Programowanie obiektowe. Część II
247
Co jednak zrobić, aby móc operować na obiekcie klasy wewnętrznej poza klasą zewnę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 niezależ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 pierwszego 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();
}
}
Klasa zewnętrzna Outside zawiera metodę getInside, która zwraca odniesienie do
obiektu klasy Inside. Obiekt ten jest tworzony wewnątrz tej metody, a referencja jest
zwracana za pomocą standardowej instrukcji return. Klasa wewnętrzna Inside zawiera
jedną metodę o nazwie g, wyświetlającą napis informujący o klasie, z której pochodzi.
Aby utworzyć teraz klasę Main, która skorzysta z obiektu klasy Inside, można użyć
sposobu przedstawionego na listingu 5.56.
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ępnie wywołujemy metodę getInside tego obiektu, która zwraca odniesienie do obiektu
klasy Inside, oraz wywołujemy metodę g tego obiektu. Po skompilowaniu i uruchomieniu 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
248
Java. Praktyczny kurs
Przedstawiony sposób dostępu do obiektu klasy wewnętrznej jest jak najbardziej poprawny, 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 pierwszej 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 deklarujemy 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.
Pozostał do omówienia drugi sposób tworzenia obiektów klas wewnętrznych, to znaczy
bezpośrednie powoływanie ich do życia w klasach niezależnych. Pytanie brzmi: co zrobić
w sytuacji, kiedy klasa zewnętrzna nie udostępnia żadnej metody zwracającej nowy
obiekt klasy wewnętrznej, tak jak zostało to przedstawione na listingu 5.58?
Listing 5.58.
public class Outside {
class Inside {
public void g() {
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 powoł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ływać 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();
}
}
Rodzaje klas wewnętrznych
Dotychczas zostało zaprezentowanych już kilkanaście przykładów z klasami wewnętrznymi. Łatwo zauważyć, że w ich definicjach nie pojawiały się żadne modyfikatory dostępu.
Wynikałoby z tego, że wszystkie stosowane do tej pory klasy wewnętrzne były klasami
pakietowymi. Tak jest w istocie. Brakowało jednak okazji, by się o tym przekonać, gdyż,
jak wiadomo (lekcja 17.), dla klas znajdujących się w tym samym katalogu, które nie zostały formalnie umieszczone w żadnym pakiecie, jest tworzony pakiet domyślny, dzięki
czemu mogą się one wzajemne do siebie odwoływać. Na tej zasadzie działały przykłady
z listingów 5.55 – 5.59. Jeśli jednak definicje klasy wewnętrznej i zewnętrznej umieścimy w oddzielnym pakiecie, sytuacja ulegnie diametralnej zmianie. Zostało to zobrazowane w przykładzie widocznym na listingu 5.60.
10
Istnieje jednak specyficzny typ klas wewnętrznych, dla którego ta zasada nie obowiązuje. Zostanie on
omówiony w lekcji 30.
250
Java. Praktyczny kurs
Listing 5.60.
package pakiet_klas;
public class Outside {
class Inside {
public void g() {
System.out.println("Inside:g()");
}
}
}
import pakiet_klas.*;
public class Main {
public static void main(String args[]) {
Outside out = new Outside();
Outside.Inside ins = out.new Inside();
ins.g();
}
}
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 wewnętrzna dla Outside, to klasa pakietowa, w związku z tym nie ma możliwości odwoływania się do niej z klasy Main. Dlatego też kolejna instrukcja metody main (tworzą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 uzyskania 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()");
}
}
W tym miejscu powraca problem klasyfikacji klas występujących w Javie. Z lekcji
17. wiemy, że klasy dzielą się na:
 publiczne,
 pakietowe,
 wewnętrzne.
Jest to oczywiście podział poprawny. Ponieważ jednak klasy wewnętrzne są definiowane
wewnątrz innych klas i można o nich myśleć jako o składowych tych klas, możliwe
jest używanie w stosunku do nich modyfikatorów dostępu. W związku z tym klasy
wewnętrzne mogą być:
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 zabronić 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();
}
}
public class Main {
public static void main(String args[]) {
Outside out = new Outside();
//poniższe trzy zapisy są nieprawidłowe
//w klasie Main nie ma dostępu do klasy Inside
//Outside.Inside ins = out.new Inside();
//ins.g();
//out.getInside().g();
//prawidłowo, ale bez praktycznego znaczenia
out.getInside();
}
}
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 zdefiniowana 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ć referencji 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ą.
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.
Dziedziczenie po klasie zewnętrznej
Kolejna kwestia, której znajomość jest konieczna przy wykorzystywaniu klas wewnętrznych, to sposób dziedziczenia. Otóż z klasy zewnętrznej można oczywiście bez żadnych problemów wyprowadzać klasy potomne. W klasie potomnej będzie też dostęp
do składowych klasy wewnętrznej, o ile oczywiście nie jest to klasa prywatna. Przykład
dziedziczenia po klasie zewnętrznej pokazano na listingu 5.62.
Listing 5.62.
public class Outside {
public class Inside {
public void g() {
System.out.println("Inside:g()");
}
}
public Inside getInside() {
return new Inside();
}
}
public class ExtendedOutside extends Outside {
public void h() {
System.out.println("ExtendedOutside:h()");
}
}
public class Main {
public static void main(String args[]) {
ExtendedOutside extOut = new ExtendedOutside();
extOut.h();
ExtendedOutside.Inside extIns1 = extOut.getInside();
extIns1.g();
ExtendedOutside.Inside extIns2 = extOut.new Inside();
extIns2.g();
}
}
Klasy Outside i Inside mają postać analogiczną do przedstawionej na listingu 5.61,
z tą różnicą, że Inside jest teraz klasą publiczną. Z klasy Outside została wyprowadzona klasa potomna o nazwie ExtendedOutside, w której została zdefiniowana metoda
o nazwie h. Oprócz tego jest jeszcze niezależna klasa Main operująca na obiektach wcześniej wymienionych klas. W tej klasie w metodzie main tworzymy obiekt extOut klasy
ExtendedOutside, a następnie wywołujemy jego metodę h. Ten fragment z pewnością
nie budzi żadnych wątpliwości, jest to typowa konstrukcja programistyczna.
Rozdział 5.  Programowanie obiektowe. Część II
253
W dalszej kolejności tworzymy pierwszy obiekt klasy Inside, wywołując metodę
getInside obiektu extOut. Obiekt extOut jest klasy ExtendedOutside, zatem wykorzystywana jest tu metoda getInside odziedziczona po klasie Outside. Zwróćmy też uwagę na
sposób deklaracji zmiennej extIns1: wykorzystujemy składnię ExtendedOutside.Inside,
a nie — jak we wcześniejszych przykładach — Outside.Inside. Jest to możliwe dzięki
temu, że klasa ExtendedOutside dziedziczy po Outside. Następnie wywołujemy metodę g
obiektu extIns1.
W kolejnym kroku tworzymy nowy obiekt klasy Inside, również wykorzystując obiekt
extOut klasy ExtendedOutside. Stosujemy przedstawioną na początku bieżącej lekcji
metodę wywołującą konstruktor klasy Inside poprzez obiekt klasy zewnętrznej. Zwróćmy
uwagę, że jest to sytuacja, w której ExtendedOutside można traktować jako klasę zewnętrzną dla Inside, wszystko dzięki dziedziczeniu po klasie bazowej Outside. Dlatego
też można zastosować powyższy sposób wywołania konstruktora, a także bez problemu
wywołać metodę g obiektu extIns2. Ostatecznie po uruchomieniu kodu pojawi się
widok zaprezentowany na rysunku 5.31.
Rysunek 5.31.
Ilustracja dostępu
do klasy wewnętrznej
z klasy pochodnej
Przed chwilą padło stwierdzenie, że klasę potomną od klasy zewnętrznej można traktować 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ą, publiczną 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 {
}
}
public class ExtendedOutside extends Outside {
//poniższe odwołanie jest nieprawidłowe, jeśli
//klasa Inside jest klasą prywatną
//Inside ins = new Inside();
}
Klasa Inside jest w tym przykładzie klasą prywatną, czyli można ją traktować jako prywatną 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.
254
Java. Praktyczny kurs
Ćwiczenia do samodzielnego wykonania
Ć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 chronionym. 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.
Lekcja 30. Klasy anonimowe i zagnieżdżone
Lekcja 30. kończy rozdział 5. poświęcony bardziej zaawansowanym technikom związanym z programowaniem obiektowym w Javie. Kontynuujemy w niej temat klas
wewnętrznych. Tym razem okaże się, że oferują one kilka niespotykanych i wręcz zaskakujących możliwości, bo czy nie jest zaskakująca możliwość utworzenia klasy,
która nie ma swojej nazwy, lub zadeklarowania klasy jako statycznej? Takie właśnie
tematy zostaną omówione na kilku najbliższych stronach. Na początek rzutowanie
obiektów na typy interfejsowe.
Interfejs jako typ zwracany
W lekcji 23. była mowa o technikach rzutowania obiektów na klasę bazową lub potomną.
Okazuje się jednak, że rzutować można również na typy interfejsowe. Dokładniej: jeśli dana klasa implementuje pewien interfejs, to jej obiekt można rzutować na typ tego interfejsu. Oczywiście w takiej sytuacji programista będzie miał jedynie dostęp do
składowych zdefiniowanych przez interfejs. Tak więc po schematycznej definicji klasy:
class nazwa_klasy implements interfejs {
/*…pola i składowe klasy…*/
}
Rozdział 5.  Programowanie obiektowe. Część II
255
można wykonać instrukcję:
interfejs obiekt = new nazwa_klasy();
lub (stosując jawną konwersję typów):
interfejs obiekt = (interfejs) new nazwa_klasy();
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 widocznym na listingu 5.64.
Listing 5.64.
public interface PierwszyInterfejs {
public int getLiczba();
public void setLiczba(int liczba);
}
public class MojaKlasa implements PierwszyInterfejs {
private int liczba;
public void setLiczba(int liczba) {
this.liczba = liczba;
}
public int getLiczba() {
return liczba;
}
public int setAndGet(int liczba) {
int temp = this.liczba;
this.liczba = liczba;
return temp;
}
public static void main(String args[]) {
MojaKlasa obj = new MojaKlasa();
obj.setLiczba(1);
int temp1 = obj.getLiczba();
int temp2 = obj.setAndGet(2);
PierwszyInterfejs pi = new MojaKlasa();
pi.setLiczba(3);
int temp3 = pi.getLiczba();
//int temp4 = pi.getAndSet(4);
}
}
W interfejsie PierwszyInterfejs zostały zadeklarowane dwie metody: getLiczba,
bezargumentowa zwracająca wartość typu int, oraz setLiczba przyjmująca wartość typu
int. Klasa MojaKlasa implementuje ten interfejs, zatem zawiera również definicje
metod setLiczba i getLiczba. Metoda setLiczba służy do ustawiania wartości prywatnego pola o nazwie liczba, a getLiczba do pobierania wartości tego pola. Dodatkowo
w klasie MojaKlasa została zdefiniowana metoda getAndSet, której zadaniem jest ustawianie wartości pola liczba oraz zwrócenie poprzedniej wartości tego pola.
256
Java. Praktyczny kurs
Wykonywanie kodu rozpoczyna się od metody main. Jej kod zaczyna się od zadeklarowania 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.
Kolejny blok instrukcji metody main rozpoczynamy od zadeklarowania zmiennej pi
typu PierwszyInterfejs i przypisania jej nowo utworzonego obiektu klasy MojaKlasa.
Jak wiemy, jest to możliwe dzięki temu, że klasa MojaKlasa implementuje interfejs
PierwszyInterfejs. Pamiętajmy tylko, że typem powstałego obiektu jest MojaKlasa,
a jest on jedynie rzutowany na typ PierwszyInterfejs (podobna sytuacja miała miejsce
podczas rzutowania obiektu danej klasy na klasę bazową lub pochodną, rzeczywisty
typ obiektu nigdy nie ulegał zmianie). Ponieważ w interfejsie PierwszyInterfejs są
zdefiniowane metody setLiczba oraz getLiczba, można je wywołać na rzecz obiektu pi.
Instrukcje:
pi.setLiczba(3);
int temp3 = pi.getLiczba();
są zatem poprawne i działają zgodnie z założeniami. Nie można natomiast wykonać
ujętej w komentarz instrukcji:
int temp4 = pi.getAndSet(4);
gdyż nie ma metody getAndSet w interfejsie PierwszyInterfejs. Usunięcie komentarza skończyłoby się błędem kompilacji.
Rzutowanie na interfejs nie musi oczywiście oznaczać konieczności tworzenia nowego
obiektu. Tak jak ma to miejsce w przypadku zwykłych typów klasowych, można również rzutować obiekt już istniejący, rzecz jasna o ile implementuje on dany interfejs.
Jeśli więc ogólna postać klasy jest następująca:
class nazwa_klasy implements interfejs {
/*…definicja klasy…*/
}
to prawidłowe będzie wykonanie poniższych instrukcji:
nazwa_klasy nazwa_zmiennej_klasowej = new nazwa_klasy();
interfejs nazwa_zmiennej_interfejsowej = [(interfejs)] nazwa_zmiennej_klasowej;
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 zestawem klas takim jak przedstawiony na listingu 5.64, możemy wykonać instrukcje dla
rzutowania jawnego:
MojaKlasa obj = new MojaKlasa();
PierwszyInterfejs pi = (PierwszyInterfejs) obj;
oraz dla rzutowania domyślnego:
Rozdział 5.  Programowanie obiektowe. Część II
257
MojaKlasa obj = new MojaKlasa();
PierwszyInterfejs pi = obj;
Obie formy są prawidłowe i mają takie samo znaczenie.
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);
}
public class MojaKlasa {
public static void main(String args[]) {
PierwszyInterfejs pi;
pi = new PierwszyInterfejs() {
private int liczba;
public void setLiczba(int liczba) {
this.liczba = liczba;
}
public int getLiczba() {
return liczba;
}
};
pi.setLiczba(100);
int temp = pi.getLiczba();
System.out.println(temp);
}
}
Przeanalizujmy, co tu się stało. Co właściwie zostało przypisane zmiennej pi? Już
pierwsza część tego przypisania powinna wzbudzić zdziwienie, bo wygląda ona tak,
jakby był wywoływany konstruktor klasy PierwszyInterfejs. Tymczasem Pierwszy
Interfejs jest… interfejsem, z pewnością nie można więc tworzyć obiektów takiej
klasy. Dalej jest jeszcze ciekawiej, ponieważ znajdują się tam deklaracje pól i metod.
Cała ta konstrukcja oznacza stworzenie obiektu klasy anonimowej implementującej
interfejs PierwszyInterfejs.
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.
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 (formalnie kończący instrukcję przypisania). Jest on niezbędny do prawidłowej kompilacji
kodu.
Skoro utworzyliśmy obiekt takiej klasy i przypisaliśmy go zmiennej pi typu Pierwszy
Interfejs, możemy za pośrednictwem metod setLiczba i getLiczba wykonywać operacje na polu liczba. Wywołujemy więc metodę setLiczba, przekazując jej w argumencie wartość 100, a następnie metodę getLiczba, przypisując zwrócony przez nią
wynik zmiennej temp typu int. Ostatecznie wyświetlamy wartość zmiennej temp na
ekranie, wykorzystując instrukcję System.out.println.
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.
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 przypomnieniu 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 anonimowej. W drugim przypadku zmienna obj2 jest typu MojaKlasa i obiekt przez nią wskazywany 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
Należy rozpatrzyć jeszcze jedną kwestię związaną z klasami anonimowymi, mianowicie
problem konstruktorów. Co bowiem zrobić w sytuacji, kiedy klasa bazowa, po której
dziedziczy klasa anonimowa, nie ma konstruktora domyślnego? Na szczęście nie trzeba
tworzyć żadnych skomplikowanych konstrukcji. Wystarczy przekazać odpowiedni argument podczas tworzenia klasy anonimowej. Zostało to zobrazowane na listingu 5.67.
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…
*/
}
}
Klasa MojaKlasa przedstawiona na listingu 5.67 nie ma bezargumentowego konstruktora
domyślnego, zawiera natomiast konstruktor przyjmujący jeden argument typu int.
Klasa anonimowa dziedzicząca po MojaKlasa musi mieć możliwość wywołania tego
konstruktora, trzeba jej więc przekazać argument typu int. Umieszcza się go w nawiasie okrągłym po nazwie klasy; należy zatem postępować tak, jakby wywoływany był
konstruktor klasy MojaKlasa, mimo iż w dalszych instrukcjach znajduje się definicja
klasy anonimowej.
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 przechowuje 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();
}
}
Klasa Zewnetrzna jest klasą zewnętrzną, natomiast Wewnetrzna to klasa wewnętrzna —
utworzona jednak przy użyciu słowa kluczowego static. Oznacza to, że można utworzyć obiekt tej klasy, nawet jeśli nie istnieje obiekt klasy Zewnetrzna. Taka sytuacja
ma miejsce w metodzie main. Powstaje tam obiekt klasy Wewnetrzna i jest przypisywany
zmiennej wewnetrzna, mimo że nie istnieje obiekt klasy zewnętrznej. Widać też wyraźnie,
że użyto zwykłej składni z wykorzystaniem operatora new, tak jak w przypadku typowych klas (pamiętajmy, że w przypadku klas wewnętrznych z lekcji 28. i 29. używana
była składnia specjalna).
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 wewnę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. Przekonamy się o tym, jeśli spróbujemy skompilować kod z listingu 5.69. Zamiast spodziewanych 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;
Rozdział 5.  Programowanie obiektowe. Część II
261
wewLiczba = 100;
System.out.println("Wewnetrzna:f()");
}
}
public void g() {
zewLiczba = 100;
//nieprawidłowo, nie ma dostępu do pola
//wewLiczba klasy Wewnetrzna
wewLiczba = 100;
//nieprawidłowo, nie ma dostępu do metody
//f klasy Wewnetrzna
f();
}
}
Rysunek 5.33. Nieprawidłowy dostęp do składowych klas
Interfejsy funkcjonalne i wyrażenia lambda
W praktyce programistycznej spotyka się sytuacje, kiedy pewnej metodzie trzeba przekazać fragment kodu wykonujący jakieś zadanie. Często jest tak np. w przypadku funkcji
bibliotecznych wykonujących sortowanie — potrzebują one informacji o tym, który
z dwóch dowolnych elementów jest mniejszy, a który większy. Jeżeli procedura sortująca
ma być uniwersalna, kod wykonujący takie porównanie musi być napisany przez programistę stosującego daną funkcję biblioteczną. Przyjrzyjmy się jednak całej koncepcji
na nieco prostszym przykładzie.
Załóżmy, że mamy klasę przechowującą długości przyprostokątnych trójkąta prostokątnego, która zawiera również metodę wyliczającą długość przeciwprostokątnej. Niech
klasa nazywa się po prostu Przyprostokatne, ma dwa pola typu double, p1 i p2, a także
konstruktor i metody setPrzyprostokatne oraz przeciwprostokatna. Ostatnia wymieniona
metoda ma zwracać długość przeciwprostokątnej w postaci wartości typu double. Kod tak
opisanej klasy Przyprostokatne mógłby wyglądać tak jak na listingu 5.70.
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ątnych 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.
Działanie nowej klasy można wypróbować za pomocą programu przedstawionego na listingu 5.71. W metodzie main tworzony jest nowy obiekt klasy Przyprostokatne, a w konstruktorze przekazywane są wartości 3 i 4. Następnie wywoływana jest metoda
przeciwprostokatna wykonująca obliczenie długości przeciwprostokątnej dla zadanych
danych, a wynik jej działania jest wyświetlany na ekranie (łatwo przewidzieć, że będzie to
wartość 5).
Listing 5.71.
public class Main {
public static void main(String args[]) {
Przyprostokatne p = new Przyprostokatne(3, 4);
System.out.println(p.przeciwprostokatna());
}
}
W przygotowanym kodzie nie ma nic nadzwyczajnego, zastosowane zostały różne techniki omawiane do tej pory. Metodę przeciwprostokatna można byłoby jednak przygotować w taki sposób, aby wykonywała dodatkowe działania na przyprostokątnych przed
wyliczeniem przeciwprostokątnej, przy czym to, jakie to będą działania, ma być ustalane przez programistę korzystającego z klasy Przyprostokatne, a nie przez twórcę tej
klasy. Być może potrzebne będą wyniki dla przedłużonych albo skróconych boków
trójkąta, może obie wielkości będzie trzeba pomnożyć albo podzielić. Nie chcemy żadnych ograniczeń.
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 rzeczywistoś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);
}
Teraz przed wykonaniem właściwego obliczenia wartości pól p1 i p2 są poddawane
przekształceniu, o którego charakterze decyduje metoda przeksztalc obiektu typu
Przeksztalcenia wskazywanego przez argument prz. Instrukcja return nie zmieniła się
przy tym w stosunku do poprzedniego przykładu, jednak (na co warto zwrócić uwagę)
operuje ona tym razem na zmiennych lokalnych metody przeciwprostokatna, a nie na
polach obiektu klasy Przyprostokatne (jak miało to miejsce w poprzednim przypadku).
Pierwotny stan obiektu nie zostanie zatem zmieniony.
To jeszcze nie koniec działań. Programista korzystający z najnowszej wersji naszej
klasy będzie musiał używać obiektów klasy Przeksztalcenia zawierających metodę
przeksztalc o zadanej sygnaturze (deklaracji). Skąd ma wiedzieć, jak ta metoda ma wyglądać? Trzeba byłoby przygotować wzorcową wersję klasy. Nie powinna ona jednak
zawierać implementacji metody przeksztalc, ale jedynie jej deklarację. Czyli można byłoby utworzyć klasę abstrakcyjną. Tylko czy aby na pewno chcemy wymuszać korzystanie z konkretnej klasy Przeksztalcenia, która będzie pełniła wyłącznie rolę pomocniczą (rolę opakowania dla metody przeksztalc)? Nie chodzi przecież o to, żeby
dostarczany był obiekt konkretnej klasy (dokładniej: pochodny od pewnej klasy abstrakcyjnej), ale o to, by dostarczony został obiekt zawierający konkretną metodę (nie
ma tak naprawdę znaczenia, jakiej będzie klasy, chodzi tylko o metodę). Istnienie metody
w obiekcie można wymusić za pomocą interfejsu. A więc najlepiej przygotować po prostu interfejs Przeksztalcenia (lub o innej nazwie). Mógłby on mieć postać przedstawioną
na listingu 5.73.
Listing 5.73.
public interface Przeksztalcenia{
public double przeksztalc(double arg);
}
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;
}
}
public class Main {
public static void main(String args[]) {
Przyprostokatne p = new Przyprostokatne(3, 4);
System.out.println(p.przeciwprostokatna(new Zwiekszaj()));
}
}
Przygotowana została tu pakietowa klasa Zwiekszaj, która implementuje interfejs
Przeksztalcenia i w związku z tym zawiera metodę przeksztalc. Ta metoda zwraca
przekazany jej argument typu double powiększony o wartość 5. W programie (klasa Main,
metoda main) tworzony jest nowy obiekt typu Przyprostokatne, a następnie wywoływana
jest jego metoda przeciwprostokatna. To takie samo działanie jak w przypadku przykładu z listingu 5.71. Wykorzystana została jednak przeciążona wersja metody, która
otrzymała w postaci argumentu nowy obiekt klasy Zwiekszaj. Tym samym przed obliczeniem długości przeciwprostokątnej wartości przyprostokątnych zostały zwiększone
o 5 (co wynika z działania metody przeksztalc z klasy Zwiekszaj).
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 typowych 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 opakowaniem 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 metodzie 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){
Rozdział 5.  Programowanie obiektowe. Część II
265
return arg - 1;
}
})
);
}
}
Przedstawiony zapis może wydawać się niezbyt czytelny, ale to wyłącznie kwestia przyzwyczajenia. To rodzaj wywołania kaskadowego (charakterystycznego dla stylu programowania funkcyjnego), w którym unika się tworzenia zbędnych zmiennych pomocniczych (dodatkowo można byłoby pozbyć się zmiennej p, co może być dobrym
ćwiczeniem do samodzielnego wykonania). Metoda println otrzymała w postaci argumentu wartość zwróconą przez wywołanie metody przeciwprostokatna obiektu wskazywanego przez zmienną p.
Argumentem metody przeciwprostokatna jest z kolei nowy obiekt klasy anonimowej
implementującej interfejs Przeksztalcenia. W związku z tym pojawi się definicja metody przeksztalc, która tym razem zwraca wartość przekazanego jej argumentu pomniejszoną o 1. Tym samym w efekcie na ekranie pojawi się długość przeciwprostokątnej
dla wartości przyprostokątnych zmniejszonych o 1. Gdyby całość rozpisać z użyciem
zmiennych pomocniczych, program wyglądałby tak jak na listingu 5.76 (dla początkujących programistów ta wersja będzie zapewne bardziej klarowna).
Listing 5.76.
public class Main {
public static void main(String args[]) {
Przyprostokatne p = new Przyprostokatne(3, 4);
Przeksztalcenia prz = new Przeksztalcenia(){
public double przeksztalc(double arg){
return arg - 1;
}
};
double wynik = p.przeciwprostokatna(prz);
System.out.println(wynik);
}
}
Zwróćmy tu jednak ponownie uwagę na kwestię, która została już zasygnalizowana.
Otóż jedynym celem, dla którego istnieje klasa bądź interfejs Przeksztalcenia, jest
opakowanie metody znajdującej się wewnątrz. To ta metoda jest istotna. Niestety nie
można przekazać metody jako argumentu, dlatego stosuje się przedstawione wyżej
nadmiarowe konstrukcje programistyczne (trzeba napisać sporo dodatkowego kodu, który
pełni tylko funkcję pomocniczą13). W Java 8, jeśli mamy do czynienia z interfejsem
funkcjonalnym (jak w powyższych przykładach), zapis można uprościć, korzystając z tzw.
13
To skądinąd charakterystyczna cecha Javy, która jest często przez praktyków określana jako język
„przegadany”. Dlatego też wiele zintegrowanych narzędzi programistycznych (NetBeans, Eclipse,
IntelliJ itp.) umożliwia automatyczne generowanie wielu typowych struktur.
266
Java. Praktyczny kurs
wyrażeń lambda (ang. lambda expressions). Takie wyrażenie można przedstawić w schematycznej postaci:
(typ1 arg1, typ2 arg2, … , typN argN) -> {treść metody}
i należy je umieścić zamiast całej definicji klasy anonimowej implementującej dany
interfejs. Przykładowo dla wersji z listingu 5.75 wyrażenie wyglądałoby jak poniżej:
(double arg) -> {return arg - 1;}
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;
}
);
}
}
Ćwiczenia do samodzielnego wykonania
Ć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 przyjmują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świetlającą dowolny napis na ekranie. Wywołaj tę metodę, nie tworząc żadnego obiektu.
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 przekształ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.
268
Java. Praktyczny kurs
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 danych, 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 programu. Przykładowo standardowy strumień wyjściowy jest reprezentowany przez obiekt
System.out, a wykonanie metody println tego obiektu powoduje, że dane domyślnie zostają 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ż komunikację między obiektami w obrębie jednej aplikacji. W tym rozdziale zostanie
jednak omówiona tylko komunikacja aplikacji ze światem zewnętrznym.
Lekcja 31. Standardowe wejście
Opisy podstawowych operacji wyjściowych, czyli wyświetlania informacji na ekranie
konsoli, pojawiły się już wielokrotnie; operacje te zostaną dokładniej przedstawione
w ramach kolejnej lekcji. Omówienie systemu wejścia-wyjścia rozpoczniemy od wprowadzania danych i standardowego strumienia wejściowego reprezentowanego przez
obiekt System.in. Tej tematyce zostanie poświęcona cała bieżąca lekcja. Zostaną w niej
przedstawione informacje, jak odczytywać dane wprowadzane przez użytkownika
z klawiatury.
Standardowy strumień wejściowy
Standardowy strumień wejściowy jest reprezentowany przez obiekt System.in, czyli
obiekt in zawarty w klasie System. Jest to obiekt typu InputStream, klasy reprezentującej strumień wejściowy. Metody udostępniane przez tę klasę są zebrane w tabeli 6.1. Jak
widać, nie jest to imponujący zestaw, niemniej jest to podstawowa klasa operująca na
strumieniu wejściowym.
270
Java. Praktyczny kurs
Tabela 6.1. Metody udostępniane przez klasę InputStream
Deklaracja metody
Opis
int available()
Zwraca przewidywaną liczbę bajtów, które mogą być pobrane
ze strumienia przy najbliższym odczycie.
void close()
Zamyka strumień i zwalnia związane z nim zasoby.
void mark(int
readlimit)
Zaznacza bieżącą pozycję w strumieniu.
boolean markSupported()
Sprawdza, czy strumień może obsługiwać metody mark i reset.
abstract int read()
Odczytuje kolejny bajt ze strumienia.
int read(byte[] b)
Odczytuje ze strumienia liczbę bajtów nie większą niż rozmiar tablicy
b. Zwraca rzeczywiście odczytaną liczbę bajtów.
int read(byte[] b,
int off, int len)
Odczytuje ze strumienia liczbę bajtów nie większą niż określona
przez len i zapisuje je w tablicy b, począwszy od komórki wskazywanej
przez off. Zwraca rzeczywiście przeczytaną liczbę bajtów.
void reset()
Wraca do pozycji strumienia wskazywanej przez wywołanie metody mark.
long skip(long n)
Pomija w strumieniu liczbę bajtów wskazywanych przez n. Zwraca
rzeczywiście pominiętą liczbę bajtów.
Widać wyraźnie, że odczyt bajtów ze strumienia można przeprowadzić za pomocą
jednej z metod o nazwie read. Przyjrzyjmy się metodzie odczytującej tylko jeden bajt.
Mimo że odczytuje ona bajt, zwraca wartość typu int. Jest tak dlatego, że zwracana
wartość zawsze zawiera się w przedziale 0 – 255 (to tyle, ile może przyjmować jeden
bajt). Tymczasem zmienna typu byte reprezentuje wartości „ze znakiem” w zakresie
od –128 do +1271. Spróbujmy zatem napisać prosty program, który odczyta wprowadzony z klawiatury znak, a następnie wyświetli ten znak oraz jego kod ASCII na
ekranie. Kod realizujący przedstawione zadanie jest widoczny na listingu 6.1.
Listing 6.1.
import java.io.*;
public class Main {
public static void main(String args[]) {
System.out.print("Wprowadź dowolny znak z klawiatury: ");
try{
char c = (char) System.in.read();
System.out.print("Wprowadzony znak to: ");
System.out.println(c);
System.out.print("Jego kod to: ");
System.out.println((int) c);
}
catch(IOException e){
System.out.println("Błąd podczas odczytu strumienia.");
}
}
}
1
Oczywiście reprezentowanie zakresu od 0 do 255 byłoby możliwe również za pomocą typów short i long.
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 strumień 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ć pojedyncze 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.
272
Java. Praktyczny kurs
Zadanie to będzie wymagało użycia dwóch klas pośredniczących. Klasą udostępniającą metodę readLine, która jest w stanie prawidłowo zinterpretować znaki przychodzące ze
strumienia, jest BufferedReader. Aby jednak można było utworzyć obiekt takiej klasy,
musi powstać obiekt klasy Reader lub klasy od niej pochodnej — w tym przypadku najodpowiedniejszy będzie InputStreamReader. Ponieważ nie ma takiego obiektu w aplikacji
(do dyspozycji mamy jedynie obiekt System.in typu InputStream), należy go utworzyć, wywołując odpowiedni konstruktor. Będzie to obiekt (strumień) pośredniczący
w wymianie danych. Do jego utworzenia potrzeba z kolei obiektu klasy InputStream,
a tym przecież dysponujemy. Dlatego też kod programu, który odczytuje linię tekstu ze
standardowego wejścia, będzie wyglądać tak, jak zostało to przedstawione na listingu 6.2.
Listing 6.2.
import java.io.*;
public class Main {
public static void main(String args[]) {
BufferedReader brIn = new BufferedReader(
new InputStreamReader(System.in)
);
System.out.println("Wprowadź linię tekstu zakończoną znakiem Enter:");
try{
String line = brIn.readLine();
System.out.print("Wprowadzona linia to: " + line);
}
catch(IOException e){
System.out.println("Błąd podczas odczytu strumienia.");
}
}
}
Pierwszym zadaniem jest utworzenie obiektu brIn klasy BufferedReader. Mamy tu do
czynienia ze złożoną instrukcją, która najpierw tworzy obiekt typu InputStreamReader,
wykorzystując obiekt System.in, i dopiero ten obiekt przekazuje konstruktorowi klasy
BufferedReader. Zatem konstrukcję:
BufferedReader brIn = new BufferedReader(
new InputStreamReader(System.in)
);
można również rozbić na dwie części:
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader brIn = new BufferedReader(isr);
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
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 rysunku 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ć program, 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.");
Warto przeanalizować ten przykład. Na początku deklarujemy zmienną line i przypisujemy jej pusty ciąg znaków. W pętli while sprawdzamy warunek, czy zmienna line jest
różna od ciągu znaków quit — jeśli tak, to pętla jest kontynuowana, a jeśli nie, to kończy
działanie. Zatem znaczenie jest następujące: dopóki line jest różne od quit, wykonuj
instrukcje z wnętrza pętli. Wewnątrz pętli przypisujemy natomiast zmiennej line
wiersz tekstu odczytany ze standardowego strumienia wejściowego oraz wyświetlamy
go na ekranie.
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 powinien zakończyć działanie, tymczasem działa nadal, co jest widoczne na rysunku 6.3.
274
Java. Praktyczny kurs
Rysunek 6.3.
Błąd w pętli while
uniemożliwia
zakończenie programu
Listing 6.3.
import java.io.*;
public class Main {
public static void main(String args[]) {
BufferedReader brIn = new BufferedReader(
new InputStreamReader(System.in)
);
System.out.println("Wprowadzaj linie tekstu. Aby zakończyć, wpisz quit.");
String line = "";
try{
while(line != "quit"){
line = brIn.readLine();
System.out.println("Wprowadzona linia to: " + line);
}
System.out.println("Koniec wczytywania danych.");
}
catch(IOException e){
System.out.println("Błąd podczas odczytu strumienia.");
}
}
}
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ł nienazwany 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śniejszych 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ównania zawartości obiektów. Dlatego program nie może działać poprawnie i wpada w nieskończoną pętlę (aby zakończyć jego działanie, trzeba wcisnąć na klawiaturze kombinację klawiszy Ctrl+C).
Koniecznie należy więc zapamiętać, że do porównywania obiektów nie używa się
operatorów porównywania! Zamiast tego należy skorzystać z metody equals. Jest ona
zadeklarowana w klasie Object i w związku z tym dziedziczona przez wszystkie klasy.
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 znakó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");
będzie wartość true, a wynikiem operacji:
napis1.equals("java");
będzie wartość false.
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*/
}
Jeśli wprowadzimy go do kodu z listingu 6.3, aplikacja zacznie poprawnie reagować
na polecenie quit — będzie się wydawać, że całość nareszcie działa prawidłowo. W zasadzie można byłoby się zgodzić z tym stwierdzeniem, gdyż program rzeczywiście działa
zgodnie z założeniami, tylko że teraz zawiera kolejny błąd, tym razem dużo trudniejszy do
wykrycia. Ujawni się on dopiero przy próbie przerwania pracy aplikacji. Spróbujmy
uruchomić program, a po wpisaniu testowej linii tekstu przerwijmy jego działanie
(w większości systemów należy wcisnąć kombinację klawiszy Ctrl+C). Efekt jest widoczny
na rysunku 6.4. Zapewne nie spodziewaliśmy się wyjątku NullPointerException…
Rysunek 6.4. Ukryty błąd w programie spowodował wygenerowanie wyjątku
Na przyczynę powstania tego błędu naprowadza linia tekstu poprzedzająca komunikat
maszyny wirtualnej, mianowicie: Wprowadzona linia to: null. Oznacza to bowiem
(spójrzmy na kod), że metoda readLine obiektu brIn zwróciła — zamiast ciągu znaków
— wartość null. Jest to standardowe działanie, metoda ta zwraca wartość null, kiedy
zostanie osiągnięty koniec strumienia, czyli kiedy nie ma w nim już żadnych danych
do odczytania. Co się dzieje dalej? Otóż wartość null jest przypisywana zmiennej line,
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 konstrukcji 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 zaprezentowany na rysunku 6.5.
Listing 6.4.
import java.io.*;
public class Main {
public static void main(String args[]) {
BufferedReader brIn = new BufferedReader(
new InputStreamReader(System.in)
);
System.out.println("Wprowadzaj linie tekstu. Aby zakończyć, wpisz quit.");
String line = "";
try{
while(!"quit".equals(line)){
line = brIn.readLine();
System.out.println("Wprowadzona linia to: " + line);
}
System.out.println("Koniec wczytywania danych.");
}
catch(IOException e){
System.out.println("Błąd podczas odczytu strumienia.");
}
}
}
3
Czyli inaczej stała znakowa (napisowa), stały ciąg znaków umieszczony w kodzie programu.
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 aplikacji 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 dopiero 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ł poprawnej 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 sposób został wykorzystany w programie z listingu 6.5. Zadaniem tego programu jest wczytanie liczby całkowitej oraz wyświetlenie wyniku mnożenia tej liczby przez wartość 2.
Listing 6.5.
import java.io.*;
public class Main {
public static void main(String args[]) {
BufferedReader brIn = new BufferedReader(
new InputStreamReader(System.in)
);
System.out.print("Wprowadź liczbę całkowitą: ");
String line = null;
try{
line = brIn.readLine();
}
4
Jak widać, metody statyczne, które były przedmiotem lekcji 18., przydają się w praktyce.
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ściowym. 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 tekstu za pomocą metody readLine obiektu brIn, nie zapominając o przechwyceniu ewentualnego wyjątku IOException. W przypadku wystąpienia takiego wyjątku wyświetlamy stosowną 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 oznacza 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. Deklarujemy 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 wypadku 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 zatem 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
Gdyby miała być wczytana liczba zmiennoprzecinkowa, należałoby do konwersji zastosować metodę parseDouble z klasy Double lub parseFloat z klasy Float, co jest doskonałym ćwiczeniem do samodzielnego wykonania.
Rozdział 6.  System wejścia-wyjścia
279
Ćwiczenia do samodzielnego wykonania
Ć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żliwiał 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 (zmiennoprzecinkowe).
Ćwiczenie 31.5.
Napisz klasę Main testującą działanie metod getInt oraz getDouble z ćwiczenia 31.4.
Lekcja 32. Standardowe wejście i wyjście
W lekcji 32. będą kontynuowane tematy, które pojawiły się wcześniej w tym rozdziale.
Omówimy klasę StreamTokenizer, która potrafi dzielić dane ze strumienia wejściowego
na jednostki leksykalne nazwane tokenami, co pozwala używać jej do wprowadzania danych do aplikacji. Zostanie też pokazane, jak zbudować program wykonujący działanie arytmetyczne oraz aplikację obliczającą pierwiastki równania kwadratowego o parametrach wprowadzanych z klawiatury. Przedstawione zostaną również bardziej
nowoczesne klasy Scanner i Console. Na końcu lekcji znajdzie się rozwiązanie problemu obsługi polskich znaków diakrytycznych w aplikacjach korzystających z konsoli
w systemie Windows.
Wykorzystanie klasy StreamTokenizer
W lekcji 31. zaprezentowano sposób, w jaki odczytuje się w aplikacji liczby wprowadzane ze standardowego strumienia wejściowego. Tym sposobem było wczytanie linii
tekstu za pomocą metody readLine z klasy BufferedReader oraz dokonanie konwersji
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 leksykalne, 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 poszczególne jednostki leksykalne (w powyższym przykładzie są to spacje), można zdefiniować 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.).
Jak to jednak w praktyce wykorzystać do wprowadzania liczb? Otóż klasa Stream
Tokenizer ma pole o nazwie nval, które zawiera wartość aktualnego tokena w postaci
liczby typu double, oczywiście o ile ten token jest liczbą. Niezbędne jest zatem rozpoznawanie sytuacji, kiedy token nie reprezentuje wartości liczbowej. W tym celu wystarczy zbadać stan pola ttype lub wartość zwróconą przez metodę nextToken. Możliwe
wartości są zebrane w tabeli 6.2.
Tabela 6.2. Wartości pola ttype klasy StreamTokenizer
Nazwa pola
Znaczenie
StreamTokenizer.TT_EOF
Osiągnięty został koniec strumienia.
StreamTokenizer.TT_EOL
Osiągnięty został koniec linii.
StreamTokenizer.TT_NUMBER
Token jest liczbą.
StreamTokenizer.TT_WORD
Token jest słowem.
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.*;
public class Main {
public static void main(String args[]) {
StreamTokenizer strTok = new StreamTokenizer(
new BufferedReader(
new InputStreamReader(System.in)
)
);
System.out.print("Wprowadź liczbę: ");
try{
strTok.nextToken();
5
W sposób analogiczny można wykorzystać metody klas Byte, Long, Short.
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;
}
double liczba = strTok.nval;
System.out.print(liczba + " * 2 = " + liczba * 2);
}
}
Tworzymy nowy obiekt klasy StreamTokenizer. W konstruktorze musimy przekazać
obiekt klasy BufferedReader6, stosujemy więc konstrukcję kaskadową podobną do
wykorzystywanej w przykładach z listingów 6.2 – 6.5. Zapis:
StreamTokenizer strTok = new StreamTokenizer(
new BufferedReader(
new InputStreamReader(System.in)
)
);
można byłoby również rozbić na ciąg instrukcji wykorzystujący dodatkowe zmienne:
InputStreamReader isr = InputStreamReader(System.in);
BufferedReader inbr = new BufferedReader(isr);
StreamTokenizer strTok = new StreamTokenizer(inbr);
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.
Po pobraniu tokena sprawdzamy, czy jest on wartością liczbową — informuje o tym
stan pola ttype obiektu wskazywanego przez strTok. Jeśli ta wartość jest równa wartości
TT_NUMBER zdefiniowanej w klasie StreamTokenizer7, token reprezentuje liczbę, w przeciwnym wypadku nie jest liczbą. Wykorzystujemy więc dalej instrukcję if do dokonania
porównania. Jeśli wprowadzona została liczba, będzie się ona znajdowała w polu nval8
obiektu strTok, przypisujemy zatem jego wartość zmiennej typu double o nazwie liczba.
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.
282
Java. Praktyczny kurs
Tej zmiennej używamy następnie do wykonania działania arytmetycznego, którego wynik
wyświetlamy na ekranie. Przykładowy efekt działania programu jest widoczny na rysunku 6.7.
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ż podawane dane będą poprawne. Takie rozwiązanie zostało przedstawione na listingu 6.7.
Listing 6.7.
import java.io.*;
public class Main {
public static void main(String args[]) {
StreamTokenizer strTok = new StreamTokenizer(
new BufferedReader(
new InputStreamReader(System.in)
)
);
System.out.print("Wprowadź liczbę: ");
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;
}
double liczba = strTok.nval;
System.out.print(liczba + " * 2 = " + liczba * 2);
}
}
Obiekt przypisywany zmiennej strTok jest tworzony dokładnie tak samo jak w poprzednim przykładzie. Po wyświetleniu w bloku try prośby o wprowadzenie liczby rozpoczyna 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
Rozdział 6.  System wejścia-wyjścia
283
swoje działanie i wartość ta zostanie przypisana zmiennej liczba. Na ekranie jest natomiast wyświetlany wynik mnożenia wartości zapisanej w zmiennej liczba przez 2.
Wprowadzanie danych w rzeczywistych aplikacjach
Wszystkie prezentowane dotychczas w tym rozdziale przykłady były raczej teoretyczne,
nie wykonywały żadnych praktycznych zadań. Spróbujmy tym razem napisać dwie
bardzo proste aplikacje, które będą realizowały konkretne zadania. Pierwsza z nich będzie
wykonywała mnożenie dwóch liczb, druga to obiecany w lekcji 7. z rozdziału 2. program obliczający pierwiastki równania kwadratowego o zadanych parametrach wprowadzanych z klawiatury. Na pewno nie są to bardzo skomplikowane zadania programistyczne. Przykład numer jeden jest zilustrowany na listingu 6.8.
Listing 6.8.
import java.io.*;
public class Main {
public static void main(String args[]) {
StreamTokenizer strTok = new StreamTokenizer(
new BufferedReader(
new InputStreamReader(System.in)
)
);
System.out.print("Wprowadź pierwszą liczbę: ");
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;
}
double pierwszaLiczba = strTok.nval;
System.out.print("Wprowadź drugą liczbę: ");
try{
while(strTok.nextToken() != StreamTokenizer.TT_NUMBER){
System.out.println("To nie jest poprawna liczba.");
System.out.print("Wprowadź drugą liczbę: ");
}
}
catch(IOException e){
System.out.print("Błąd podczas odczytu danych ze strumienia.");
return;
}
double drugaLiczba = strTok.nval;
double wynik = pierwszaLiczba * drugaLiczba;
284
Java. Praktyczny kurs
System.out.println(pierwszaLiczba + " * " + drugaLiczba + " = " + wynik);
}
}
Tworzymy obiekt strTok klasy StreamTokenizer sposobem opisanym kilka akapitów
wyżej, a następnie wyświetlamy prośbę o wprowadzenie pierwszej liczby. W pętli while
wywołujemy metodę nextToken tak długo, aż wprowadzony z klawiatury ciąg znaków
będzie reprezentował wartość liczbową; wartość ta zostaje przypisana zmiennej typu
double o nazwie pierwszaLiczba. Następnie wykonujemy identyczne czynności umożliwiające wprowadzenie drugiej liczby. Jedyną różnicą są komunikaty wyświetlane na
ekranie oraz przypisanie odczytanej liczby zmiennej drugaLiczba. Po odczytaniu obu
wartości pozostaje wykonanie mnożenia i wyświetlenie jego wyniku na ekranie. Przykładowy efekt działania programu jest widoczny na rysunku 6.8.
Rysunek 6.8.
Mnożenie dwóch liczb
Drugi przykład dotyczy obliczania pierwiastków równania kwadratowego. Jak wykonać
samo obliczenie, wiadomo już z lekcji 7., trzeba zatem dodać możliwość wprowadzania
współczynników równania z klawiatury. Kod realizujący to zadanie jest przedstawiony na listingu 6.9.
Listing 6.9.
import java.io.*;
public class Main {
public double pobierzLiczbe(StreamTokenizer strTok)
throws IOException {
while(strTok.nextToken() != StreamTokenizer.TT_NUMBER){
System.out.println("To nie jest poprawna liczba.");
System.out.print("Wprowadź poprawną liczbę: ");
}
return strTok.nval;
}
public void oblicz(double A, double B, double C) {
System.out.println ("Parametry równania:\n");
System.out.println ("A: " + A + " B: " + B + " C: " + C + "\n");
if (A == 0){
System.out.println ("To nie jest równanie kwadratowe: A = 0!");
}
else{
double delta = B * B - 4 * A * C;
if (delta < 0){
System.out.println ("Delta < 0");
Rozdział 6.  System wejścia-wyjścia
285
System.out.println ("To równanie nie ma rozwiązania w " +
"zbiorze liczb rzeczywistych.");
}
else if(delta == 0){
double wynik = -B / (2 * A);
System.out.println ("Rozwiązanie: x = " + wynik);
}
else if(delta > 0){
double wynik;
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);
}
}
}
public void start() {
StreamTokenizer strTok = new StreamTokenizer(
new BufferedReader(
new InputStreamReader(System.in)
)
);
double paramA = 0;
double paramB = 0;
double paramC = 0;
try{
System.out.print("Podaj parametr
paramA = pobierzLiczbe(strTok);
System.out.print("Podaj parametr
paramB = pobierzLiczbe(strTok);
System.out.print("Podaj parametr
paramC = pobierzLiczbe(strTok);
System.out.println("");
}
catch(IOException e){
System.out.println("Błąd podczas
return;
}
oblicz(paramA, paramB, paramC);
A: ");
B: ");
C: ");
odczytu strumienia.");
}
public static void main (String args[]) {
Main main = new Main();
main.start();
}
}
Jak widać, program został dosyć mocno rozbudowany w stosunku do pierwowzoru.
Instrukcje wykonujące właściwe obliczenia pierwiastków zostały przeniesione do metody
oblicz. Sama metoda obliczeń oczywiście się nie zmieniła, różnica jest taka, że metoda oblicz nie ma zapisanych na stałe parametrów A, B i C, tylko otrzymuje je w postaci
argumentów.
286
Java. Praktyczny kurs
Wprowadzanie danych odbywa się za pomocą klasy StreamTokenizer, tak jak we
wcześniej prezentowanych przykładach. Tym razem odczyt pojedynczej liczby następuje
jednak w metodzie pobierzLiczbe, nie ma bowiem sensu pisać tego samego kodu dla
wprowadzania wartości każdego z parametrów. Trzeba byłoby trzy razy napisać praktycznie identyczną pętlę while. W przykładzie z listingu 6.8, gdzie wprowadzane były tylko
dwie wartości, można było zdecydować się na taką metodę, ale tym razem zdecydowanie lepszym pomysłem jest właśnie zastosowanie dodatkowej metody odczytującej
dane wejściowe.
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 wymienione zmienne odzwierciedlają kolejne parametry równania kwadratowego. Wykonywanie 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 wczytywania 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
W pierwszej grupie istnieją więc metody hasNextInt, hasNextDouble, hasNextByte itd.
Wszystkie one zwracają wartość true, o ile w powiązanym strumieniu danych kolejną
jednostką leksykalną jest wartość danego typu prostego. Istnieje też metoda hasNext,
która zwraca wartość true, jeżeli w strumieniu istnieje jakakolwiek kolejna jednostka
Rozdział 6.  System wejścia-wyjścia
287
leksykalna. Dodatkowo dostępna jest metoda hasNextLine, która określa, czy w strumieniu 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 poprzednim akapicie, istnieje również metoda nextLine, która zwraca całą linię tekstu.
Zobaczmy, jak wykorzystać to w praktyce: napiszmy program analogiczny do prezentowanego w poprzedniej części lekcji, ale korzystający z klasy Scanner. Będzie wczytywał 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.*;
public class Main {
public static void main(String args[]) {
Scanner scanner = new Scanner(System.in);
System.out.print("Wprowadź wartość całkowitą: ");
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);
}
}
Na początku tworzymy nowy obiekt klasy Scanner i przypisujemy go zmiennej scanner.
Ponieważ argumentem konstruktora może być obiekt klasy InputStream, wykorzystujemy
bezpośrednio obiekt System.in:
Scanner scanner = new Scanner(System.in);
Następnie wyświetlamy prośbę o wprowadzenie wartości całkowitej i przechodzimy
do pętli while:
while(!scanner.hasNextInt()){
Jak widać, pętla zostanie zakończona, kiedy metoda hasNextInt obiektu zwróci wartość 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 informacja 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());
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 obsłudze konsoli (zawarta jest w pakiecie java.io). Dzięki niej możliwe jest pobranie odwoł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łaniem 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 wykonywać różne operacje, lub też wartość null, o ile konsola nie jest dostępna. Wspomniane 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;
public class Main {
public static void main (String args[]) {
Console con = System.console();
if(con == null){
System.out.println("Brak konsoli!");
}
else{
String line = con.readLine("Wprowadź tekst: ");
con.printf("Wprowadzony tekst: " + line);
}
}
}
Rozdział 6.  System wejścia-wyjścia
289
Tabela 6.3. Metody zdefiniowane w klasie Console
Deklaracja
Opis
void flush()
Opróżnia bufor i wyświetla wszystkie znajdujące się w nim dane.
Console format(String
fmt, Object… args)
Wyświetla dane sformatowane za pomocą ciągu formatującego fmt.
Console printf(String
format, Object… args)
Wyświetla dane sformatowane za pomocą ciągu formatującego format.
Reader reader()
Zwraca powiązany z konsolą obiekt typu Reader (używany do odczytu
danych).
String readLine()
Odczytuje wiersz tekstu.
String readLine(String
fmt, Object… args)
Wyświetla tekst zachęty (prompt) sformatowany za pomocą ciągu fmt,
a następnie wczytuje wiersz tekstu.
char[] readPassword()
Odczytuje wiersz tekstu (hasło), nie wyświetlając wprowadzanych
z klawiatury znaków.
char[] readPassword
(String fmt, Object…
args)
Wyświetla tekst zachęty (prompt) sformatowany za pomocą ciągu fmt,
a następnie wczytuje wiersz tekstu (hasło), nie wyświetlając
wprowadzanych z klawiatury znaków.
PrintWriter writer()
Zwraca powiązany z konsolą obiekt typu PrintWriter (używany
do zapisu danych).
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 zapisywane 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 metody readLine). Po odczytaniu danych są one ponownie wyświetlane za pomocą metody 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 sformatowanej 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 kolejnych 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);
O tym, że mamy do czynienia ze znacznikiem, informuje znak %. Możliwe do zastosowania znaczniki zostały przedstawione w tabeli 6.49.
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.
290
Java. Praktyczny kurs
Tabela 6.4. Znaczniki formatujące dla metody printf
Ciąg
Znaczenie
b lub B
Argument będzie traktowany jak wartość boolowska.
h lub H
Ciąg będący wynikiem wykonania funkcji skrótu (hash code) argumentu.
s lub S
Argument będzie traktowany jak ciąg znaków.
c lub C
Argument będzie traktowany jak znak.
d
Argument będzie traktowany jak wartość całkowita dziesiętna.
o
Argument będzie traktowany jak wartość całkowita ósemkowa.
x lub X
Argument będzie traktowany jak wartość całkowita szesnastkowa.
e lub E
Argument będzie traktowany jak wartość rzeczywista w notacji naukowej.
f
Argument będzie traktowany jak wartość rzeczywista dziesiętna.
g lub G
Argument będzie traktowany jak wartość rzeczywista dziesiętna w notacji zwykłej
lub naukowej w zależności od zastosowanej precyzji.
a lub A
Argument będzie traktowany jak wartość rzeczywista szesnastkowa.
t lub T
Argument będzie traktowany jak data i (lub) czas. Wymaga określenia dodatkowego
formatowania.
%
Znak %.
n
Znak nowego wiersza.
Jeżeli zatem w kodzie zostaną zdefiniowane przykładowe zmienne:
boolean zmienna1 = true;
int zmienna2 = 254;
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);
Wtedy zamiast ciągu %b zostanie podstawiona wartość zmiennej zmienna1, a pod %d —
wartość zmiennej zmienna2 (oczywiście zmienna con musi zawierać odwołanie do
konsoli).
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;
public class Main {
public static void main (String args[]) {
Console con = System.console();
if(con == null){
System.out.println("Brak konsoli!");
}
else{
int liczba = 50;
con.printf("Znak: %c%n", liczba);
Rozdział 6.  System wejścia-wyjścia
291
con.printf("Dziesiętnie: %d%n", liczba);
con.printf("Ósemkowo: %o%n", liczba);
con.printf("Szesnastkowo: %x%n", liczba);
}
}
}
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ążonych wersjach i umożliwiają wyświetlanie różnorodnych typów danych. Wszystkie
dostępne metody klasy PrintStream zostały zebrane w tabeli 6.5.
Tabela 6.5. Metody klasy PrintStream
Deklaracja
Opis
PrintStream append(char c)
Dodaje znak do strumienia.
PrintStream
append(CharSequence csq)
Dodaje do strumienia sekwencję znaków.
PrintStream
append(CharSequence csq,
int start, int end)
Dodaje do strumienia część sekwencji znaków wyznaczaną
przez indeksy start i end.
boolean checkError()
Opróżnia bufor oraz sprawdza, czy nie wystąpił błąd.
protected void clearError()
Zeruje status błędu.
void close()
Zamyka strumień.
void flush()
Powoduje opróżnienie bufora.
PrintStream format(Locale l,
String format, Object… args)
Zapisuje w strumieniu dane określone przez argumenty
args w formacie zdefiniowanym przez format, zgodnie
z ustawieniami narodowymi wskazanymi przez locale.
PrintStream format(String
format, Object… args)
Zapisuje w strumieniu dane określone przez argumenty args
w formacie określonym przez format.
void print(boolean b)
Wyświetla wartość typu boolean.
void print(char c)
Wyświetla znak.
void print(char[] s)
Wyświetla tablicę znaków.
void print(double d)
Wyświetla wartość typu double.
void print(float f)
Wyświetla wartość typu float.
void print(int i)
Wyświetla wartość typu int.
void print(long l)
Wyświetla wartość typu long.
void print(Object obj)
Wyświetla ciąg znaków uzyskany przez wywołanie metody
toString obiektu obj.
void print(String s)
Wyświetla ciąg znaków s.
292
Java. Praktyczny kurs
Tabela 6.5. Metody klasy PrintStream (ciąg dalszy)
Deklaracja
Opis
PrintStream printf(Locale l,
String format, Object… args)
Zapisuje w strumieniu dane określone przez argumenty args
w formacie zdefiniowanym przez format, zgodnie z ustawieniami
narodowymi wskazanymi przez locale.
PrintStream printf(String
format, Object… args)
Zapisuje w strumieniu dane określone przez argumenty args
w formacie zdefiniowanym przez format.
void println()
Wyświetla znak końca linii (powoduje przejście do nowej linii).
void println(boolean b)
Wyświetla wartość typu boolean oraz znak końca linii.
void println(char c)
Wyświetla znak zapisany w c oraz znak końca linii.
void println(char[] s)
Wyświetla tablicę znaków oraz znak końca linii.
void println(double d)
Wyświetla wartość typu double oraz znak końca linii.
void println(float f)
Wyświetla wartość typu float oraz znak końca linii.
void println(int i)
Wyświetla wartość typu int oraz znak końca linii.
void println(long l)
Wyświetla wartość typu long oraz znak końca linii.
void println(Object obj)
Wyświetla ciąg znaków uzyskany przez wywołanie
metody toString obiektu obj oraz znak końca linii.
void println(String s)
Wyświetla ciąg znaków s oraz znak końca linii.
protected void setError()
Ustawia strumień w stan błędu.
void write(byte[] buf,
int off, int len)
Zapisuje do strumienia liczbę bajtów wskazywaną przez len,
poczynając od miejsca tablicy buf wskazywanego przez off.
void write(int b)
Zapisuje bajt b do strumienia.
Polskie znaki w systemie Windows
Powróćmy teraz do problemu wyświetlania polskich znaków na konsoli w środowisku Windows. Wiemy już z rozdziału 2., z lekcji 5., oraz z własnego doświadczenia,
że w niektórych wersjach systemów (np. w Windows XP i Java 7) polskie znaki nie
będą wyświetlane poprawnie, jeśli nie wykona się dodatkowych czynności, takich jak
zmiana systemu kodowania znaków na konsoli oraz stosowanej czcionki10. Sposób
ten został opisany dokładnie we wspomnianej lekcji. Warto jednak znać rozwiązanie
ogólniejszego problemu — jak spowodować, aby program w Javie wyświetlał na konsoli
(standardowym wyjściu) znaki w określonym standardzie kodowania?
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 (Windows 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.
Rozdział 6.  System wejścia-wyjścia
293
reprezentowane w b-kodzie, z kolei na konsoli są wyświetlane w standardzie CP85212.
Z przekodowaniem znaków z CP1250 na standard Unicode na szczęście kompilator
(środowisko JDK) radzi sobie sam (można to regulować za pomocą opcji encoding
kompilatora javac). Cały problem sprowadza się więc do wyświetlenia ich przy użyciu
wybranej strony kodowej — w tym przypadku 852. Pomogą nam w tym klasy o nazwach
OutputStreamWriter oraz PrintWriter.
Klasa OutputStreamWriter oferuje dwa konstruktory:
OutputStreamWriter(OutputStream out)
OutputStreamWriter(OutputStream out, String enc)
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");
Niezbędne jest w tym miejscu również przechwycenie wyjątku UnsupportedEncoding
Exception, który pojawia się, kiedy nie jest możliwe zastosowanie wskazanej strony
kodowej. To jednak nie wszystko, klasa OutputStreamWriter oferuje bowiem jedynie
trzy metody write, które pozwalają na wysłanie do strumienia znaku, tablicy znaków
oraz łańcucha znaków w postaci obiektu klasy String. Nie jest więc odpowiednikiem
klasy PrintStream. Na szczęście używając obiektu klasy OutputStreamWriter, można
zbudować obiekt klasy PrintWriter, który zawiera wszystkie potrzebne metody (metody print oraz println z tabeli 6.5). Spójrzmy na listing 6.13, został na nim zobrazowany
sposób utworzenia i wykorzystania obiektu klasy PrintWriter wyświetlającego znaki
przy użyciu strony kodowej 852.
Listing 6.13.
import java.io.*;
public class Main {
public static void main (String args[]) {
PrintWriter outp = null;
try{
outp = new PrintWriter(
new OutputStreamWriter(System.out, "Cp852"), true
);
}
catch(UnsupportedEncodingException e){
System.out.println("Nie można ustawić strony kodowej Cp852.");
outp = new PrintWriter(new OutputStreamWriter(System.out), true);
}
System.out.println("Test polskich znaków, obiekt System.out:");
System.out.println("Małe litery: ąćęłńóśźż");
System.out.println("Duże litery: ĄĆĘŁŃÓŚŹŻ");
12
W polskiej wersji systemu Windows.
13
Dostępny, począwszy od JDK 1.4.
294
Java. Praktyczny kurs
System.out.println();
outp.println("Test polskich znaków, obiekt outp:");
outp.println("Małe litery: ąćęłńóśźż");
outp.println("Duże litery: ĄĆĘŁŃÓŚŹŻ");
}
}
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 konstruktora klasy PrintWriter jest nowo utworzony obiekt klasy OutputStreamWriter powią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ą.
Rozdział 6.  System wejścia-wyjścia
295
Ćwiczenia do samodzielnego wykonania
Ćwiczenie 32.1.
Zmodyfikuj program z listingu 6.7 tak, aby poprawnie reagował (tzn. nie wyświetlał kolejnej 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 wprowadzeniu 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.
Lekcja 33. System plików
Lekcja 33. jest poświęcona technikom pozwalającym operować na systemie plików.
Zawarte są w niej informacje na temat tego, jak tworzyć i usuwać pliki oraz katalogi.
Zaprezentowana zostanie bliżej klasa File i metody przez nią udostępniane, jak również
to, jak pobrać zawartość katalogu oraz jak usunąć katalog wraz z całą jego zawartością,
co będzie wymagać zastosowania techniki rekurencyjnej. Po przedstawieniu tych tematów będzie można omówić metody zapisu i odczytu plików, tym jednak zajmiemy się
dopiero w kolejnej lekcji.
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.
296
Java. Praktyczny kurs
Tabela 6.6. Metody udostępniane przez klasę File
Deklaracja metody
Opis
Wersja JDK
boolean canExecute()
Zwraca true, jeśli aplikacja może uruchomić dany plik.
1.6
boolean canRead()
Zwraca true, jeśli aplikacja może odczytywać dany plik.
1.0
boolean canWrite()
Zwraca true, jeśli aplikacja może zapisywać dany plik.
1.0
int compareTo(File
pathname)
Porównuje ścieżki dostępu do plików.
1.2
boolean createNewFile()
Jeśli nie istnieje plik określony przez bieżący obiekt File,
tworzy go.
1.2
static File create
TempFile(String
prefix, String suffix)
Tworzy pusty plik tymczasowy w systemowym katalogu
przeznaczonym dla plików tymczasowych. Nazwa powstaje
przy wykorzystaniu ciągów przekazanych w parametrach
prefix i suffix.
1.2
static File
createTempFile(String
prefix, String suffix,
File directory)
Podobnie jak dwie poprzednie metody, tworzy plik
tymczasowy, zostaje on jednak umieszczony w katalogu
wskazywanym przez argument directory.
1.2
boolean delete()
Usuwa plik lub katalog (jeśli polecenie dotyczy katalogu,
musi on być pusty).
1.0
void deleteOnExit()
Zaznacza, że plik ma zostać usunięty, kiedy maszyna
wirtualna będzie kończyć pracę.
1.2
boolean equals(Object
obj)
Zwraca true, jeśli obiekt obj reprezentuje taką samą
ścieżkę dostępu.
1.0
boolean exists()
Zwraca true, jeśli plik lub katalog istnieje.
1.0
File getAbsoluteFile()
Zwraca obiekt zawierający bezwzględną nazwę pliku lub
katalogu (wraz z pełną ścieżką dostępu).
1.2
String getAbsolutePath()
Zwraca bezwzględną ścieżkę dostępu do pliku lub
katalogu (bez nazwy pliku).
1.0
File getCanonicalFile()
Zwraca obiekt zawierający kanoniczną postać nazwy
pliku lub katalogu (wraz z pełną ścieżką dostępu).
1.2
String
getCanonicalPath()
Zwraca kanoniczną postać ścieżki dostępu do pliku
lub katalogu.
1.1
long getFreeSpace()
Zwraca ilość wolnego miejsca na partycji, na której
znajduje się plik lub katalog.
1.6
String getName()
Zwraca nazwę pliku (bez ścieżki dostępu).
1.0
String getParent()
Zwraca nazwę katalogu nadrzędnego.
1.0
File getParentFile()
Zwraca obiekt typu File wskazujący na katalog nadrzędny.
1.2
String getPath()
Zwraca nazwę bieżącego katalogu lub pliku w postaci
obiektu typu String.
1.0
long getTotalSpace()
Zwraca całkowitą ilość miejsca na partycji, na której
znajduje się plik lub katalog.
1.6
long getUsableSpace()
Zwraca dostępną dla danej maszyny wirtualnej ilość
miejsca na partycji, na której znajduje się plik lub katalog.
1.6
int hashCode()
Oblicza wartość funkcji skrótu dla danej ścieżki dostępu.
1.0
Rozdział 6.  System wejścia-wyjścia
297
Tabela 6.6. Metody udostępniane przez klasę File (ciąg dalszy)
Deklaracja metody
Opis
Wersja JDK
boolean isAbsolute()
Zwraca true, jeśli dana ścieżka dostępu jest ścieżką
bezwzględną.
1.0
boolean isDirectory()
Zwraca true, jeśli ścieżka dostępu wskazuje na katalog.
1.0
boolean isFile()
Zwraca true, jeśli ścieżka dostępu wskazuje na plik.
1.0
boolean isHidden()
Zwraca true, jeśli ścieżka dostępu wskazuje na ukryty
katalog lub plik.
1.2
long lastModified()
Zwraca czas ostatniej modyfikacji pliku lub katalogu.
1.0
long length()
Zwraca wielkość pliku w bajtach.
1.0
String[] list()
Zwraca zawartość katalogu w postaci tablicy obiektów
typu String.
1.0
String[] list
(FilenameFilter filter)
Zwraca listę plików i podkatalogów spełniających
kryteria wskazane przez argument filter.
1.0
File[] listFiles()
Zwraca zawartość katalogu w postaci tablicy obiektów
typu File.
1.2
File[] listFiles
(FileFilter filter)
Zwraca w postaci tablicy obiektów typu File zawartość
katalogu spełniającą kryteria wskazane przez filter.
1.2
File[] listFiles
(FilenameFilter filter)
Zwraca w postaci tablicy obiektów typu File zawartość
katalogu spełniającą kryteria wskazane przez filter.
1.2
static File[]
listRoots()
Wyświetla wszystkie „korzenie” (węzły główne) systemu
plików.
1.2
boolean mkdir()
Tworzy nowy katalog.
1.0
boolean mkdirs()
Tworzy nowy katalog z uwzględnieniem nieistniejących
katalogów nadrzędnych.
1.0
boolean renameTo
(File dest)
Zmienia nazwę pliku lub katalogu na wskazywaną przez
argument dest.
1.0
boolean setExecutable
(boolean executable)
Ustawia prawo (atrybut) wykonywalności (executable)
katalogu lub pliku (dla właściciela obiektu).
1.6
boolean setExecutable
(boolean executable,
boolean ownerOnly)
Ustawia prawo (atrybut) wykonywalności (executable)
pliku lub katalogu dla właściciela lub wszystkich
użytkowników.
1.6
boolean setLastModified
(long time)
Ustawia datę ostatniej modyfikacji pliku lub katalogu.
1.2
boolean setReadable
(boolean readable)
Ustawia prawo (atrybut) do odczytu (readable) katalogu
lub pliku (dla właściciela obiektu).
1.6
boolean setReadable
(boolean readable,
boolean ownerOnly)
Ustawia prawo (atrybut) do odczytu (readable) pliku lub
katalogu dla właściciela lub wszystkich użytkowników.
1.6
boolean setReadOnly()
Ustawia atrybut ReadOnly pliku lub katalogu.
1.2
boolean setWriteable
(boolean readable)
Ustawia prawo (atrybut) do odczytu (writeable) katalogu
lub pliku (dla właściciela obiektu).
1.6
298
Java. Praktyczny kurs
Tabela 6.6. Metody udostępniane przez klasę File (ciąg dalszy)
Deklaracja metody
Opis
Wersja JDK
boolean setWriteable
(boolean readable,
boolean ownerOnly)
Ustawia prawo (atrybut) do zapisu (writeable) do pliku
lub katalogu dla właściciela lub wszystkich użytkowników.
1.6
String toString()
Zwraca ścieżkę dostępu w postaci obiektu klasy String.
1.0
URI toURI()
Konwertuje ścieżkę dostępu reprezentowaną przez obiekt File
na zunifikowany identyfikator zasobów — obiekt typu URI.
1.4
URL toURL()
Konwertuje ścieżkę dostępu reprezentowaną przez obiekt
File na obiekt klasy URL. Metoda przestarzała, o ile to
możliwe, nie należy z niej korzystać.
1.2
Pobranie zawartości katalogu
W celu poznania zawartości danego katalogu można skorzystać z metody list klasy
File. Zwraca ona tablicę obiektów typu String zawierającą nazwy plików i katalogów,
zatem napisanie programu, którego zadaniem będzie wyświetlenie zawartości jakiegoś
katalogu, dla nikogo nie powinno stanowić problemu. Taki przykładowy kod obrazujący wykorzystanie metody list jest widoczny na listingu 6.14.
Listing 6.14.
import java.io.*;
public class Main {
public static void main (String args[]) {
File file = new File(".");
String[] dirList = file.list();
for(int i = 0; i < dirList.length; i++){
System.out.println(dirList[i]);
}
}
}
Konstruujemy obiekt file klasy File, podając w konstruktorze ścieżkę dostępu do katalogu, 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 zależny od konwencji stosowanej w danym systemie operacyjnym, klasa File sama radzi
sobie z rozkodowaniem takiego zapisu. Następnie deklarujemy zmienną dirList i przypisujemy jej obiekt zwrócony przez wywołanie metody list. Ponieważ ten obiekt to w rzeczywistoś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 listingu 6.14 jest widoczny na rysunku 6.12.
Proste wyświetlenie zawartości katalogu z pewnością nie było zbyt skomplikowanym
zadaniem, zauważmy jednak, że klasa File udostępnia również drugą, przeciążoną
metodę list, która daje większe możliwości. Pozwala ona bowiem na pobranie nazw tylko tych plików i katalogów, które pasują do określonego wzorca. Przyjmuje parametr
typu FilenameFilter określający, które nazwy należy zaakceptować, a które odrzucić.
Rozdział 6.  System wejścia-wyjścia
299
Rysunek 6.12.
Wyświetlenie zawartości
katalogu przy użyciu
metody list klasy File
W rzeczywistości FilenameFilter to interfejs, w którym zadeklarowano tylko jedną metodę: accept. Aby zobaczyć, jak jego zastosowanie wygląda w praktyce, napiszmy
program, który będzie wyświetlał zawartość dowolnego katalogu pasującą do wzorca
określonego za pomocą wyrażenia regularnego16. Nazwa katalogu oraz wzorzec będą
wczytywane z wiersza poleceń podczas uruchamiania aplikacji. Spójrzmy na kod przedstawiony na listingu 6.15.
Listing 6.15.
import java.io.*;
import java.util.regex.*;
public class Main {
public static void main (String args[]) {
if(args.length < 1){
System.out.println("Wywołanie programu: Main katalog maska");
return;
}
File file = new File(args[0]);
String[] dirList;
if(args.length < 2){
dirList = file.list();
}
else{
try{
dirList = file.list((FilenameFilter) new MyFilenameFilter(args[1]));
}
catch(PatternSyntaxException e){
System.out.println("Nieprawidłowe wyrażenie regularne: " + args[1]);
return;
}
}
for(int i = 0; i < dirList.length; i++){
System.out.println(dirList[i]);
}
}
}
class MyFilenameFilter implements FilenameFilter {
Pattern pattern;
public MyFilenameFilter(String mask) {
pattern = Pattern.compile(mask);
}
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.
300
Java. Praktyczny kurs
public boolean accept(File dir, String name) {
Matcher matcher = pattern.matcher(name);
return matcher.matches();
}
}
Początkowo może wydawać się to trochę skomplikowane, w szczególności zastosowanie dodatkowej klasy MyFilenameFilter17, przeanalizujmy jednak wszystko po kolei,
począwszy od kodu klasy Main. Zaczynamy od sprawdzenia, czy podczas wywołania został podany przynajmniej jeden argument (por. lekcja 14.). Jeśli nie, należy poinformować o tym użytkownika, wyświetlając informację na ekranie, i zakończyć działanie
aplikacji. Jeśli jednak został podany przynajmniej jeden argument, zakładamy, że zawiera on nazwę katalogu, którego zawartość ma zostać wyświetlona, wykorzystujemy
go zatem jako argument konstruktora klasy File:
File file = new File(args[0]);
Przygotowujemy również zmienną dirList, której zostanie przypisana wynikowa tablica obiektów klasy String. Sprawdzamy następnie, czy aplikacji został przekazany
drugi argument wiersza poleceń — jeśli go nie ma (args.length < 2), wywołujemy
znaną nam już bezargumentową wersję metody list i przypisujemy wynik jej wykonania zmiennej dirList.
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])
);
powoduje wykonanie następujących operacji: utworzenie nowego obiektu klasy MyFile
nameFilter, rzutowanie tego obiektu na typ interfejsowy FilenameFilter (to rzutowanie formalnie nie jest konieczne; jak wiadomo z wcześniejszych lekcji, w miejscu, gdzie
oczekiwany jest obiekt danej klasy, można użyć obiektu klasy pochodnej), przekazanie go metodzie list oraz przypisanie obiektu zwróconego przez list zmiennej dirList.
Instrukcja ta została ujęta w blok try, gdyż jeśli drugi argument wywołania nie będzie prawidłowym wyrażeniem regularnym, zostanie zgłoszony wyjątek Pattern
SyntaxException. Jeśli jednak wyrażenie jest poprawne, zmiennej dirList zostanie
przypisana tablica zawierająca listę plików z danego katalogu, których nazwa jest zgodna
z wzorcem przekazanym aplikacji.
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.
Rozdział 6.  System wejścia-wyjścia
301
FilenameFilter. Skoro bowiem typem argumentu jest FilenameFilter, a Filename
Filter to klasa interfejsowa, której obiektów nie można tworzyć, typem argumentu
musi być klasa implementująca ten interfejs (lekcje 26. i 27.)18. Dlatego też została
napisana klasa MyFilenameFilter implementująca interfejs FilenameFilter. Implementacja tego interfejsu jest równoznaczna z koniecznością zdefiniowania metody o nazwie
accept przyjmującej dwa argumenty: pierwszy z nich to obiekt typu File, drugi to
sama nazwa pliku (lub katalogu).
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 naszym 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.
Klasa Pattern reprezentuje wyrażenie regularne, w klasie MyFilenameFilter umieściliśmy zatem pole tego typu. Obiekt klasy Pattern jest tworzony w konstruktorze poprzez wykorzystanie statycznej metody compile. Zwraca ona obiekt klasy Pattern obsługujący wyrażenie przekazane jako argument. W metodzie accept trzeba porównać
ciąg znaków (nazwę pliku lub katalogu) przekazany w argumencie name z wyrażeniem
reprezentowanym przez pattern. W tym celu należy powołać do życia dodatkowy
obiekt klasy Matcher. Wywołujemy więc metodę matcher obiektu pattern, przekazując
jej parametr name:
Matcher matcher = pattern.matcher(name);
w wyniku czego otrzymamy obiekt klasy Matcher. Zostaje on przypisany zmiennej
matcher. Jeśli teraz wywołamy metodę matches obiektu matcher, uzyskamy odpowiedź,
czy argument name jest zgodny z wyrażeniem regularnym reprezentowanym przez
pattern. Tak więc wynik zwrócony przez wywołanie:
matcher.matches()
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.
302
Java. Praktyczny kurs
Przykładowy efekt działania tego programu został przedstawiony na rysunku 6.13. Pierwszy 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 odnalezienie i poprawienie pozostanie jednak ćwiczeniem do samodzielnego wykonania (podpowiedź: 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
Tworzenie plików i katalogów
Klasa File zawiera metodę o nazwie createNewFile umożliwiającą utworzenie pliku21.
Co prawda pliki można również tworzyć w inny sposób, korzystając z klas strumieniowych, czym zajmiemy się w lekcji 34., niemniej warto znać i ten sposób. Aby skorzystać z createNewFile, należy najpierw utworzyć obiekt klasy File, podając w konstruktorze nazwę pliku, a następnie wywołać metodę createNewFile. Przykładowy program
wykonujący takie zadanie został przedstawiony na listingu 6.16.
Listing 6.16.
import java.io.*;
public class Main {
public static void main(String args[]) {
if(args.length < 1){
System.out.println("Wywołanie programu: Main nazwa_pliku");
return;
}
File file = new File(args[0]);
try{
if(file.createNewFile()){
System.out.println("Utworzony został plik: " + args[0]);
}
else{
System.out.println("Nie mogę utworzyć pliku: " + args[0]);
}
}
catch(IOException e){
System.out.println("Błąd wejścia-wyjścia: " + e);
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ą.
Rozdział 6.  System wejścia-wyjścia
303
return;
}
}
}
Nazwa pliku do utworzenia będzie podawana w wierszu poleceń przy wywoływaniu
programu, np.:
java Main test.txt
Pierwszą instrukcją jest zatem sprawdzenie, czy nazwa ta została rzeczywiście podana. Jeśli nie, pozostaje wyświetlić stosowny komunikat i zakończyć pracę, wykorzystują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.*;
public class mkdir {
public static void main(String args[]) {
if(args.length < 1){
System.out.println("Wywołanie programu: mkdir nazwa_katalogu");
return;
}
File file = new File(args[0]);
if(file.exists()){
System.out.println("Katalog o wskazanej nazwie już istnieje.");
return;
}
if(file.mkdirs()){
304
Java. Praktyczny kurs
System.out.println("Utworzony został katalog: " + args[0]);
}
else{
System.out.println("Nie mogę utworzyć katalogu: " + args[0]);
}
}
}
Usuwanie plików i katalogów
Skoro wiadomo już, jak tworzyć pliki i katalogi, należałoby także wiedzieć, w jaki sposób
je usuwać. Funkcja wykonująca to zadanie nazywa się, jak łatwo sprawdzić w tabeli 6.6,
po prostu delete. Usuwa ona plik lub katalog i zwraca wartość true, jeśli operacja ta
zakończy się sukcesem, lub false w przeciwnym wypadku. Wartość false zostanie zwrócona również wtedy, gdy wskazany obiekt nie istnieje. Prosty przykład zastosowania
metody delete został zaprezentowany na listingu 6.18.
Listing 6.18.
import java.io.*;
public class delete {
public static void main (String args[]) {
if(args.length < 1){
System.out.println("Wywołanie programu: delete nazwa");
return;
}
File file = new File(args[0]);
if(!file.exists()){
System.out.println("Nie ma takiego pliku lub katalogu.");
return;
}
if(!file.delete()){
System.out.println("Plik/katalog nie został usunięty.");
}
else{
System.out.println("Plik/katalog został usunięty.");
}
}
}
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ć skonstruowany inaczej. Można zastosować w tym celu technikę rekurencji, czyli wywoływania
Rozdział 6.  System wejścia-wyjścia
305
funkcji przez samą siebie22. Kod realizujący takie zadanie został przedstawiony na listingu 6.19.
Listing 6.19.
import java.io.*;
public class Delete {
public void delete(String name) {
File file = new File(name);
File[] dirList = file.listFiles();
for(int i = 0; i < dirList.length; i++){
if(dirList[i].isDirectory()){
delete(dirList[i].getPath());
}
dirList[i].delete();
}
file.delete();
}
public static void main (String args[]) {
if(args.length < 1){
System.out.println("Wywołanie programu: Delete katalog");
return;
}
if(!new File(args[0]).exists()){
System.out.println("Nie ma takiego katalogu.");
return;
}
Delete delete = new Delete();
delete.delete(args[0]);
}
}
Tym razem funkcja main zajmuje się jedynie sprawdzeniem, czy aplikacji został przekazany przynajmniej jeden argument, utworzeniem obiektu klasy Delete oraz wywołaniem 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 nazwę 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, wy22
Osoby nieznające technik rekurencyjnych powinny zapoznać się z dowolną książką opisującą podstawy
algorytmów.
306
Java. Praktyczny kurs
wołujemy usuwającą go funkcję delete (dirList[i].delete();). Na zakończenie
usuwamy katalog bieżący (file.delete();).
Ćwiczenia do samodzielnego wykonania
Ćwiczenie 33.1.
Napisz program, który wyświetli listę plików i podkatalogów katalogu o nazwie przekazanej 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ć numerowane 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 nienaruszoną strukturę katalogów.
Ćwiczenie 33.5.
Zmodyfikuj program z listingu 6.19 tak, aby nie było konieczności tworzenia obiektu
klasy Delete.
Lekcja 34. Operacje na plikach
W poprzedniej lekcji został omówiony sposób wykonywania na plikach i katalogach
operacji takich jak ich tworzenie i usuwanie. Niezbędna jest jednak również wiedza
o sposobach zapisu i odczytu danych w plikach. Tej właśnie tematyce jest poświęcona
niniejsza lekcja. Przedstawiona zostanie w tym miejscu dokładniej klasa RandomAccess
File oraz jej metody pozwalające na wykonywanie tego typu operacji, nie zostaną
również pominięte informacje o klasach strumieniowych, takich jak FileInputStream
i FileOutputStream. Okaże się także, jaka jest różnica między operacjami buforowanymi
i niebuforowanymi.
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.
Tabela 6.7. Metody klasy RandomAccessFile
Deklaracja metody
Opis
Od JDK
void close()
Zamyka strumień oraz zwalnia wszystkie związane z nim zasoby. 1.0
FileChannel getChannel()
Zwraca powiązany z plikiem unikalny obiekt typu FileChannel.
1.4
FileDescriptor getFD()
Zwraca deskryptor pliku powiązanego ze strumieniem.
1.0
long getFilePointer()
Zwraca aktualną pozycję w pliku.
1.0
long length()
Zwraca długość pliku.
1.0
int read()
Odczytuje kolejny bajt danych z pliku.
1.0
int read(byte[] b)
Odczytuje z pliku liczbę bajtów nie większą niż rozmiar
tablicy b i umieszcza je w tej tablicy. Zwraca rzeczywiście
odczytaną liczbę bajtów.
1.0
int read(byte[] b,
int off, int len)
Odczytuje z pliku liczbę bajtów nie większą niż wskazywana
przez len i zapisuje je w tablicy b, począwszy od komórki
wskazywanej przez off. Zwraca faktycznie przeczytaną
liczbę bajtów.
1.0
boolean readBoolean()
Odczytuje wartość typu boolean.
1.0
byte readByte()
Odczytuje wartość typu byte.
1.0
char readChar()
Odczytuje wartość typu char.
1.0
double readDouble()
Odczytuje wartość typu double.
1.0
float readFloat()
Odczytuje wartość typu float.
1.0
void readFully(byte[] b)
Odczytuje z pliku liczbę bajtów równą wielkości tablicy b.
1.0
void readFully(byte[] b,
int off, int len)
Odczytuje z pliku liczbę bajtów wskazywaną przez len i zapisuje 1.0
je w tablicy b, począwszy od komórki wskazywanej przez off.
int readInt
Odczytuje wartość typu int.
1.0
String readLine
Odczytuje linię tekstu.
1.0
long readLong
Odczytuje wartość typu long.
1.0
short readShort
Odczytuje wartość typu short.
1.0
int readUnsignedByte
Odczytuje wartość typu unsigned byte.
1.0
int readUnsignedShort
Odczytuje wartość typu unsigned short.
1.0
String readUTF
Odczytuje tekst w kodowaniu UTF-8.
1.0
void seek(long pos)
Zmienia wskaźnik pozycji w pliku na pos.
1.0
void setLength
(long newLength)
Ustawia rozmiar pliku na newLength.
1.2
int skipBytes(int n)
Pomija n bajtów.
1.0
308
Java. Praktyczny kurs
Tabela 6.7. Metody klasy RandomAccessFile (ciąg dalszy)
Deklaracja metody
Opis
Od JDK
void write(byte[] b)
Zapisuje tablicę bajtów b w pliku.
1.0
void write(byte[] b,
int off, int len)
Zapisuje w pliku len bajtów z tablicy b, począwszy od komórki
wskazywanej przez off.
1.0
void write(int b)
Zapisuje bajt b w pliku.
1.0
void writeBoolean
(boolean v)
Zapisuje w pliku wartość boolean w postaci jednego bajtu.
1.0
void writeByte(int v)
Zapisuje w pliku bajt v.
1.0
void writeBytes(String s)
Zapisuje w pliku ciąg znaków s w postaci sekwencji bajtów.
1.0
void writeChar(int v)
Zapisuje w pliku wartość typu char w postaci dwóch bajtów.
1.0
void writeChars(String s)
Zapisuje w pliku ciąg wskazywany przez s w postaci sekwencji
znaków.
1.0
void writeDouble(double v)
Konwertuje wartość v na typ long, korzystając z metody
doubleToLongBits z klasy Double, i tak powstałą wartość
zapisuje do pliku (8 bajtów).
1.0
void writeFloat(float v)
Konwertuje wartość v na typ int, korzystając z metody
floatToLongBits z klasy Float, i tak powstałą wartość
zapisuje w pliku (4 bajty).
1.0
void writeInt(int v)
Zapisuje w pliku wartość typu int w postaci czterech bajtów.
1.0
void writeLong(long v)
Zapisuje w pliku wartość typu long w postaci ośmiu bajtów.
1.0
void writeShort(int v)
Zapisuje w pliku wartość typu short w postaci dwóch bajtów.
1.0
void writeUTF(String
str)
Zapisuje w pliku ciąg znaków s w kodowaniu UTF-8.
1.0
Aby wykonywać operacje na plikach przy użyciu klasy RandomAccessFile, należy
utworzyć jej instancję, podając w konstruktorze ścieżkę dostępu do pliku oraz tryb jego
otwarcia. Ścieżka dostępu może być przedstawiona w postaci ciągu znaków String
lub jako obiekt klasy File. Do dyspozycji są więc dwa konstruktory:
RandomAccessFile(File file, String mode)
RandomAccessFile(String name, String mode)
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;
Rozdział 6.  System wejścia-wyjścia
309
 rwd — otwarcie do odczytu i zapisu wymuszające synchroniczny zapis
w rzeczywistym urządzeniu (np. na dysku twardym) przy każdej modyfikacji
danych w pliku.
Tryby rws i rwd oznaczają, że przy każdej modyfikacji danych mają one być od razu,
z pominięciem buforowania, zapisywane w urządzeniu. Dokładny opis tych trybów wykracza poza ramy niniejszej publikacji.
Odczyt danych z pliku
Skoro wiadomo już, jakie funkcje udostępnia klasa RandomAccessFile, można przystąpić do wykonywania operacji na plikach. Na początek odczyt danych. Do dyspozycji jest
wiele różnych metod operujących na różnych typach danych, nie ma jednak potrzeby,
aby wszystkie dokładnie omawiać, ich zastosowanie wydaje się jasne. Pojawi się za
to przykład pokazujący odczyt z pliku tekstowego. Będzie to program odczytujący
kolejne wiersze tekstu z pliku, którego nazwa została podana jako pierwszy argument
wywołania, i wyświetlający je na ekranie. Kod realizujący takie zadanie został zaprezentowany na listingu 6.20.
Listing 6.20.
import java.io.*;
public class Main {
public static void main (String args[]) {
if(args.length < 1){
System.out.println("Wywołanie programu: Main nazwa_pliku");
return;
}
File file = new File(args[0]);
}
}
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.");
}
310
Java. Praktyczny kurs
Na początku sprawdzamy, czy istnieje przynajmniej jeden argument wywołania — jeśli
nie, wyświetlamy odpowiednią informację na ekranie i za pomocą instrukcji return
kończymy działanie funkcji main, a tym samym całego programu. Jeśli argument jest
obecny, tworzymy zmienną file i przypisujemy jej referencję do nowego obiektu klasy
File. Konstruktorowi tego obiektu przekazujemy argument zapisany w tablicy args pod
indeksem 0. Sprawdzamy dalej, czy plik istnieje na dysku, wywołując metodę exists —
jeśli nie, kończymy działanie aplikacji.
Tym sposobem dotarliśmy do miejsca deklaracji zmiennej klasy RandomAccessFile
i utworzenia obiektu tej klasy. Konstruktorowi przekazujemy dwa argumenty: pierwszy to
obiekt klasy File, drugi — ciąg znaków określający tryb dostępu do pliku. Znak r określa
oczywiście tryb tylko do odczytu (zgodnie z opisem trybów podanym wyżej). Jeśli plik
wskazywany przez file nie istnieje, konstruktor zgłosi wyjątek FileNotFoundException,
dlatego też został ujęty w blok try. Kwestią do samodzielnego rozważenia pozostanie
tu następujący problem: czy wobec tego konieczne było wcześniejsze sprawdzenie
istnienia pliku za pomocą metody exists obiektu file, czy też był to tylko nadmiar
ostrożności?
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 przypisany 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 stwierdzenia, ż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 strumień, wywołując metodę close. Całość jest ujęta w blok try, gdyż podczas odczytu
może wystąpić wyjątek IOException.
Zapis danych do pliku
Klasa RandomAccessFile udostępnia również wiele funkcji pozwalających na zapis różnego typu danych w plikach. W rzeczywistości funkcje te zapisują jednak zwykłe ciągi
bajtów. Przykładowo funkcja writeInt nie zapisuje abstrakcyjnej wartości typu int, ale po
prostu cztery bajty danych reprezentujących wartość typu int. Z kolei funkcja readInt
odczytuje ze strumienia cztery bajty i składa z nich wartość typu int.
To, jak odczytywać dane z pliku, wiadomo już z poprzedniej części lekcji. Czas więc poznać sposób, w jaki dokonywany jest zapis. Napiszemy program, który będzie odczytywał 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.*;
public class Main {
Rozdział 6.  System wejścia-wyjścia
public static void main (String args[]) {
BufferedReader brIn = new BufferedReader(
new InputStreamReader(System.in)
);
String fileName = "";
if(args.length < 1){
System.out.print("Podaj nazwę pliku:");
try{
fileName = brIn.readLine();
}
catch(IOException e){
System.out.print("\nBłąd wejścia-wyjścia.");
return;
}
}
else{
fileName = args[0];
}
File file = new File(fileName);
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;
}
String line = "";
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;
}
}
}
311
312
Java. Praktyczny kurs
Tworzymy obiekt brIn klasy BufferedReader powiązany ze standardowym strumieniem
wejściowym, w dokładnie taki sam sposób jak w przypadku przykładów z lekcji 31. i 32.
Sprawdzamy dalej, czy w linii wywołania został podany przynajmniej jeden argument.
Jeśli tak, przypisujemy go zmiennej fileName. Jeżeli argument nie został podany, wyświetlamy na ekranie prośbę o wprowadzenie nazwy pliku, a odczytaną linię również
przypisujemy zmiennej fileName.
Następnie tworzymy obiekt klasy File, przekazując konstruktorowi wartość zapisaną
w fileName, oraz sprawdzamy, czy taki plik istnieje. Jeśli istnieje, kończymy wykonywanie programu (instrukcja return). Jeżeli jednak wskazanego pliku nie ma, tworzymy
nowy obiekt klasy RandomAccessFile, ustawiając parametr mode na rw, czyli w trybie do
zapisu i odczytu. Instrukcje te trzeba ująć w blok try, ponieważ gdy nie uda się utworzyć pliku, zostanie zgłoszony wyjątek FileNotFoundException.
Kiedy obiekt raf klasy RandomAccessFile zostanie utworzony, można przystąpić do
zasadniczej części zadania, czyli odczytu danych ze standardowego wejścia i zapisu
do pliku. Rozpoczynamy więc pętlę while, w której odczytujemy wprowadzane linie
tekstu za pomocą metody readLine obiektu brIn. Każda odczytana linia zostaje przypisana zmiennej line. Następnie sprawdzamy, czy w line jest ciąg znaków quit, co by
oznaczało, że użytkownik zakończył wprowadzanie danych, oraz czy line jest równe
null, co z kolei znaczyłoby, że nastąpił koniec strumienia. Jeżeli zachodzi którakolwiek
z tych sytuacji, kończymy pętlę. Jeśli jednak line jest różne i od quit, i od null, zapisujemy odczytaną linię tekstu w pliku przy użyciu metody writeBytes obiektu raf. Do
każdej linii dodawany jest znak końca wiersza \n. To konieczne, gdyż dane odczytane
za pomocą metody readLine takiego znaku nie zawierają (a więc bez tej czynności tekst
w pliku nie byłby podzielony na wiersze). Należy zwrócić uwagę, że zastosowany został
znak końca wiersza charakterystyczny dla systemów uniksowych. W Windowsie powinny to być dwa znaki: \r\n. Po zakończeniu pętli zamykamy strumień (plik), wykorzystując metodę close.
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łowego, 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 docelowego będą podawane przy wywołaniu programu. Klasę realizującą to zadanie nazwijmy Kopiuj; jej kod jest widoczny na listingu 6.22.
Listing 6.22.
import java.io.*;
public class Kopiuj {
public static void main (String args[]) {
if(args.length < 2){
System.out.println("Wywołanie programu: Kopiuj plik_źródłowy
plik_docelowy");
return;
}
Rozdział 6.  System wejścia-wyjścia
313
File sourceFile = new File(args[0]);
File destFile = new File(args[1]);
if(!sourceFile.exists()){
System.out.println("Plik
return;
}
if(destFile.exists()){
System.out.println("Plik
return;
}
RandomAccessFile rafSource
RandomAccessFile rafDest =
źródłowy nie istnieje.");
docelowy istnieje.");
= null;
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;
}
}
}
Tworzymy obiekty sourceFile oraz destfile, wykorzystując parametry przekazane
aplikacji. Obiekt reprezentowany przez sourceFile będzie powiązany z plikiem źródłowym, natomiast ten wskazywany przez destfile — z plikiem docelowym. Sprawdzamy następnie, czy istnieje plik źródłowy — jeśli nie, kończymy działanie programu.
Sprawdzamy też, czy istnieje plik docelowy — jeśli tak, również kończymy działanie
programu (tak, aby pliku nie nadpisać). Jeżeli jednak istnieje plik źródłowy oraz nie
istnieje plik docelowy, tworzymy obiekty rafSource i rafDest, które będą użyte podczas
właściwej operacji kopiowania. Obiekt rafSource jest tworzony w trybie tylko do odczytu, natomiast rafDest w trybie do odczytu i zapisu.
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
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ć sygnalizowany 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. Instrukcja 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 pliku 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ę jedynie 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 wykorzystanie 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.*;
public class Kopiuj {
public static void main (String args[]) {
/*
tutaj początek kodu z listingu 6.22
aż do instrukcji tworzących obiekty
rafSource i rafDest
*/
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;
}
}
}
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 zapisane 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 eksperymentem 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 programie metody read. Liczba odczytanych bajtów może wahać się od jednego do wielkoś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 odczytanych 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
316
Java. Praktyczny kurs
wielokrotnością rozmiaru bufora). W większości wypadków (szczególnie kiedy operuje się na plikach dyskowych) tak właśnie się dzieje i może się wydawać, że program
działa poprawnie. Nie wolno jednak czynić takiego założenia! Powtórzmy: zastosowana
wersja funkcji read ma pełne prawo odczytać ze strumienia dowolną liczbę bajtów z przedziału od jednego do rozmiaru tablicy. To, że w większości przypadków odczytuje pełny rozmiar bufora, nie oznacza, że zawsze tak będzie. Trzeba więc za każdym razem
kontrolować liczbę odczytanych bajtów.
Strumieniowe operacje na plikach
Tytuł może być nieco mylący, gdyż dotychczas opisywana klasa RandomAccessFile
również tworzy strumień, niemniej jest ona niezależna od hierarchii klas pochodzących
od InputStream i OutputStream, które pojawiły się przy omówieniu standardowego wejścia i wyjścia. Co więcej, zawiera metody odczytujące i zapisujące dane w plikach, podczas gdy tradycyjne operacje strumieniowe są rozdzielone. Inne klasy obsługują strumienie wejściowe, a inne wyjściowe. Spróbujmy wykonać kilka przykładów operujących
na klasycznych strumieniach.
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).
Tabela 6.8. Metody odczytujące dane w klasie FileInputStream
Deklaracja metody
Opis
int read()
Wersja
JDK
Odczytuje jeden bajt danych.
1.0
int read(byte[] b)
Odczytuje liczbę bajtów nie większą od długości tablicy b
i zapisuje je w tablicy b. Zwraca rzeczywiście odczytaną
liczbę bajtów.
1.0
int read(byte[] b,
int off, int len)
Odczytuje liczbę bajtów nie większą niż wskazywana przez len
i zapisuje je w tablicy b, począwszy od komórki wskazywanej
przez off. Zwraca rzeczywiście przeczytaną liczbę bajtów.
1.0
Rozdział 6.  System wejścia-wyjścia
317
Tabela 6.9. Metody odczytujące dane w klasie FileReader
Deklaracja metody
Opis
Wersja
JDK
int read()
Odczytuje pojedynczy znak.
1.0
int read(char[]
cbuf, int offset,
int length)
Odczytuje liczbę znaków nie większą niż wskazywana przez length
i zapisuje je w tablicy cbuf, począwszy od komórki wskazywanej
przez offset. Zwraca rzeczywiście przeczytaną liczbę znaków.
1.0
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ą funkcjonalność, np. BufferedInputStream lub BufferedReader. Ta technika jest już znana z przykładów obrazujących obsługę standardowego strumienia wejściowego. Napiszmy więc przykł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.*;
public class Main {
public static void main (String args[]) {
if(args.length < 1){
System.out.print("Wywołanie programu: Main nazwa_pliku");
return;
}
BufferedReader brIn = null;
try{
brIn = new BufferedReader(
new FileReader(args[0])
);
}
catch(FileNotFoundException e){
System.out.print("Nie mogę odnaleźć wskazanego pliku.");
return;
}
String line = "";
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;
}
}
}
Zasada działania aplikacji jest bardzo podobna do tej przedstawionej w przykładzie
z listingu 6.20, wykorzystany zostaje tylko obiekt innej klasy — BufferedReader zamiast
318
Java. Praktyczny kurs
RandomAccessFile. Obiekt brIn jest tworzony przez kaskadowe (podobnie jak w przykładach z lekcji 31.) wywołanie konstruktorów klasy BufferedReader oraz FileReader.
Konstruktorowi klasy FileReader jest przekazywany argument odczytany z wiersza poleceń. 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 plikó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).
Tabela 6.10. Metody zapisujące dane klasy FileOutputStream
Deklaracja metody
Opis
Wersja
JDK
void write(byte[] b)
Zapisuje w strumieniu tablicę bajtów b.
1.0
void write(byte[] b,
int off, int len)
Zapisuje w strumieniu len bajtów z tablicy b, począwszy
od komórki wskazywanej przez off.
1.0
void write(int b)
Zapisuje w strumieniu bajt b.
1.0
Tabela 6.11. Metody zapisujące dane klasy FileWriter
Deklaracja metody
Opis
Wersja
JDK
void write(char[] cbuf,
int off, int len)
Zapisuje w strumieniu len znaków z tablicy cbuf,
począwszy od komórki wskazywanej przez off.
1.0
void write(int c)
Zapisuje w strumieniu znak c.
1.0
void write(String str,
int off, int len)
Zapisuje w strumieniu len znaków z obiektu str,
począwszy od znaku o indeksie wskazywanym przez off.
1.0
Obie klasy mają po pięć przeciążonych konstruktorów. Trzy z nich są jednoargumentowe
i mogą przyjmować następujące argumenty:
 ciąg znaków zawierający nazwę pliku;
 obiekt klasy FileDescriptor;
 obiekt klasy File.
Rozdział 6.  System wejścia-wyjścia
319
Pozostałe konstruktory są dwuargumentowe. Pierwszy przyjmuje ciąg znaków oraz
wartość boolean, drugi obiekt klasy File i wartość boolean. W obu przypadkach drugi argument ustawiony na true oznacza, że dane mają być dopisywane na końcu pliku, a ustawiony na false — że mają być zapisywane od początku pliku (przy nadpisywaniu jego
wcześniejszej zawartości).
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 nastę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 wczytywał dane ze standardowego wejścia i zapisywał je do pliku. Taki program jest przedstawiony na listingu 6.25.
Listing 6.25.
import java.io.*;
public class Main {
public static void main (String args[]) {
if(args.length < 1){
System.out.print("Wywołanie programu: Main nazwa_pliku");
return;
}
BufferedReader brIn = new BufferedReader(
new InputStreamReader(System.in)
);
FileWriter fileWriter = null;
try{
fileWriter = new FileWriter(args[0], true);
}
catch(IOException e){
System.out.println("Nie mogę otworzyć wskazanego pliku.");
return;
}
String line = "";
try{
while(true){
line = brIn.readLine();
if("quit".equals(line) || line == null){
break;
}
line += "\n";
fileWriter.write(line, 0, line.length());
}
fileWriter.close();
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ę nieprawidł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 znaleźć 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.*;
public class Kopiuj {
public static void main (String args[]) {
if(args.length < 2){
System.out.println("Wywołanie programu: Kopiuj plik_źródłowy plik_docelowy");
return;
}
File sourceFile = new File(args[0]);
File destFile = new File(args[1]);
if(!sourceFile.exists()){
Rozdział 6.  System wejścia-wyjścia
321
System.out.println("Plik źródłowy nie istnieje.");
return;
}
if(destFile.exists()){
System.out.println("Plik docelowy istnieje.");
return;
}
BufferedInputStream source = null;
BufferedOutputStream dest = null;
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;
}
}
}
Zasada działania tego programu jest analogiczna do tej z przykładu przedstawionego
na listingu 6.22, z tą różnicą, że operuje się na innych klasach. Obiektem źródłowym
jest teraz obiekt source klasy BufferedInputStream, natomiast docelowym obiekt dest
klasy BufferedOutputStream. Tym razem można sobie pozwolić na kopiowanie bajt po
bajcie, gdyż wymienione klasy same „dbają” o buforowanie danych, każda z nich zawiera
w sobie wewnętrzny bufor. Warto porównać szybkość działania tej aplikacji z kodem
z listingu 6.22, różnica z pewnością będzie widoczna, mimo że w obu przypadkach wykorzystuje się metody odczytujące i zapisujące po jednym bajcie danych.
Oczywiście fakt, że klasy BufferedInputStream i BufferedOutputStream wykorzystują
swoje własne mechanizmy buforowania, nie wyklucza zastosowania dodatkowego bufora
w aplikacji, tak jak to miało miejsce w przypadku kodu z listingu 6.23. Pytanie brzmi
tylko: czy zastosowanie takiego dodatkowego bufora przyspieszyłoby działanie powyższej wersji programu? Warto samodzielnie wykonać takie ćwiczenie.
322
Java. Praktyczny kurs
Ćwiczenia do samodzielnego wykonania
Ć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 poszczegó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.
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ć rozległ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.
Lekcja 35. Kontenery
Klasy kontenerowe, czyli takie, które umożliwiają tworzenie obiektów przechowujących
inne obiekty, są dostępne w Javie od samego początku, choć większość z nich została
wprowadzona w Java 2 (wersja 1.2), a część dopiero w Java 5 (1.5). W lekcji 35. zostaną omówione podstawy kontenerów, zobaczymy, jak można napisać własną klasę
dynamicznie przechowującą dane, oraz przeanalizujemy przykłady wykorzystania
dwóch klas kontenerowych dostępnych w JDK służących do utworzenia struktury dynamicznej tablicy oraz stosu.
Przechowywanie wielu danych
W lekcji 11. zostały przedstawione tablice, czyli struktury przechowujące dane różnych typów. Jednym z ich głównych ograniczeń była konieczność jawnej deklaracji
ich wielkości. Zwykła tablica może przechowywać tylko tyle elementów, ile określono podczas jej tworzenia. W trakcie programowania często nie da się stwierdzić z góry, ile liczb czy obiektów będzie faktycznie potrzebnych, stąd konieczne są struktury
danych, które pozwalają na dynamiczne zwiększanie swojej wielkości, a tym samym
możliwości przechowywania danych. Jak poradzić sobie z takim problemem? Można
324
Java. Praktyczny kurs
napisać własną klasę symulującą zachowanie dynamicznej tablicy. Spróbujmy wykonać to zadanie. Klasę nazwijmy np. TablicaInt — będzie ona umożliwiała przechowywanie dowolnej wartości typu int. Dostęp do danych będzie realizowany za pomocą
metod get oraz set. Trzeba też ustalić, w jaki sposób przechowywać dane wewnątrz
tej klasy. Użyjemy do tego zwykłej tablicy typu int. Przykładowy kod został zaprezentowany na listingu 7.1.
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 argument konstruktora. Do pobierania danych służy metoda get, która przyjmuje jeden argument — index — określający indeks wartości, jaka ma zostać zwrócona. Indeks pobieranego 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ą indeksowane od 0) ani też mniejszy od 0. Jeśli zatem indeks znajduje się poza zakresem, generowany jest wyjątek ArrayIndexOutOfBoundsException:
throw new ArrayIndexOutOfBoundsException("index = " + index);
Rozdział 7.  Kontenery i typy uogólnione
325
Jeśli zaś argument przekazany metodzie jest poprawny, zwrócona zostaje wartość odczytana 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 indeksem. W tym przypadku na początku sprawdzamy, czy argument index jest mniejszy 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 wartość wskazaną przez value komórce określonej przez index:
tab[index] = value;
Pozostało więc przyjrzeć się metodzie resize. Nie wykonuje ona żadnych skomplikowanych 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];
Po wykonaniu tej czynności niezbędne jest oczywiście przepisanie zawartości starej
tablicy do nowej, co odbywa się w pętli for, a następnie przypisanie nowej tablicy
polu tab:
tab = newTab;
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”).
326
Java. Praktyczny kurs
Rysunek 7.1. Efekt działania kodu testującego nowy typ tablicy
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 programu 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);
}
Rozdział 7.  Kontenery i typy uogólnione
327
if(index >= tab.length){
resize(index + 1);
}
tab[index] = value;
}
protected void resize(int size){
Object newTab[] = new Object[size];
for(int i = 0; i < tab.length; i++){
newTab[i] = tab[i];
}
tab = newTab;
}
public int size(){
return tab.length;
}
public static void main(String args[]){
Tablica tab = new Tablica(2);
tab.set(0, 1);
tab.set(1, 2);
tab.set(2, new Object());
for(int i = 0; i < tab.size(); i++){
System.out.println("tab[" + i + "] = " + tab.get(i) + " ");
}
tab.get(3);
}
}
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 przyjmuje 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ą zapisanie 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ż przechowywać dane różnych typów.
328
Java. Praktyczny kurs
Rysunek 7.2. Obiekt klasy Tablica przechowuje dane różnych typów
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 nowych 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 najrozmaitsze struktury danych, takie jak tablice, tablice asocjacyjne, zbiory, stosy, kolejki,
drzewa itp. Oczywiście wszystkie pozwalają na dynamiczne dodawanie i usuwanie danych 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 zdziwienie 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.
Oprócz wybranych metod do dyspozycji są także konstruktory:
 ArrayList(),
 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 przechowywała dużo większą liczbę elementów, należy skorzystać z drugiej wersji konstruktora, 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.
Rozdział 7.  Kontenery i typy uogólnione
329
Tabela 7.1. Wybrane metody klasy ArrayList
Deklaracja metody
Opis
boolean add(E e)
Dodaje element na końcu listy.
void add(int index, E element)
Dodaje element w konkretnym miejscu listy
(wyznaczonym przez argument index).
void clear()
Usuwa wszystkie elementy listy.
boolean contains(Object o)
Sprawdza, czy lista zawiera określony element.
void ensureCapacity
(int minCapacity)
Zwiększa pojemność listy.
E get(int index)
Pobiera element znajdujący się na pozycji wskazywanej
przez argument index.
int indexOf(Object o)
Zwraca indeks pierwszego wystąpienia na liście obiektu
o lub –1, jeśli ten obiekt na liście nie występuje.
boolean isEmpty()
Zwraca true, jeśli lista nie zawiera żadnych elementów,
bądź false w przeciwnym wypadku.
int lastIndexOf(Object o)
Zwraca indeks ostatniego wystąpienia na liście obiektu o lub –1,
jeśli ten obiekt na liście nie występuje.
E remove(int index)
Usuwa z listy element znajdujący się na pozycji wskazanej
przez argument index.
boolean remove(Object o)
Usuwa z listy pierwsze wystąpienie elementu o.
protected void removeRange
(int fromIndex, int toIndex)
Usuwa z listy elementy z zakresu od fromIndex (włącznie)
do toIndex (wyłącznie).
E set(int index, E element)
Zamienia element znajdujący się na pozycji index na określony
przez argument element.
int size()
Zwraca aktualną liczbę elementów listy.
Object[] toArray()
Zwraca wszystkie elementy listy w postaci tablicy typu Object.
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.*;
public class Main {
public static void main(String args[]) {
ArrayList list = new ArrayList();
BufferedReader brIn = new BufferedReader(
new InputStreamReader(System.in)
);
System.out.println("Wprowadzaj linie tekstu. Aby zakończyć, wpisz quit.");
String line = "";
try{
while(!"quit".equals(line)){
line = brIn.readLine();
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).
Zebrane dane są następnie ponownie wyświetlane na ekranie. Odpowiada za to pętla
for. Liczba wprowadzonych wierszy tekstu, czyli liczba elementów listy list, jest
pobierana za pomocą wywołania metody size i zapisywana w zmiennej size. Ta właśnie
zmienna jest wykorzystywana w wyrażeniu warunkowym pętli. Poszczególne elementy
listy (to obiekty typu String) są odczytywane przy użyciu metody get oraz konwertowane na typ String i zapisywane w pomocniczej zmiennej line:
line = list.get(i).toString();
Ta zmienna jest następnie wykorzystywana jako jeden z argumentów instrukcji System.
out.println.
Po dokonaniu kompilacji przedstawionego programu w Java 5 (1.5) lub wyższej wersji na ekranie pojawi się informacja o potencjalnym braku kontroli typów, tak jak zostało to przedstawione na rysunku 7.3. Na razie nie trzeba się tym przejmować — ta
kwestia zostanie wyjaśniona w lekcji 36. Po uruchomieniu aplikacji można się przekonać, że wszystko działa zgodnie z założeniami (rysunek 7.4), a zastosowana dynamiczna lista pozwala na wprowadzenie praktycznie dowolnej ilości danych.
Rysunek 7.3.
Kompilator informuje
o braku kontroli typów
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 niego 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.
Tabela 7.2. Metody klasy Stack
Deklaracja metody
Opis
boolean empty()
Zwraca true, jeśli stos jest pusty, bądź false w przeciwnym wypadku.
E peek()
Zwraca obiekt znajdujący się na szczycie stosu, nie zdejmując go ze stosu.
E pop()
Zdejmuje obiekt ze szczytu stosu i zwraca go jako rezultat działania.
E push(E item)
Umieszcza obiekt na stosie.
int search(Object o)
Zwraca pozycję, w której obiekt o znajduje się na stosie, lub –1, kiedy obiektu
na stosie nie ma.
Listing 7.5.
import java.io.*;
import java.util.*;
public class Main {
public static void main(String args[]) {
Stack stack = new Stack();
BufferedReader brIn = new BufferedReader(
new InputStreamReader(System.in)
);
System.out.println("Wprowadzaj linie tekstu. Aby zakończyć, wpisz quit.");
String line = "";
try{
while(!"quit".equals(line)){
line = brIn.readLine();
stack.push(line);
}
System.out.println("Koniec wczytywania danych.");
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);
}
}
}
Kod jest pod względem struktury podobny do przedstawionego w poprzednim przykładzie. Najpierw jest tworzony obiekt, który będzie przechowywał dane — tym razem
jest nim stack:
Stack stack = new Stack();
a następnie obiekt brIn obsługujący strumień wejściowy. Dane wprowadzane z klawiatury są odczytywane w pętli while oraz umieszczane na stosie za pomocą metody
push obiektu stack:
stack.push(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 pomocą 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();
Przykładowy efekt działania programu został zaprezentowany na rysunku 7.5. Widać
więc wyraźnie, że obiekty umieszczane na stosie są z niego rzeczywiście zdejmowane
w odwrotnej kolejności.
Rysunek 7.5.
Przykładowy efekt
działania aplikacji
wykorzystującej
klasę Stack
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.*;
public class Main {
public static void main(String args[]) {
ArrayList list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
for(Object o: list){
System.out.println(o.toString());
}
}
}
Powstał tu obiekt list typu ArrayList, do którego za pomocą metody add zostały dodane 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 odczytano już wszystkie elementy (por. listing 7.5).
Dociekliwy programista zada jednak w tym miejscu pytanie, skąd właściwie rozszerzona 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 został napisany na początku tej lekcji (listing 7.3), i sprawmy, aby mogła korzystać z dobrodziejstw rozszerzonej pętli for. Odpowiedni kod został zaprezentowany na listingu 7.7.
334
Java. Praktyczny kurs
Tabela 7.3. Metody zdefiniowane w interfejsie Iterable
Deklaracja metody
Opis
boolean hasNext()
Zwraca true, jeśli istnieje kolejny obiekt, lub false w przeciwnym wypadku.
E next()
Zwraca kolejny obiekt kolekcji.
void remove()
Usuwa ostatni obiekt zwrócony przez iterator (operacja opcjonalna).
Listing 7.7.
import java.util.*;
public class Tablica implements Iterable{
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);
}
if(index >= tab.length){
resize(index + 1);
}
tab[index] = value;
}
protected void resize(int size){
Object newTab[] = new Object[size];
for(int i = 0; i < tab.length; i++){
newTab[i] = tab[i];
}
tab = newTab;
}
public int size(){
return tab.length;
}
public Iterator iterator(){
return new Iterator(){
int position = 0;
public boolean hasNext(){
return position < tab.length;
}
public Object next(){
return tab[position++];
}
public void remove(){
throw new UnsupportedOperationException();
}
};
}
Rozdział 7.  Kontenery i typy uogólnione
335
public static void main(String args[]){
Tablica tab = new Tablica(2);
tab.set(0, 1);
tab.set(1, 2);
tab.set(2, new Object());
for(Object o: tab){
System.out.println(o);
}
}
}
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łaniu pierwszy obiekt, przy drugim — drugi itd. W tym przypadku ma to być obiekt o indeksie 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 odbywa 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.
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.
Ćwiczenia do samodzielnego wykonania
Ć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 bardzo 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 obiekcie 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 zastosowana 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żytkownika. 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.
Lekcja 36. Typy uogólnione
Typy uogólnione pojawiły się w Javie dopiero w wersji 5 (1.5) i była to bardzo duża
zmiana w stosunku do poprzednich wersji tego języka. Mówimy o nich także jako o typach ogólnych lub generycznych, co pochodzi od angielskiej nazwy generics. Wszystkie
trzy określenia funkcjonują zarówno w literaturze, jak i w mowie potocznej i wszystkie mają swoich zwolenników i przeciwników. W książce będzie stosowany termin
typy uogólnione. W skrócie można powiedzieć, że pozwalają one na konstruowanie
kodu, który operuje nie na konkretnych typach danych (konkretnych klasach czy interfejsach), ale na typach nieokreślonych, ogólnych. To rozległy temat, którego omówienie
Rozdział 7.  Kontenery i typy uogólnione
337
wykracza poza ramy tematyczne niniejszej publikacji, ponieważ jednak każdy programista musi znać przynajmniej podstawy tego zagadnienia, lekcja 36. została poświęcona
niezbędnym podstawom.
Problem kontroli typów
Powróćmy do przykładu z klasą Tablica zaprezentowanego w poprzedniej lekcji (listing 7.3). Była to implementacja tablicy dynamicznej, w przypadku której nie trzeba było
troszczyć się o rozmiar, gdyż był on zwiększany automatycznie w miarę dodawania
nowych danych. Co więcej, było to rozwiązanie uniwersalne, gdyż klasa ta pozwalała na
przechowywanie danych dowolnych typów. Można powiedzieć: pełna wygoda. Niestety
ta uniwersalność i wygoda niosą też ze sobą pewne zagrożenia. Aby to sprawdzić, dopiszmy do przedstawionego na listingu 7.3 przykładu dwie dodatkowe klasy pakietowe,
Triangle i Rectangle, oraz nową metodę main, tak jak jest to widoczne na listingu 7.8.
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. Ponieważ 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 prawidł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 wychwycenia 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 traktowana 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.
338
Java. Praktyczny kurs
Rysunek 7.6. Podczas wykonywania programu został zgłoszony wyjątek ClassCastException
Przedstawiony problem dotyczy nie tylko tej wersji klasy Tablica. Jeśli w podobny sposó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 poprzednim przykładzie — wystąpi wyjątek ClassCastException.
Listing 7.9.
import java.util.*;
public class Main {
public static void main(String args[]){
ArrayList list = new ArrayList();
list.add(new Rectangle());
list.add(new Triangle());
for(Object o: list){
((Rectangle)o).diagonal();
}
}
}
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 programu, co sprawdzi się w praktyce, choć będzie wymagało zarówno większej pracy programisty, jak i większego narzutu związanego z wykonywaniem dodatkowych instrukcji
weryfikujących. Najwygodniejsze byłoby rozwiązanie polegające na możliwości przechowywania 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.
Zwróćmy uwagę, że kompilacja programu z listingu 7.9 powoduje wyświetlenie przez
kompilator ostrzeżenia, takiego samego jak to, które pojawiło się w poprzedniej lekcji
(rysunek 7.3). Dzieje się tak dlatego, że począwszy od Java 5 (Java 1.5), kontenery korzystają z dobrodziejstw typów uogólnionych. Wyświetlona informacja mówi właśnie o tym,
Rozdział 7.  Kontenery i typy uogólnione
339
że używamy kontenera w sposób klasyczny, nie wykorzystując możliwości kontroli
typów przez kompilator, i wykonujemy przez to potencjalnie niebezpieczną operację,
która może wygenerować błąd w programie wynikowym.
Po skorzystaniu z opcji Xlint:unchecked, czyli po wykonaniu kompilacji za pomocą
polecenia:
javac -Xlint:unchecked Main.java
kompilator wskaże miejsca w kodzie, które „uzna” za podejrzane. Jest to widoczne na
rysunku 7.7. Jak więc korzystać z kontenerów? Odpowiedź na to pytanie znajdzie się
w kolejnym podpunkcie tej lekcji.
Rysunek 7.7. Kompilator wskazuje podejrzane miejsca w programie
Jak korzystać z kontenerów?
Kontener może przechowywać obiekty dowolnego typu danych, ale możemy zadeklarować, jaki typ będzie używany w konkretnym przypadku. Jeśli więc w dynamicznej liście typu ArrayList zechcemy przechowywać obiekty typu Rectangle, możemy o tym
poinformować kompilator. Nie dopuści on wtedy do jawnego umieszczenia w kontenerze
obiektu innego typu (chyba że byłby to obiekt pochodny od Rectangle). W tym celu stosuje się składnię z nawiasami kątowymi, tak jak zostało to zilustrowane na listingu 7.10.
Listing 7.10.
import java.util.*;
public class Main {
public static void main(String args[]){
ArrayList<Rectangle> list = new ArrayList<Rectangle>();
list.add(new Rectangle());
//list.add(new Triangle());
for(Rectangle o: list){
o.diagonal();
}
list.get(0).diagonal();
}
}
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
jak i instrukcji tworzącej sam obiekt:
new ArrayList<Rectangle>()
Tym samym instrukcja:
list.add(new Rectangle());
może być wykonana bez problemu, ale już ujęta w komentarz:
list.add(new Triangle());
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ń, ponieważ typ danych jest jasno określony. Tak samo dzieje się w przypadku instrukcji:
list.get(0).diagonal();
Rysunek 7.8. Kompilator wykrył próbę użycia niewłaściwego typu danych
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.
W Java 8 wprowadzono możliwość zastosowania składni uproszczonej związanej z typami uogólnionymi. Można zauważyć, że przy tworzeniu obiektu ArrayList oraz zmiennej
referencyjnej trzeba było dwukrotnie użyć zapisu <Rectangle>. Raz przy określaniu
typu zmiennej (list) i drugi raz przy użyciu operatora new. To zapis nadmiarowy,
łatwo można wywnioskować, że w obu przypadkach chodzi o typ Rectangle. Zatem
począwszy od Java 8, poprawny będzie również zapis
ArrayList<Rectangle> list = new ArrayList<>();
Znaczenie jest takie samo jak przy zapisie z listingu 7.10. W tej sytuacji mówimy o dookreś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. Pobieraniem danych zajmuje się metoda get, a ustawianiem — set. Metoda set przyjmuje
jeden argument typu Object i przypisuje jego wartość polu data, natomiast metoda
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 pakietowe klasy pomocnicze, Triangle i Rectangle.
W wersjach Javy przed 5 opisane rozwiązanie było typowym sposobem pozwalającym na przechowywanie danych dowolnych typów. Dobrze znamy już jego wady
— zostały zilustrowane w kodzie metody main. Powstały w niej dwa obiekty klasy
Opakowanie: opk1 i opk2. Do obiektu opk1 został wstawiony nowy obiekt typu Rectangle,
a do opk2 — typu Triangle. Ponieważ konstruktor (podobnie jak metoda set) przyjmuje argument typu Object, w obu przypadkach zostało wykonane niejawne rzutowanie. A zatem instrukcje te zostały potraktowane jako:
Opakowanie opk1 = new Opakowanie((Object)new Rectangle());
Opakowanie opk2 = new Opakowanie((Object)new Triangle());
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 pobrane 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();
można byłoby rozbić na dwie, np.:
Rectangle rect1 = (Rectangle) opk1.get();
rect1.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, natomiast 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.
Skorzystajmy zatem z dobrodziejstw typów uogólnionych. Koncepcja jest następująca:
nasza klasa wciąż ma mieć możliwość przechowywania dowolnego typu danych, ale
podczas korzystania z niej chcemy mieć możliwość zadecydowania, jaki typ zostanie
użyty w konkretnym przypadku. Jak to zrobić? Po pierwsze, trzeba zaznaczyć, że klasa
będzie korzystała z typów uogólnionych, po drugie, trzeba zastąpić konkretny typ danych (w tym przypadku typ Object) typem uogólnionym (ogólnym). Tak więc za nazwą
klasy w nawiasie kątowym umieszczamy określenie typu uogólnionego, a następnie
używamy tego określenia wewnątrz klasy, zastępując nim typ konkretny. Zapewne brzmi
to nieco zawile, spójrzmy zatem od razu na listing 7.12.
Listing 7.12.
public class Opakowanie<T> {
private T data;
Opakowanie(T value) {
set(value);
}
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 dopiero 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 momencie 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(){}
}
344
Java. Praktyczny kurs
Możliwości klasy Opakowanie są testowane w metodzie main. Pierwsza instrukcja to
utworzenie zmiennej opk1 oraz przypisanie jej nowego obiektu. Ponieważ w tym obiekcie
chcemy przechowywać obiekty klasy Rectangle, zaznaczamy to, umieszczając tę nazwę w nawiasie kątowym za nazwą klasy Opakowanie. W Javie do wersji 8 należy to
zrobić zarówno przy deklaracji zmiennej:
Opakowanie<Rectangle> opk1
jak i w wywołaniu konstruktora:
new Opakowanie<Rectangle>(new Rectangle());
(na listingu to wywołanie zostało rozbite na trzy wiersze). Począwszy od Java 8, wystarczy 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.
Instrukcja tworząca obiekt opk2:
Opakowanie<Rectangle> opk2 = new Opakowanie<Rectangle>(new Triangle());
została ujęta w komentarz, gdyż jest nieprawidłowa i spowodowałaby błąd kompilacji,
który widać na rysunku 7.9. Błąd jest chyba dobrze widoczny — otóż kompilator otrzymał informację, że obiekt opk2 będzie współpracował z danymi typu Rectangle, tymczasem jako argument konstruktora został użyty obiekt klasy Triangle. Ponieważ klasy
Rectangle i Triangle nie mają ze sobą nic wspólnego (oprócz tego, że obie dziedziczą
po Object), taka instrukcja nie może być wykonana i kompilator protestuje. Z tych
samych względów nie może być również wykonana instrukcja:
opk2.get().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();
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.
Deklaracja klasy jako stosującej uogólnienie nie wyklucza jednak wykorzystywania
jej w sposób klasyczny. Taka sytuacja pojawiła się przecież w przykładach z klasami
kontenerowymi w lekcji 35. Ta zasada dotyczy także naszych własnych klas, takich
jak Opakowanie. Przykładowy obiekt opk4 można więc utworzyć w sposób następujący:
Opakowanie opk4 = new Opakowanie(
new Triangle()
);
Kompilator wygeneruje wtedy takie samo ostrzeżenie jak przedstawione wcześniej na
rysunku 7.3, jednak kod uda się bez problemu skompilować. Oczywiście w takim przypadku przy dostępie do obiektów przechowywanych w klasie Opakowanie niezbędne
będzie dokonywanie rzutowania, np.:
((Triangle)opk4.get()).type();
i wystąpią wszelkie problemy związane z konwersją typów, opisane na poprzednich
stronach.
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 argumencie 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)
Prosty przykład znajduje się na listingu 7.14.
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
346
Java. Praktyczny kurs
mógł być typ dowolny. Pokazuje to metoda main, w której metoda show została wywoł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 wywołanie metody toString) na typ String i wyświetlenie jej na ekranie, po uruchomieniu aplikacji zobaczymy widok przedstawiony na rysunku 7.10.
Rysunek 7.10.
Efekt działania
programu z listingu 7.14
Jak zwrócić wiele wartości?
Zwykła metoda może zwracać tylko jedną wartość. Jeśli ma być ich więcej, trzeba zastosować dodatkowy obiekt-kontener przechowujący dane. Można użyć którejś z klas
kontenerowych, można także napisać własną. Mogłaby ona wyglądać tak jak klasa
Opakowanie z przykładu widocznego na listingu 7.15.
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(){}
}
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 metody jest zwrócenie dwóch wartości typów Triangle i Rectangle. Odbywa się to przez utworzenie 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 rzutowania 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 dokonaniu rzutowania na tę klasę można wywołać metodę diagonal:
((Rectangle)opk.pole2).diagonal();
Program da się skompilować i uruchomić, jednak dotyczą go wszystkie wady związane
z brakiem kontroli typów, jakie zostały opisane już na początku tego rozdziału. Wystarczy pomyłka choćby przy rzutowaniu, a aplikacja nadal będzie się kompilowała bez
problemu, jednak podczas uruchomienia zostaną zgłoszone błędy.
Oczywiście można byłoby napisać specyficzną wersję klasy Opakowanie, która przechowywał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();
}
}
348
Java. Praktyczny kurs
class Opakowanie<M, N>{
M pole1;
N pole2;
Opakowanie(M pole1, N pole2){
this.pole1 = pole1;
this.pole2 = pole2;
}
}
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 zwracanego jako Opakowanie<Triangle, Rectangle>:
public Opakowanie<Triangle, Rectangle> getValues(){
Podobna konstrukcja występuje wewnątrz metody przy tworzeniu nowego obiektu
klasy Opakowanie:
new Opakowanie<Triangle, Rectangle>(new Triangle(), new Rectangle());
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 uniwersalności klasy Opakowanie.
Rozdział 7.  Kontenery i typy uogólnione
349
Ćwiczenia do samodzielnego wykonania
Ć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ł poprawnie.
Ć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ą manipulację 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ń.
350
Java. Praktyczny kurs
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, najczęściej przeglądarki internetowej, i ma dostęp jedynie do środowiska, które ten program mu udostępni. Aplet to inaczej program zagnieżdżony w innej aplikacji. Zazwyczaj 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ępnienie 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 chronionych 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 aplikacjach 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 rozdział 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 trybie tekstowym, najwyższy czas zatem przejść w świat aplikacji pracujących w trybie
graficznym. W lekcji 37. rozpoczynamy omawianie apletów, czyli niewielkich aplikacji
352
Java. Praktyczny kurs
uruchamianych pod kontrolą przeglądarek internetowych. Zostanie w niej przedstawione m.in. to, jak wygląda ogólna konstrukcja apletu, w jaki sposób jest on wywoływany
przez przeglądarkę oraz jak przekazać mu parametry, które mogą sterować sposobem
jego wykonania.
Pierwszy aplet
Utworzenie apletu wymaga użycia klasy Applet lub, jeśli miałyby się w nim znaleźć komponenty pakietu (biblioteki) Swing, JApplet. Co prawda w pierwszych przykładach nie
będzie wykorzystywany ten pakiet, jednak aplety zostaną wyprowadzone z nowocześniejszej klasy JApplet. Komponenty Swing zostaną natomiast przedstawione w lekcji 42.
Przed dokładnym omówieniem budowy tego typu programów warto na rozgrzewkę napisać 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.*;
public class PierwszyAplet extends JApplet {
public void paint (Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.drawString ("Pierwszy aplet", 100, 50);
}
}
Na początku importujemy klasę JApplet z pakietu javax.swing oraz pakiet java.awt.
Pakiet java.awt jest potrzebny, gdyż zawiera definicję klasy Graphics, dzięki której
można wykonywać operacje graficzne. Aby utworzyć własny aplet, trzeba wyprowadzić
klasę pochodną od JApplet, nasza klasa nazywa się po prostu PierwszyAplet. Co dalej?
Otóż aplet będzie wyświetlany w przeglądarce i zajmie pewną część jej okna. By na
powierzchni apletu wyświetlić napis, trzeba więc w jakiś sposób uzyskać do niej dostęp.
Umożliwia to metoda paint. Jest ona automatycznie wywoływana za każdym razem,
kiedy zaistnieje konieczność odrysowania (odświeżenia) powierzchni apletu. Metoda
ta otrzymuje w argumencie wskaźnik do specjalnego obiektu udostępniającego metody pozwalające na wykonywanie operacji na powierzchni apletu. Wywołujemy więc
metodę clearRect, która wyczyści obszar okna apletu1, a następnie drawString rysującą
napis we wskazanych współrzędnych. W powyższym przypadku będzie to napis Pierwszy
aplet we współrzędnych x = 100 i y = 50. Metoda clearRect przyjmuje cztery argumenty
określające prostokąt, który ma być wypełniony kolorem tła. Dwa pierwsze określają
współrzędne lewego górnego rogu, a dwa kolejne — szerokość i wysokość. Szerokość
jest uzyskiwana przez wywołanie getSize().width2, a wysokość — getSize().height.
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.
Rozdział 8.  Aplikacje i aplety
353
Przedstawiony kod należy skompilować w standardowy sposób, aby uzyskać plik
PierwszyAplet.class.
Do techniki rysowania na powierzchni apletu wrócimy już niebawem, teraz zajmiemy
się umieszczeniem go w przeglądarce. W tym celu trzeba napisać fragment kodu
HTML i umieścić w nim odpowiedni znacznik. Znacznikiem historycznym służącym
do umieszczania apletów był <applet>, w postaci:
<applet
code = "nazwa_klasy"
width = "szerokość apletu"
height = "wysokość apletu"
>
</applet>
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 podobnej do <applet>:
<object
type="application/x-java-applet"
code = "nazwa klasy"
width = "szerokość apletu"
height = "wysokość apletu">
</object>
W przypadku naszego pierwszego apletu miałby on postać widoczną na listingu 8.2.
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).
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
jeśli np. kod HTML został zapisany w pliku index.html:
appletviewer index.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 Firefoksa (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.
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, wykonuje 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 ładowany do pamięci i tworzona jest instancja (czyli obiekt) znalezionej klasy, a tym samym wywołany zostaje konstruktor. Następnie jest wykonywana metoda init utworzonego 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 powinien 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.*;
public class Aplet extends JApplet {
public Aplet() {
System.out.println("Konstruktor...");
}
public void init() {
System.out.println("Inicjalizacja apletu...");
}
public void start() {
System.out.println("Uruchomienie apletu...");
}
public void stop() {
System.out.println("Zatrzymanie apletu...");
}
public void paint (Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.drawString ("Pierwszy aplet", 100, 50);
}
}
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ść oczywiście istnieje, znacznik <object> (a także <applet>) pozwala na zastosowanie znacznikó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>
Znaczniki <param> umożliwiają właśnie przekazywanie różnych parametrów. Załóżmy
przykładowo, że apletowi ma być przekazany tekst, który będzie następnie wyświetlany
na jego powierzchni. Należy zatem w kodzie HTML zastosować następującą konstrukcję:
<object
<!--atrybuty znacznika object-->
>
<param name="tekst" value="Testowanie parametrów apletu">
</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.*;
Rozdział 8.  Aplikacje i aplety
357
public class Aplet extends JApplet {
private String tekst;
public void init() {
if((tekst = getParameter("tekst")) == null)
tekst = "Nie został podany parametr: tekst";
}
public void paint (Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.drawString (tekst, 60, 50);
}
}
Do odczytu wartości parametru została użyta metoda getParameter, której jako argument należy przekazać nazwę parametru (w tym przypadku tekst). Metoda ta zwraca
wartość wskazanego argumentu lub wartość null, jeżeli parametr nie został uwzględniony 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ł umieszczony w kodzie HTML i nie można było go pobrać) — jeśli tak, przypisujemy mu własny 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
358
Java. Praktyczny kurs
Lekcja 38. Kroje pisma (fonty) i kolory
Wiadomo już, w jaki sposób wyświetlać w apletach napisy, ten temat był poruszany
w pierwszej lekcji niniejszego rozdziału. W lekcji 38. zostaną omówione sposoby
umożliwiające wykorzystanie dostępnych w systemie fontów (potocznie określanych
też jako czcionki). Zostanie przedstawiona bliżej klasa Font. W drugiej części lekcji będzie natomiast mowa o kolorach, czyli klasie Color i takim jej wykorzystaniu, które
umożliwi pokolorowanie napisów wyświetlanych w apletach.
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 wyglą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 uruchomieniowe Javy) najlepiej pasująca do jednej z wymienionych rodzin.
Aby w praktyce zobaczyć, jakie są różnice pomiędzy wymienionymi krojami, napiszmy
teraz aplet, który wyświetli je na ekranie. Jest on widoczny na listingu 8.7.
Listing 8.7.
import javax.swing.JApplet;
import java.awt.*;
public class Aplet extends JApplet {
Font serif, sansSerif, monospaced, dialog, dialogInput;
public void init() {
serif = new Font("Serif", Font.BOLD, 24);
sansSerif = new Font("SansSerif", Font.BOLD, 24);
monospaced = new Font("Monospaced", Font.BOLD, 24);
dialog = new Font("Dialog", Font.BOLD, 24);
dialogInput = new Font("DialogInput", Font.BOLD, 24);
}
public void paint (Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.setFont(serif);
gDC.drawString ("Czcionka Serif", 60, 40);
gDC.setFont(sansSerif);
gDC.drawString ("Czcionka SansSerif", 60, 80);
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);
}
}
W klasie Aplet przygotowujemy pięć pól klasy Font odpowiadających poszczególnym
krojom pisma: serif, sansSerif, monospaced, dialog i dialogInput. W metodzie init
tworzymy pięć obiektów klasy Font i przypisujemy je przygotowanym polom. Konstruktor klasy Font przyjmuje trzy parametry. Pierwszy z nich określa nazwę rodziny
czcionek, drugi — styl czcionki, natomiast trzeci — jej wielkość. Styl czcionki ustalamy,
posługując się stałymi (zmienne finalne) z klasy Font. Do dyspozycji są trzy różne
wartości:
 Font.BOLD — czcionka pogrubiona,
 Font.ITALIC — czcionka pochylona,
 Font.PLAIN — czcionka zwyczajna.
Z instrukcji zawartych w metodzie init wynika zatem, że każdy krój będzie pogrubiony, o wielkości liter równej 24 punktom.
Wyświetlanie napisów odbywa się oczywiście w metodzie paint. Wykorzystujemy
w tym celu znaną nam już z poprzednich przykładów metodę drawString, która wyświetla tekst w miejscu ekranu o współrzędnych wskazywanych przez drugi (współrzędna x) i trzeci (współrzędna y) argument. Do zmiany kroju pisma stosujemy metodę
o nazwie setFont. Przyjmuje ona jako argument obiekt klasy Font zawierający opis danego fontu. Ostatecznie po skompilowaniu kodu i uruchomieniu apletu na ekranie pojawi się widok zaprezentowany na rysunku 8.6.
Rysunek 8.6.
Lista czcionek
dostępnych standardowo
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 dostę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ępnych 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.*;
public class Aplet extends JApplet {
Font[] fonts;
String[] fontNames;
public void init() {
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
fonts = ge.getAllFonts();
fontNames = ge.getAvailableFontFamilyNames();
}
public void paint(Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
int y = 20;
for(int i = 0; i < fonts.length; i++){
gDC.drawString(fonts[i].getFontName(), 20, y);
y += 15;
}
y = 20;
for(int i = 0; i < fontNames.length; i++){
gDC.drawString(fontNames[i], 240, y);
y += 15;
}
}
}
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.
Rozdział 8.  Aplikacje i aplety
361
W metodzie paint pozostaje odczytać i wyświetlić na ekranie zawartość tablic. Służą
do tego dwie pętle typu for. Do wyświetlania wykorzystuje się tradycyjnie metodę
drawString. Ponieważ tablica fonts zawiera obiekt typu Font, aby uzyskać nazwy
czcionek, wywołujemy metodę getFontName. Tablica fontNames zawiera obiekty typu
String (ciągi znaków), można więc bezpośrednio użyć ich jako argumentu metody
drawString. Do określenia współrzędnej y każdego napisu wykorzystuje się zmienną y,
której wartość jest po każdym wyświetleniu zwiększana o 15. Przykładowy wynik
działania programu jest widoczny na rysunku 8.7. W pierwszej kolumnie są wyświetlane
nazwy konkretnych fontów, a w drugiej nazwy rodzin czcionek dostępnych w systemie,
na którym aplet został uruchomiony. Obie listy z pewnością będą na tyle duże, że nie
zmieszczą się w standardowym oknie apletu. W ramach ćwiczenia do samodzielnego
wykonania można więc napisać program, który te dane wyświetli w oknie konsoli (podpunkt „Ćwiczenia do samodzielnego wykonania”).
Rysunek 8.7.
Lista czcionek i rodzin
czcionek dostępnych
w systemie
Powróćmy teraz do przykładu, który wyświetlał na ekranie prosty napis. Zauważmy,
że występuje w nim problem pozycjonowania tekstu na ekranie. Centrowanie musi się
odbywać metodą prób i błędów, co z pewnością nie jest wygodne. Automatyczne wykonanie tego zadania wymaga jednak znajomości długości oraz wysokości napisu. Wiadomo też, że dla każdego fontu wartości te będą różne. Trzeba w związku z tym posłużyć
się metodami udostępnianymi przez klasę FontMetrics: stringWidth i getHeight.
Pierwsza z nich podaje długość napisu w pikselach, natomiast druga — jego przeciętną
wysokość przy zastosowaniu danego fontu. Można to wykorzystać do ustawienia tekstu
na środku powierzchni apletu. Kod realizujący to zadanie jest widoczny na listingu 8.9.
Listing 8.9.
import javax.swing.JApplet;
import java.awt.*;
public class Aplet extends JApplet {
String tekst = "Przykładowy tekst";
Font font = null;
public void init() {
font = new Font("Arial", Font.BOLD, 20);
}
public void paint (Graphics gDC) {
gDC.setFont(font);
362
Java. Praktyczny kurs
FontMetrics fm = gDC.getFontMetrics();
int strWidth = fm.stringWidth(tekst);
int strHeight = fm.getHeight();
int x = (getWidth() - strWidth) / 2;
int y = (getHeight() + strHeight) / 2;
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.drawString(tekst, x, y);
}
}
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świetlaniu 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ędne 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 pobierzemy 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
Po wykonaniu obliczeń można już wywołać metodę drawString, która spowoduje, że
napis zapisany w polu tekst pojawi się na ekranie.
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 finalne określające łącznie dwanaście różnych kolorów. Pola te zostały zebrane w tabeli 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
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.setColor(Color.RED);
gDC.drawString(tekst, x, y);
}
Rozdział 8.  Aplikacje i aplety
363
Tabela 8.1. Stałe określające kolory w klasie Color
Nazwa stałej
Nazwa stałej
Reprezentowany kolor
Color.black
Color.BLACK
Czarny
Color.blue
Color.BLUE
Niebieski
Color.darkGray
Color.DARK_GRAY
Ciemnoszary
Color.gray
Color.GRAY
Szary
Color.green
Color.GREEN
Zielony
Color.lightGray
Color.LIGHT_GRAY
Jasnoszary
Color.magenta
Color.MAGENTA
Magenta (fuksja)9
Color.orange
Color.ORANGE
Pomarańczowy
Color.pink
Color.PINK
Różowy
Color.red
Color.RED
Czerwony
Color.white
Color.WHITE
Biały
Color.yellow
Color.YELLOW
Żółty
Szybko przekonamy się, że dwanaście kolorów to jednak trochę za mało — niestety
nie ma więcej stałych w klasie Color. Jeśli więc w tabeli 8.1 nie odnajdziemy odpowiadającego nam koloru, musimy postąpić inaczej. Otóż należy w takim przypadku utworzyć nowy obiekt klasy Color. Do dyspozycji jest siedem konstruktorów, są one dokładniej opisane w dokumentacji JDK. Najwygodniej i najprościej będzie zastosować jeden
z dwóch:
Color(int rgb)
Color(int r, int g, int b)
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 wymienionych 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 drugiemu 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 tabeli 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.
364
Java. Praktyczny kurs
Tabela 8.2. Składowe RGB dla wybranych kolorów
Kolor
Składowa R
Składowa G
Składowa B
Beżowy
245
245
220
Biały
255
255
255
Błękitny
0
191
255
Brązowy
165
42
42
Czarny
0
0
0
Czerwony
255
0
0
Ciemnoczerwony
139
0
00
Ciemnoniebieski
0
0
139
Ciemnoszary
169
169
169
Ciemnozielony
0
100
0
Fioletowy
238
130
238
Koralowy
255
127
80
Niebieski
0
0
255
Oliwkowy
107
142
35
Purpurowy
128
0
128
Srebrny
192
192
192
Stalowoniebieski
70
130
180
Szary
128
128
128
Zielony
0
255
0
Żółtozielony
195
205
50
Żółty
255
255
0
Listing 8.10.
import javax.swing.JApplet;
import java.awt.*;
public class Aplet extends JApplet {
String tekst;
Font font = null;
Color color = null;
public void init() {
int paramR = 0, paramG = 0, paramB = 0;
try{
paramR = Integer.parseInt(getParameter("paramR"));
paramG = Integer.parseInt(getParameter("paramG"));
paramB = Integer.parseInt(getParameter("paramB"));
}
catch(NumberFormatException e){
paramR = 0; paramG = 0; paramB = 0;
}
color = new Color(paramR, paramG, paramB);
Rozdział 8.  Aplikacje i aplety
365
font = new Font("Arial", Font.BOLD, 20);
tekst = getParameter("tekst");
if(tekst == null)
tekst = "";
}
public void paint (Graphics gDC) {
gDC.setFont(font);
FontMetrics fm = gDC.getFontMetrics();
int strWidth = fm.stringWidth(tekst);
int strHeight = fm.getHeight();
int x = (getWidth() - strWidth) / 2;
int y = (getHeight() + strHeight) / 2;
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.setColor(color);
gDC.drawString(tekst, x, y);
}
}
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 nazwach 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 zostanie 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 zajmuje 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>
366
Java. Praktyczny kurs
Ćwiczenia do samodzielnego wykonania
Ćwiczenie 38.1.
Napisz program, który w trybie tekstowym wyświetli na konsoli systemowej listę czcionek 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.
Lekcja 39. Grafika
Aplety pracują w środowisku graficznym, czas więc poznać podstawowe operacje graficzne, jakie można wykonywać. Służą do tego metody klasy Graphics, niektóre z nich,
jak drawString czy setColor, były już wykorzystywane we wcześniej prezentowanych
przykładach. W tej lekcji okaże się, jak rysować proste figury geometryczne, takie jak
linie, wielokąty czy okręgi, oraz jak wyświetlać obrazy z plików graficznych. Przedstawiony zostanie też interfejs ImageObserver, który pozwala na kontrolowanie postępów
w ładowaniu plików graficznych do apletu.
Rysowanie figur geometrycznych
Rysowanie figur geometrycznych umożliwiają wybrane metody klasy Graphics. Rysować można zarówno same kontury, jak i pełne figury wypełnione zadanym kolorem.
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).
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.
Tabela 8.3. Wybrane metody klasy Graphics
Deklaracja metody
Opis
void drawLine(int x1, int y1,
int x2, int y2)
Rysuje linię rozpoczynającą się w punkcie
o współrzędnych x1, y1 i kończącą się w punkcie x2, y2.
void drawOval(int x, int y,
int width, int height)
Rysuje owal wpisany w prostokąt opisany parametrami
x, y oraz width i height.
void drawPolygon(int[] xPoints,
int[] yPoints, int nPoints)
Rysuje wielokąt o punktach wskazywanych przez
tablice xPoints i yPoints. Liczbę segmentów linii,
z których składa się figura, wskazuje parametr nPoints.
void drawPolygon(Polygon p)
Rysuje wielokąt opisany przez argument p.
void drawPolyline(int[] xPoints,
int[] yPoints, int nPoints)
Rysuje sekwencję połączonych ze sobą odcinków (linii)
o współrzędnych zapisanych w tablicach xPoints
i yPoints. Liczba segmentów jest określona przez
argument nPoints.
void drawRect(int x, int y,
int width, int height)
Rysuje prostokąt zaczynający się w punkcie
o współrzędnych x, y oraz szerokości i wysokości
określonej przez argumenty width i height.
void drawRoundRect(int x, int y,
int width, int height, int arcWidth,
int arcHeight)
Rysuje prostokąt o zaokrąglonych rogach zaczynający
się w punkcie o współrzędnych x, y oraz szerokości
i wysokości określonej przez argumenty width i height.
Stopień zaokrąglenia rogów jest określany przez argumenty
arcWidth i arcHeight.
void fillOval(int x, int y,
int width, int height)
Rysuje koło lub elipsę wpisaną w prostokąt opisany
parametrami x, y oraz width i height.
void fillPolygon(int[] xPoints,
int[] yPoints, int nPoints)
Rysuje wypełniony bieżącym kolorem wielokąt o punktach
wskazywanych przez tablice xPoints i yPoints. Liczbę
segmentów, z których składa się figura, wskazuje
argument nPoints.
void fillPolygon(Polygon p)
Rysuje wypełniony bieżącym kolorem wielokąt opisany
przez argument p.
void fillRect(int x, int y,
int width, int height)
Rysuje wypełniony bieżącym kolorem prostokąt
zaczynający się w punkcie o współrzędnych x, y oraz
szerokości i wysokości określonej przez argumenty
width i height.
void fillRoundRect(int x, int y, int
width, int height, int arcWidth, int
arcHeight)
Rysuje wypełniony bieżącym kolorem prostokąt
o zaokrąglonych rogach, zaczynający się w punkcie
o współrzędnych x, y oraz szerokości i wysokości
określonej przez argumenty width i height. Stopień
zaokrąglenia rogów jest definiowany przez argumenty
arcWidth i arcHeight.
368
Java. Praktyczny kurs
Punkty i linie
Klasa Graphics nie oferuje oddzielnej metody, która umożliwiałaby rysowanie pojedynczych punktów, zamiast tego można jednak skorzystać z metody rysującej linie. Wystarczy, 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 pojedynczym 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.*;
public class Aplet extends JApplet {
public void paint (Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.drawLine(100, 40, 200, 40);
gDC.drawLine(100, 120, 200, 120);
gDC.drawLine(100, 40, 100, 120);
gDC.drawLine(200, 40, 200, 120);
gDC.drawLine(150, 80, 150, 80);
}
}
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.
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ólnym 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 prostoką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 jednoznacznie wyznacza koło lub elipsę.
Obie metody przyjmują cztery argumenty. Pierwsze dwa określają współrzędne x i y
lewego górnego rogu prostokąta, natomiast dwa kolejne — jego szerokość oraz wysokość.
Przykład wykorzystania metod drawOval i fillOval do narysowania na powierzchni
apletu kół i elips o różnych kolorach został zaprezentowany na listingu 8.13, a efekt widać
na rysunku 8.9.
Listing 8.13.
import javax.swing.JApplet;
import java.awt.*;
public class Aplet extends JApplet {
public void paint (Graphics gDC){
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.setColor(Color.BLUE);
gDC.drawOval (20, 20, 100, 100);
gDC.drawOval (140, 120, 100, 40);
gDC.setColor(Color.RED);
gDC.fillOval (20, 120, 100, 40);
gDC.fillOval (140, 20, 100, 100);
}
}
Rysunek 8.9.
Efekt działania metod
fillOval i drawOval
z klasy Graphics
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łnione 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 — szerokość 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.*;
public class Aplet extends JApplet {
public void paint (Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.setColor(Color.BLUE);
gDC.drawRect(20, 20, 80, 80);
gDC.drawRoundRect(120, 20, 80, 80, 20, 20);
gDC.setColor(Color.GREEN);
gDC.fillRoundRect(20, 120, 80, 80, 40, 20);
gDC.fillRect(120, 120, 80, 80);
}
}
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.
Rozdział 8.  Aplikacje i aplety
371
Oprócz prostokątów można rysować praktycznie dowolne wielokąty określone zbiorem
punktów wskazujących kolejne wierzchołki. Podobnie jak w przypadku wcześniej omawianych kształtów, mogą to być zarówno jedynie kontury, jak i figury wypełnione kolorem. Punkty określające wierzchołki przekazuje się w postaci dwóch tablic, pierwsza
zawiera współrzędne x, druga współrzędne y. Jeśli więc chcemy narysować sam kontur,
wykorzystamy metodę:
drawPolygon(int[] x, int[] y, int ile)
jeśli natomiast ma to być pełna figura — metodę:
fillPolygon(int[] x, int[] y, int ile)
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.*;
public class Aplet extends JApplet {
int tabX1[] = {20, 60, 140, 180, 140, 60};
int tabY1[] = {100, 20, 20, 100, 180, 180};
int tabX2[] = {200, 240, 320, 360, 320, 240};
int tabY2[] = {100, 20, 20, 100, 180, 180};
public void paint (Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.setColor(Color.RED);
gDC.drawPolygon(tabX1, tabY1, 6);
gDC.setColor(Color.GREEN);
gDC.fillPolygon(tabX2, tabY2, 6);
}
}
Rysunek 8.11.
Wykorzystanie metod
drawPolygon
i fillPolygon
do wyświetlenia
sześciokątów
372
Java. Praktyczny kurs
Istnieją również przeciążone wersje metod drawPolygon i fillPolygon, które przyjmują
jako argument obiekt klasy Polygon. Ich deklaracje mają postać:
drawPolygon(Polygon p)
fillPolygon(Polygon p)
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)
Pierwsze dwa argumenty to tablice współrzędnych kolejnych punktów, natomiast argument trzeci to liczba punktów. Przykładowy sześciokąt można zatem utworzyć w następujący sposób:
int tabX[] = {20, 60, 140, 180, 140, 60};
int tabY[] = {100, 20, 20, 100, 180, 180};
Polygon polygon = new Polygon(tabX, tabY, 6);
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 umiejscowienie 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 umieszczony dokument HTML i (lub) kod apletu, wygodne będzie użycie drugiej postaci konstruktora. Nie trzeba wtedy tworzyć nowego obiektu klasy URL bezpośrednio — wystarczy wywołać jedną z dwóch metod:
getDocumentBase()
lub:
getCodeBase()
Metoda getDocumentBase zwraca obiekt URL wskazujący lokalizację dokumentu HTML,
w którym znajduje się aplet, natomiast getCodeBase — lokalizację, w której umieszczony
jest kod apletu.
Po otrzymaniu zwróconego przez getImage obiektu klasy Image będzie można go użyć jako 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);
Rozdział 8.  Aplikacje i aplety
373
Argument img to referencja do obiektu klasy Image, wspX to współrzędna x, a wspY —
współrzędna y, this to referencja do obiektu implementującego interfejs ImageObserver
(w naszym przypadku będzie to obiekt apletu, czyli obiekt klasy dziedziczącej po
JApplet (Applet) — nieco dalej zostanie to wyjaśnione dokładniej). Przykładowy aplet
wyświetlający opisanym sposobem plik graficzny jest widoczny na listingu 8.16, natomiast efekt działania tego apletu został zaprezentowany na rysunku 8.12.
Listing 8.16.
import javax.swing.JApplet;
import java.awt.*;
public class Aplet extends JApplet {
Image img;
public void init() {
img = getImage(getDocumentBase(), "obraz.jpg");
}
public void paint(Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.drawImage (img, 20, 20, this);
}
}
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 metodzie 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 pierwszego wywołania metody drawImage.
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 informowany 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.*;
public class Aplet extends JApplet {
Image img;
public void init() {
img = getImage(getDocumentBase(), "obraz.jpg");
}
public void paint (Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.drawImage (img, 20, 20, this);
}
public boolean imageUpdate(Image img, int flags, int x, int y,
int width, int height) {
if ((flags & ImageObserver.ALLBITS) == 0){
showStatus ("Ładowanie obrazu...");
return true;
}
else{
showStatus ("Obraz załadowany");
repaint();
return false;
}
}
}
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żniejszy dla nas w tej chwili — flags. Argument flags to wartość typu int, w której poszczególne bity informują o stanie ładowanego obrazu. Informacje o ich dokładnym
znaczeniu można znaleźć w dokumentacji JDK w opisie interfejsu ImageObserver.
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.
Te właściwości są zatem wykorzystywane w metodzie imageUpdate. Badany jest wynik
iloczynu logicznego flags & ImageObserver.ALLBITS. Kiedy jest on równy 0, wykonana zostaje metoda showStatus ustawiająca na pasku stanu przeglądarki tekst (rysunek 8.13) informujący, że obraz jest w trakcie ładowania. Kiedy jest różny od 0, zostaje
wyświetlony napis, że obraz został załadowany. W tym drugim przypadku należy dodatkowo odświeżyć ekran apletu, za co odpowiada metoda repaint.
Rysunek 8.13.
Wyświetlanie informacji
o stanie załadowania
obrazu
Ćwiczenia do samodzielnego wykonania
Ć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.
376
Java. Praktyczny kurs
Rysunek 8.14.
Wzór graficzny
do ćwiczenia 39.4
Lekcja 40. Dźwięki i obsługa myszy
Niektóre aplety wymagają reakcji na działania użytkownika, zwykle chodzi o zdarzenia
związane z obsługą myszy. Jeśli aplet ma reagować na zmiany położenia kursora czy
na kliknięcia, może bezpośrednio implementować odpowiedni interfejs bądź też korzystać z dodatkowego obiektu. Jak taka obsługa wygląda w praktyce, okaże się właśnie
w lekcji 40. W drugiej części tej lekcji będzie mowa o odtwarzaniu dźwięków przez
aplety i interfejsie AudioClip.
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.
Tabela 8.4. Metody interfejsu MouseListener
Deklaracja metody
Opis
Od JDK
void mouseClicked(MouseEvent e)
Metoda wywoływana po kliknięciu przyciskiem
myszy.
1.1
void mouseEntered(MouseEvent e)
Metoda wywoływana, kiedy kursor myszy wejdzie
w obszar komponentu.
1.1
void mouseExited(MouseEvent e)
Metoda wywoływana, kiedy kursor myszy opuści
obszar komponentu.
1.1
void mousePressed(MouseEvent e)
Metoda wywoływana po naciśnięciu przycisku myszy.
1.1
void mouseReleased(MouseEvent e)
Metoda wywoływana po puszczeniu przycisku
myszy.
1.1
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.
Rozdział 8.  Aplikacje i aplety
377
Listing 8.18.
import javax.swing.JApplet;
import java.awt.event.*;
public class Aplet extends JApplet implements MouseListener {
//pola i metody apletu
public void mouseClicked(MouseEvent e) {
//kod wykonywany po kliknięciu myszą
}
public void mouseEntered(MouseEvent e) {
//kod wykonywany, kiedy kursor wejdzie w obszar komponentu
}
public void mouseExited(MouseEvent e) {
//kod wykonywany, kiedy kursor opuści obszar komponentu
}
public void mousePressed(MouseEvent e) {
//kod wykonywany, kiedy zostanie wciśnięty przycisk myszy
}
public void mouseReleased(MouseEvent e) {
//kod wykonywany, kiedy przycisk myszy zostanie zwolniony
}
}
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 otrzymać, 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ładowy 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.*;
public class Aplet extends JApplet implements MouseListener {
String tekst = "";
public void init() {
addMouseListener(this);
12
Metoda ta jest dostępna, począwszy od JDK 1.4, wcześniejsze wersje JDK jej nie zawierają.
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ść metod 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.).
W metodzie mouseClicked, wywołując metodę getButton, odczytujemy kod naciśniętego
przycisku i przypisujemy go zmiennej pomocniczej button (nie jest niezbędna i została
użyta wyłącznie ze względu na zwiększenie czytelności kodu; w praktyce wywołanie
evt.getButton() mogłoby być umieszczone bezpośrednio w instrukcji switch). Następnie sprawdzamy wartość tej zmiennej za pomocą instrukcji wyboru switch i w zależności od tego, czy jest to wartość BUTTON1, BUTTON2, czy BUTTON3, przypisujemy odpowiedni ciąg znaków zmiennej tekst, która odpowiada za napis, jaki będzie wyświetlany na
ekranie. W dalszej części kodu dodajemy do zmiennej tekst określenie współrzędnej
x i współrzędnej y, w której nastąpiło kliknięcie. Wartości wymienionych współrzędnych odczytujemy dzięki metodom getX i getY. Na końcu metody mouseClicked wywołujemy metodę repaint, która spowoduje odświeżenie ekranu. Odświeżenie ekranu
wiąże się oczywiście z wywołaniem metody paint, w której znana nam dobrze metoda
drawString jest używana do wyświetlenia tekstu zawartego w polu tekst.
Bardzo ważny jest również fragment kodu wykonywany w metodzie init. Otóż wywoł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ą.
Rozdział 8.  Aplikacje i aplety
379
O wywołaniu metody addMouseListener trzeba koniecznie pamiętać, gdyż jeśli jej zabraknie, 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ą
Skoro metoda addActionListener może przekazać obsługę zdarzeń dowolnemu obiektowi
implementującemu interfejs MouseListener, warto sprawdzić, jak by to wyglądało w praktyce. Napiszmy dodatkową klasę pakietową współpracującą z klasą Aplet i odpowiedzialną
za obsługę myszy. Aby nie komplikować kodu, jej zadaniem będzie jedynie wyświetlenie na konsoli współrzędnych ostatniego kliknięcia. Kod obu klas został zaprezentowany na listingu 8.20.
Listing 8.20.
import javax.swing.JApplet;
import java.awt.event.*;
public class Aplet extends JApplet {
public void init() {
addMouseListener(new MyMouseListener());
}
}
class MyMouseListener implements MouseListener {
public void mouseClicked(MouseEvent evt) {
String tekst = "";
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();
System.out.println(tekst);
}
public void mouseEntered(MouseEvent evt){}
public void mouseExited(MouseEvent evt){}
public void mousePressed(MouseEvent evt){}
public void mouseReleased(MouseEvent evt){}
}
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 wykonywana 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 przekazać 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 wewnę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.
Tabela 8.5. Metody interfejsu MouseMotionListener
Deklaracja metody
Opis
Od
JDK
void mouseDragged(MouseEvent e)
Metoda wywoływana podczas ruchu myszy,
kiedy wciśnięty jest jeden z przycisków.
1.1
void mouseMoved(MouseEvent e)
Metoda wywoływana przy każdym ruchu myszy,
o ile nie jest wciśnięty żaden przycisk.
1.1
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.*;
public class Aplet extends JApplet implements MouseMotionListener {
String tekst = "";
public void init() {
addMouseMotionListener(this);
}
public void paint (Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.drawString(tekst, 20, 20);
}
public void mouseMoved(MouseEvent evt) {
tekst = "zdarzenie mouseMoved, ";
tekst += "współrzędne: x = " + evt.getX() + ", ";
tekst += "y = " + evt.getY();
repaint();
}
public void mouseDragged(MouseEvent evt) {
tekst = "zdarzenie mouseDragged, ";
tekst += "współrzędne: x = " + evt.getX() + ", ";
tekst += "y = " + evt.getY();
repaint();
}
}
Sposób postępowania jest tu identyczny z prezentowanym w przypadku interfejsu
MouseListener. Klasa Aplet implementuje interfejs MouseMotionListener, zawiera zatem
metody mouseMoved i mouseDragged. W metodzie init jest wywoływana metoda addMouse
MotionListener (podobnie jak we wcześniejszych przykładach addMouseListener),
dzięki czemu wszystkie zdarzenia związane z poruszaniem myszy będą obsługiwane
przez klasę Aplet. W metodach mouseMoved oraz mouseDragged odczytujemy współrzędne kursora myszy dzięki funkcjom getX i getY, przygotowujemy treść pola tekst
oraz wywołujemy metodę repaint wymuszającą odświeżenie ekranu. W metodzie paint
wyświetlamy natomiast zawartość pola tekst na ekranie.
Dodatkowe parametry zdarzenia
Niekiedy przy przetwarzaniu zdarzeń związanych z obsługą myszy zachodzi potrzeba
sprawdzenia stanu klawiszy specjalnych Alt, Shift lub Ctrl. Jest tak np. wtedy, kiedy
program ma inaczej reagować, gdy kliknięcie nastąpiło z równoczesnym wciśnięciem
jednego z wymienionych klawiszy. Musi zatem istnieć sposób pozwalający na sprawdzenie, czy zaszła taka specjalna sytuacja. Tym sposobem jest dokładniejsze zbadanie
obiektu klasy MouseEvent, który jest przekazywany funkcji obsługującej każde zdarzenie
związane z obsługą myszy.
382
Java. Praktyczny kurs
Klasa MouseEvent (a dokładniej klasa InputEvent, po której MouseEvent dziedziczy)
udostępnia metodę o nazwie getModifiers, zwracającą wartość typu int, której poszczególne bity określają dodatkowe parametry zdarzenia. Stan tych bitów bada się poprzez
porównanie z jedną ze stałych13 zdefiniowanych w klasie MouseEvent. W sumie jest dostępnych kilkadziesiąt stałych, w większości odziedziczonych po klasach bazowych,
ich opis można znaleźć w dokumentacji JDK. Dla nas interesujące są trzy wartości:
 MouseEvent.SHIFT_DOWN_MASK,
 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ą technikę do stwierdzenia, które z klawiszy funkcyjnych były wciśnięte podczas przesuwania 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.*;
public class Aplet extends JApplet implements MouseMotionListener {
String tekst = "";
public void init() {
addMouseMotionListener(this);
}
public void paint (Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.drawString(tekst, 20, 20);
}
public void mouseMoved(MouseEvent evt) {
tekst = "Wciśnięte klawisze [";
int modifiers = evt.getModifiersEx();
if((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0){
tekst += " SHIFT ";
}
if((modifiers & MouseEvent.ALT_DOWN_MASK) != 0){
tekst += " ALT ";
}
if((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0){
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.
Rozdział 8.  Aplikacje i aplety
383
tekst += " CTRL ";
}
tekst += "], ";
tekst += "współrzędne: x = " + evt.getX() + ", ";
tekst += "y = " + evt.getY();
repaint();
}
public void mouseDragged(MouseEvent evt){}
}
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 odtwarza 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 umiejscowienie 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 postaci 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;
public class Aplet extends JApplet {
public void start() {
play (getDocumentBase(), "ding.au");
}
}
Drugim sposobem na odtwarzanie dźwięku jest wykorzystanie interfejsu AudioClip.
Interfejs ten zawiera definicje trzech metod: start, stop i loop. Metoda start rozpoczyna odtwarzanie dźwięku, stop kończy odtwarzanie, natomiast loop rozpoczyna
odtwarzanie dźwięku w pętli. Ponieważ AudioClip został zdefiniowany w pakiecie
java.applet, tym razem jako klasa apletu zostanie wykorzystana Applet (zamiast
384
Java. Praktyczny kurs
JApplet). Obiekt implementujący interfejs AudioClip otrzymamy, wywołując metodę
getAudioClip z klasy Applet. Metoda ta występuje w dwóch przeciążonych wersjach:
getAudioClip(URL url)
getAudioClip(URL url, String name)
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 listingu 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;
public class Aplet extends Applet {
AudioClip audioClip;
public void init() {
audioClip = getAudioClip(getDocumentBase(), "ding.au");
}
public void start() {
audioClip.loop();
}
public void stop() {
audioClip.stop();
}
}
Znacznie ciekawsze byłoby jednak połączenie możliwości odtwarzania dźwięków
oraz reakcji na zdarzenia związane z obsługą myszy. Możliwości, jakie dają interfejsy
MouseListener oraz AudioClip, pozwalają na napisanie apletu, który będzie np. odtwarzał
plik dźwiękowy, kiedy użytkownik kliknie myszą. Aplet realizujący takie zadanie jest
przedstawiony na listingu 8.25.
Listing 8.25.
import java.applet.Applet;
import java.applet.AudioClip;
import java.awt.event.*;
public class Aplet extends Applet implements MouseListener {
AudioClip audioClip;
public void init() {
addMouseListener(this);
audioClip = getAudioClip(getDocumentBase(), "ding.wav");
}
public void mouseClicked(MouseEvent evt) {
audioClip.play();
}
public void mouseEntered(MouseEvent evt){}
public void mouseExited(MouseEvent evt){}
public void mousePressed(MouseEvent evt){}
public void mouseReleased(MouseEvent evt){}
}
Rozdział 8.  Aplikacje i aplety
385
Klasa Aplet implementuje interfejs MouseListener, a zatem zawiera definicje wszystkich jego metod. W tym przypadku używana jest jednak tylko metoda mouseClicked,
która będzie wywoływana po każdym kliknięciu przyciskiem myszy. Rozpoczynamy
w niej odtwarzanie dźwięku poprzez wywołanie metody play obiektu audioClip.
Obiekt wskazywany przez pole audioClip uzyskujemy w metodzie init przez wywołanie
metody getAudioClip z klasy Applet, dokładnie tak samo jak w poprzednim przykładzie.
Oczywiście w kodzie znajduje się również wywołanie metody addMouseListener, bez
której aplet nie będzie reagował na kliknięcia.
Ćwiczenia do samodzielnego wykonania
Ć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 kursora 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.
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 popularne 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. Alternatywnie 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.*;
public class PierwszaAplikacja extends JFrame {
public PierwszaAplikacja() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(320, 200);
setVisible(true);
}
public void paint(Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.drawString ("Pierwsza aplikacja", 100, 100);
}
public static void main(String args[]) {
new PierwszaAplikacja();
}
}
Za utworzenie okna odpowiada klasa PierwszaAplikacja, która dziedziczy po klasie
JFrame. W konstruktorze za pomocą metody setSize ustalamy rozmiary okna, natomiast
za pomocą metody setVisible powodujemy, że zostanie ono wyświetlone na ekranie
(został przekazany argument true). Za wyświetlenie na ekranie napisu odpowiada metoda
drawString z klasy Graphics, odbywa się to dokładnie tak samo jak w przypadku
apletów omawianych w poprzednich lekcjach. Należy również zwrócić uwagę na instrukcję:
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Rozdział 8.  Aplikacje i aplety
387
która powoduje, że domyślnym działaniem wykonywanym podczas zamykania okna
(np. gdy użytkownik kliknie przycisk zamykający lub wybierze taką akcję z menu
systemowego) będzie zakończenie działania całego programu (wskazuje na to stała
EXIT_ON_CLOSE). Jeśli ta instrukcja zostanie pominięta, nie będzie można w standardowy
sposób zakończyć działania aplikacji (okno zostanie zamknięte, ale aplikacja wciąż
będzie działać).
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 komponentem) 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 standardem metoda main powinna mieć postać:
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new PierwszaAplikacja();
}
});
}
Ten sposób będzie wykorzystywany na listingach w dalszej części rozdziału.
Zdarzenia związane z oknem
Z każdym oknem związany jest zestaw zdarzeń reprezentowanych przez interfejs
WindowListener. Interfejs ten definiuje metody, które zostały zebrane w tabeli 8.6.
Oczywiście sama implementacja interfejsu to nie wszystko, należy dodatkowo poinformować system, że to właśnie dane okno ma odbierać wysyłane komunikaty, do czego
doprowadzi wywołanie metody addWindowListener (por. metody addMouseListener
i addMouseMotionListener z lekcji 40.). Zatem zamiast stosować metodę setDefault
CloseOperation, można również w nieco inny sposób obsłużyć zdarzenie polegające
na zamknięciu okna. Przykładowy kod klasy Aplikacja tworzącej okno reagujące na próby zamknięcia przy użyciu interfejsu WindowListener jest widoczny na listingu 8.27.
388
Java. Praktyczny kurs
Tabela 8.6. Metody interfejsu WindowListener
Deklaracja metody
Opis
Od JDK
void windowActivated(WindowEvent e)
Metoda wywoływana po aktywacji okna.
1.1
void windowClosed(WindowEvent e)
Metoda wykonywana, kiedy okno zostanie
zamknięte poprzez wywołanie metody dispose.
1.1
void windowClosing(WindowEvent e)
Metoda wywoływana, kiedy następuje próba
zamknięcia okna z menu systemowego.
1.1
void windowDeactivated(WindowEvent e)
Metoda wywoływana po dezaktywacji okna.
1.1
void windowDeiconified(WindowEvent e)
Metoda wywoływana, kiedy okno zmieni stan
ze zminimalizowanego na normalny.
1.1
void windowIconified(WindowEvent e)
Metoda wywoływana po minimalizacji okna.
1.1
void windowOpened(WindowEvent e)
Metoda wywoływana po otwarciu okna.
1.1
Listing 8.27.
import javax.swing.*;
import java.awt.event.*;
public class Aplikacja extends JFrame implements WindowListener {
public Aplikacja() {
addWindowListener(this);
setSize(320, 200);
setVisible(true);
}
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Aplikacja();
}
});
}
public void windowClosing(WindowEvent e){
dispose();
}
public void windowClosed(WindowEvent e){}
public void windowOpened(WindowEvent e){}
public void windowIconified(WindowEvent e){}
public void windowDeiconified(WindowEvent e){}
public void windowActivated(WindowEvent e){}
public void windowDeactivated(WindowEvent e){}
}
Klasa Aplikacja dziedziczy po klasie JFrame i implementuje interfejs WindowListener.
Wywołanie metody addWindowListener (z parametrem this wskazującym na obiekt aplikacji) w konstruktorze powoduje, że informacje o zdarzeniach będą przekazywane właśnie
obiektowi aplikacji, czyli że będą wywoływane metody windowClosing, windowClosed, win
dowOpened, windowIconified, windowDeiconified, windowActivated, windowDeactivated
z klasy Aplikacja. Ponieważ interesuje nas jedynie obsługa zdarzenia polegającego na
zamknięciu okna, wystarczy oprogramować metodę windowClosing. Jest ona wywoływana, kiedy użytkownik próbuje zamknąć okno poprzez wybranie odpowiedniej pozycji
z menu systemowego bądź też poprzez kliknięcie odpowiedniej ikony paska tytułowego
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.
Obsługa zdarzeń przez klasy anonimowe
W przykładzie z listingu 8.27 została przedstawiona aplikacja reagująca na zdarzenia
związane z jej oknem. Konkretnie, była to aplikacja, która kończyła swoje działanie
po wybraniu przez użytkownika odpowiedniej pozycji z menu systemowego lub też
kliknięciu właściwej ikony z paska tytułowego. Było to możliwe dzięki implementacji
interfejsu WindowListener bezpośrednio przez klasę okna. W takim wypadku konieczna
była jednak deklaracja wszystkich metod klasy WindowListener, nawet tych, które nie
były wykorzystywane. Zamiast tego wygodniej jest skorzystać z klasy adaptera, czyli
specjalnej klasy zawierającej puste implementacje metod danego interfejsu. W przypadku
interfejsu WindowListener jest to klasa WindowAdapter. Jeśli więc z WindowAdapter wyprowadzi się własną klasę i przesłoni w niej wybraną metodę, nie będzie konieczności
definiowania pozostałych. Taka sytuacja została zobrazowana na listingu 8.28.
Listing 8.28.
import javax.swing.*;
import java.awt.event.*;
public class Aplikacja extends JFrame {
class MyWindowAdapter extends WindowAdapter {
public void windowClosing(WindowEvent e){
dispose();
}
}
public Aplikacja() {
addWindowListener(new MyWindowAdapter());
setSize(320, 200);
setVisible(true);
}
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Aplikacja();
}
});
}
}
Tym razem w klasie Aplikacja została zdefiniowana klasa wewnętrzna MyWindowAdapter,
pochodna od WindowAdapter, a w niej metoda windowClosing. W tej metodzie wywoływana jest natomiast metoda dispose klasy Aplikacja. Jest to możliwe, jako że klasa
wewnętrzna ma dostęp do metod klasy zewnętrznej (por. lekcja 28.). W konstruktorze
klasy Aplikacja została wywołana metoda addWindowListener i został jej przekazany
w postaci argumentu obiekt klasy MyWindowAdapter (addWindowListener(new MyWindow
Adapter());). To nic innego jak informacja, że zdarzeniami związanymi z oknem
będzie się zajmował właśnie ten obiekt. Tak więc za całą obsługę zdarzenia będzie odpowiadać klasa MyWindowAdapter.
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.*;
public class Aplikacja extends JFrame {
public Aplikacja() {
addWindowListener(
new WindowAdapter(){
public void windowClosing(WindowEvent e){
dispose();
}
}
);
setSize(320, 200);
setVisible(true);
}
public static void main(String args[]) {
//tutaj kod metody main
}
}
Spójrzmy: w konstruktorze jest wywoływana metoda addWindowListener oznajmiająca, że zdarzenia związane z obsługą okna będą przekazywane obiektowi będącemu
argumentem tej metody. Tym argumentem jest z kolei obiekt klasy anonimowej pochodnej od WindowAdapter. Ponieważ klasa anonimowa jest z natury rzeczy klasą wewnętrzną, ma ona dostęp do składowych klasy zewnętrznej — Aplikacja — i może
wywołać metodę dispose zwalniającą zasoby i zamykającą okno aplikacji.
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
import
import
import
javax.swing.JApplet;
javax.swing.event.MouseInputAdapter;
java.awt.Graphics;
java.awt.event.*;
Rozdział 8.  Aplikacje i aplety
391
public class Aplet extends JApplet {
String tekst = "";
public void init() {
addMouseMotionListener(
new MouseInputAdapter(){
public void mouseMoved(MouseEvent evt) {
tekst = "zdarzenie mouseMoved, ";
tekst += "współrzędne: x = " + evt.getX() + ", ";
tekst += "y = " + evt.getY();
repaint();
}
}
);
}
public void paint (Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.drawString(tekst, 20, 20);
}
}
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 elementy 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, zobrazowano na listingu 8.31.
Listing 8.31.
import javax.swing.*;
public class Aplikacja extends JFrame {
public Aplikacja() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JMenuBar mb = new JMenuBar();
JMenu menu1 = new JMenu("Menu 1");
JMenu menu2 = new JMenu("Menu 2");
JMenu menu3 = new JMenu("Menu 3");
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();
392
Java. Praktyczny kurs
}
});
}
}
W konstruktorze tworzymy nowy obiekt klasy JMenuBar i przypisujemy go zmiennej
mb. Następnie tworzymy trzy obiekty klasy JMenu i dodajemy je do paska menu (czyli
obiektu mb) za pomocą metody add. W konstruktorze klasy JMenu przekazujemy nazwy
menu, czyli tekst, który będzie przez nie wyświetlany. Pasek menu dodajemy do okna
przez wywołanie metody setJMenuBar. Ostatecznie po skompilowaniu i uruchomieniu
aplikacji na ekranie zobaczymy widok zaprezentowany na rysunku 8.19.
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();
JMenu menu1 = new JMenu("Menu 1");
JMenu menu2 = new JMenu("Menu 2");
JMenu menu3 = new JMenu("Menu 3");
JMenuItem menuItem11 = new JMenuItem("Pozycja 1");
JMenuItem menuItem12 = new JMenuItem("Pozycja 2");
JMenuItem menuItem21 = new JMenuItem("Pozycja 1");
JMenuItem menuItem22 = new JMenuItem("Pozycja 2");
JMenuItem menuItem31 = new JMenuItem("Pozycja 1");
JMenuItem menuItem32 = new JMenuItem("Pozycja 2");
menu1.add(menuItem11);
menu1.add(menuItem12);
menu2.add(menuItem21);
menu2.add(menuItem22);
menu3.add(menuItem31);
menu3.add(menuItem32);
Rozdział 8.  Aplikacje i aplety
393
mb.add(menu1);
mb.add(menu2);
mb.add(menu3);
setJMenuBar(mb);
setSize(320, 200);
setVisible(true);
}
Zostało utworzonych sześć różnych obiektów klasy JMenuItem odpowiadających poszczególnym pozycjom menu. Obiekty te zostają dołączone do kolejnych menu poprzez
wywołanie metod add klasy JMenu. Przykładowo instrukcja menu1.add(menuItem11);
powoduje dodanie pierwszej pozycji do pierwszego menu. Po wykonaniu wszystkich
instrukcji powyższego kodu każde menu będzie miało po dwie pozycje o nazwach Pozycja 1 i Pozycja 2. Wygląd rozwiniętego menu został przedstawiony na rysunku 8.20.
Rysunek 8.20.
Menu z rozwiniętymi
pozycjami
Wiadomo już, jak tworzyć menu, warto więc teraz dowiedzieć się, w jaki sposób spowodować, aby program reagował na wybranie jednej z pozycji. Łatwo się domyślić, że trzeba 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 metoda 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 class Aplikacja extends JFrame {
private JMenuItem miZamknij;
private ActionListener al = new ActionListener(){
public void actionPerformed(ActionEvent e) {
if(e.getSource() == miZamknij)
dispose();
}
};
public Aplikacja() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
394
Java. Praktyczny kurs
JMenuBar mb = new JMenuBar();
JMenu menu = new JMenu("Plik");
miZamknij = new JMenuItem("Zamknij");
menu.add(miZamknij);
mb.add(menu);
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 (widać to na rysunku 8.21). Sposób utworzenia menu jest analogiczny do wykorzystanego 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.
W metodzie actionPerformed sprawdzamy, czy obiektem, który wywołał zdarzenie, jest
miZamknij, czyli pozycja menu o nazwie Zamknij. Jeśli tak, zamykamy okno, a tym samym całą aplikację (wywołanie metody dispose). Aby uzyskać referencję do obiektu,
który wywołał zdarzenie, wywołujemy metodę getSource obiektu klasy ActionEvent
otrzymanego jako argument metody actionPerformed.
Pozycji menu (a dokładniej: każdemu komponentowi pochodnemu od java.awt.Button
lub javax.swing.AbstractButton) można także przypisać komendę, dzięki której będzie
można łatwo zidentyfikować źródło zdarzenia. Robi się to za pomocą metody setAction
Rozdział 8.  Aplikacje i aplety
395
Command. W argumencie należy przekazać wymyśloną nazwę komendy. Z kolei w klasie ActionEvent znajduje się metoda getActionCommand pozwalająca na ustalenie, jaka
komenda jest powiązana z danym zdarzeniem. Aplikacja z listingu 8.33 mogłaby zatem być napisana również w sposób przedstawiony na listingu 8.34.
Listing 8.34.
import javax.swing.*;
import java.awt.event.*;
public class Aplikacja extends JFrame {
private ActionListener al = new ActionListener(){
public void actionPerformed(ActionEvent e) {
if("Zamknij".equals(e.getActionCommand()))
dispose();
}
};
public Aplikacja() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JMenuBar mb = new JMenuBar();
JMenu menu = new JMenu("Plik");
JMenuItem miZamknij = new JMenuItem("Zamknij");
miZamknij.setActionCommand("Zamknij");
menu.add(miZamknij);
mb.add(menu);
//dalsze instrukcje konstruktora
}
public static void main(String args[]) {
//treść metody main
}
}
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 odczytać 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 pomocą 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.
396
Java. Praktyczny kurs
Menu kaskadowe
Pozycje menu można łączyć kaskadowo i uzyskiwać w ten sposób rozbudowane struktury podmenu. Utworzenie tego typu konstrukcji jest możliwe poprzez dodanie do menu
innego menu, czyli przekazanie metodzie add z klasy JMenu (w postaci argumentu) innego 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 class Aplikacja extends JFrame {
private ActionListener al = new ActionListener(){
public void actionPerformed(ActionEvent e) {
String text = ((JMenuItem)e.getSource()).getText();
System.out.println(text);
}
};
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(new JMenuItem("Pozycja 1"));
menu.getItem(0).addActionListener(al);
menu.add(new JMenuItem("Pozycja 2"));
menu.getItem(1).addActionListener(al);
menu.add(new JMenuItem("Pozycja 3"));
menu.getItem(2).addActionListener(al);
submenu1.add(new JMenuItem("Pozycja 5"));
submenu1.getItem(0).addActionListener(al);
submenu1.add(new JMenuItem("Pozycja 6"));
submenu1.getItem(1).addActionListener(al);
submenu1.add(new JMenuItem("Pozycja 7"));
submenu1.getItem(2).addActionListener(al);
submenu2.add(new JMenuItem("Pozycja 9"));
submenu2.getItem(0).addActionListener(al);
submenu2.add(new JMenuItem("Pozycja 10"));
submenu2.getItem(1).addActionListener(al);
submenu2.add(new JMenuItem("Pozycja 11"));
submenu2.getItem(2).addActionListener(al);
menu.add(submenu1);
submenu1.add(submenu2);
mb.add(menu);
setJMenuBar(mb);
setSize(320, 200);
setVisible(true);
}
Rozdział 8.  Aplikacje i aplety
397
public static void main(String args[]) {
//kod metody main
}
}
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. Etykieta 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, rysunek 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
Obsługą zdarzeń zajmuje się, podobnie jak we wcześniej prezentowanych przykładach,
obiekt klasy anonimowej implementującej interfejs ActionListener reprezentowany
przez prywatne pole al z klasy Aplikacja. W związku z tym dla każdego obiektu
reprezentującego daną pozycję menu jest wywoływana metoda addActionListener
(addActionListener(al)). Wykorzystywany jest tu jednak inny sposób dostępu do tych
obiektów. Ponieważ są one tworzone bezpośrednio w metodzie add klasy JMenu, np.:
menu.add(new JMenuItem("Pozycja 1"));
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 pobrania tej nazwy jest wykonywana złożona instrukcja:
String text = ((JMenuItem)e.getSource()).getText();
398
Java. Praktyczny kurs
Obiekt e to obiekt klasy ActionEvent zawierający wszystkie informacje o zdarzeniu.
Metoda getSource pozwala na pobranie obiektu, który zapoczątkował dane zdarzenie,
a więc obiektu pozycji menu wybranej przez użytkownika. Ponieważ typem wartości
zwracanej przez getSource jest Object, dokonywane jest rzutowanie na typ JMenuItem,
a potem następuje wywołanie metody getText, która zwraca nazwę wybranego menu
(tekst danej pozycji, np. Pozycja 1, Pozycja 5). Uzyskany tekst jest wyświetlany na
konsoli za pomocą instrukcji:
System.out.println(text);
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ż sposó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 poszczególnych pozycji został przedstawiony na listingu 8.36.
Listing 8.36.
import javax.swing.*;
import java.awt.event.*;
public class Aplikacja extends JFrame {
private JCheckBoxMenuItem menuItem1, menuItem2;
private ItemListener il = new ItemListener(){
public void itemStateChanged(ItemEvent e) {
if(e.getSource() == menuItem1){
if(menuItem1.getState()){
System.out.println("Pozycja 1 jest zaznaczona.");
}
else{
System.out.println("Pozycja 1 nie jest zaznaczona.");
}
}
else if(e.getSource() == menuItem2){
if(menuItem2.getState()){
System.out.println("Pozycja 2 jest zaznaczona.");
}
else{
Rozdział 8.  Aplikacje i aplety
399
System.out.println("Pozycja 2 nie jest zaznaczona.");
}
}
}
};
public Aplikacja() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JMenuBar mb = new JMenuBar();
JMenu menu = new JMenu("Menu 1");
menuItem1 = new JCheckBoxMenuItem("Pozycja 1", true);
menuItem1.addItemListener(il);
menu.add(menuItem1);
menuItem2 = new JCheckBoxMenuItem("Pozycja 2", false);
menuItem2.addItemListener(il);
menu.add(menuItem2);
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
Obsługa zdarzeń przychodzących z obiektów klasy JCheckBoxMenuItem wymaga implementacji interfejsu ItemListener, a tym samym metody itemStateChanged. Metoda ta
będzie wywoływana za każdym razem, kiedy nastąpi zmiana stanu danej pozycji menu,
czyli kiedy zostanie ona zaznaczona lub jej zaznaczenie zostanie usunięte. Wtedy należy sprawdzić, która pozycja spowodowała wygenerowanie zdarzenia: menuItem1 czy
menuItem2. Referencję uzyskuje się przez wywołanie metody getSource. Aby sprawdzić,
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 skorzystać z klasy JPopupMenu. Konstrukcja taka jest bardzo podobna do konstrukcji zwyczajnego menu, z tą różnicą, że menu kontekstowego nie dodaje się do paska menu. Przykładowa aplikacja zawierająca menu kontekstowe została przedstawiona na listingu 8.37.
Listing 8.37.
import javax.swing.*;
import java.awt.event.*;
public class Aplikacja extends JFrame {
private JPopupMenu popupMenu;
private JMenuItem miPozycja1, miPozycja2, miZamknij;
private ActionListener al = new ActionListener(){
public void actionPerformed(ActionEvent e) {
if(e.getSource() == miZamknij)
dispose();
}
};
private MouseAdapter ma = new MouseAdapter(){
public void mousePressed(MouseEvent e) {
if(e.isPopupTrigger())
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
public void mouseReleased(MouseEvent e) {
if(e.isPopupTrigger())
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
};
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);
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. Konstruktor 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 przypadku 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 poprzednich przykładach. Obiektem obsługującym zdarzenia powiązanym z każdym z obiektó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 interfejsu 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-
402
Java. Praktyczny kurs
stu obiekt aplikacji uzyskiwany przez wywołanie e.getComponent(). Metoda getCompo
nent klasy MouseEvent zwraca bowiem obiekt, który zapoczątkował zdarzenie.
Ćwiczenia do samodzielnego wykonania
Ć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.
Lekcja 42. Komponenty
Każda aplikacja okienkowa, oprócz w menu, których różne rodzaje pojawiły się w lekcji
41., jest wyposażona także w wiele innych elementów graficznych, takich jak przyciski, etykiety, pola tekstowe czy listy rozwijane16. Pakiet javax.swing zawiera odpowiedni
zestaw klas, które pozwalają na zastosowanie tego rodzaju komponentów. Tych klas
jest bardzo wiele. Część z nich zostanie przedstawiona w ostatniej, 42. lekcji.
16
Często spotyka się też termin „lista rozwijalna”.
Rozdział 8.  Aplikacje i aplety
403
Klasa Component
Praktycznie wszystkie graficzne komponenty, które pozwalają na zastosowanie typowych 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.
Tabela 8.7. Wybrane metody klasy Component
Deklaracja metody
Opis
Od JDK
void addKeyListener
(KeyListener l)
Ustala obiekt, który będzie obsługiwał zdarzenia
związane z klawiaturą.
1.1
void addMouseListener
(MouseListener l)
Ustala obiekt, który będzie obsługiwał zdarzenia
związane z klikaniem przyciskami myszy.
1.1
void addMouseMotionListener
(MouseMotionListener l)
Ustala obiekt, który będzie obsługiwał zdarzenia
związane z ruchami myszy.
1.1
boolean contains(int x,
int y)
Sprawdza, czy komponent zawiera punkt
o współrzędnych x, y.
1.1
boolean contains(Point p)
Sprawdza, czy komponent zawiera punkt wskazywany
przez argument p.
1.1
Color getBackground()
Pobiera kolor tła komponentu.
1.0
Rectangle getBounds()
Zwraca rozmiar komponentu w postaci obiektu klasy
Rectangle.
1.0
Cursor getCursor()
Zwraca kursor związany z komponentem.
1.1
Font getFont()
Zwraca font związany z komponentem.
1.0
FontMetrics getFontMetrics
(Font font)
Zwraca obiekt opisujący właściwości fontu
określonego przez argument font.
1.0 (1.5)
Color getForeground()
Zwraca pierwszoplanowy kolor komponentu.
1.0
Graphics getGraphics()
Zwraca tzw. graficzny kontekst urządzenia,
obiekt pozwalający na wykonywanie operacji
graficznych na komponencie.
1.0 (1.2)
int getHeight()
Zwraca wysokość komponentu.
1.2
String getName()
Zwraca nazwę komponentu.
1.1
Container getParent()
Zwraca obiekt nadrzędny komponentu.
1.0
Dimension getSize()
Zwraca rozmiary komponentu w postaci obiektu
klasy Dimension.
1.1
int getWidth()
Zwraca szerokość komponentu.
1.2
int getX()
Zwraca współrzędną x położenia komponentu.
1.2
17
A także klas, po których dziedziczy JComponent.
404
Java. Praktyczny kurs
Tabela 8.7. Wybrane metody klasy Component (ciąg dalszy)
Deklaracja metody
Opis
Od JDK
int getY()
Zwraca współrzędną y położenia komponentu.
1.2
void repaint()
Odrysowuje cały obszar komponentu.
1.0
void repaint(int x, int y,
int width, int height)
Odrysowuje wskazany obszar komponentu.
1.0
void setBackground(Color c)
Ustala kolor tła komponentu.
1.0 (1.2)
void setBounds(int x, int y,
int width, int height)
Ustala rozmiar i położenie komponentu.
1.1
void setBounds(Rectangle r)
Ustala rozmiar i położenie komponentu.
1.1
void setCursor(Cursor cursor)
Ustawia rodzaj kursora przypisany komponentowi.
1.1
void setFont(Font f)
Ustawia font przypisany komponentowi.
1.0 (1.2)
void setLocation(int x, int y)
Zmienia położenie komponentu.
1.1
void setLocation(Point p)
Zmienia położenie komponentu.
1.1
void setName(String name)
Ustala nazwę komponentu.
1.1
void setSize(Dimension d)
Ustala rozmiary komponentu.
1.1
void setSize(int width,
int height)
Ustala rozmiary komponentu.
1.1
void setVisible(boolean b)
Wyświetla lub ukrywa komponent.
1.0 (1.2)
Etykiety
Etykiety tekstowe to jedne z najprostszych komponentów graficznych. Umożliwiają
wyświetlanie tekstu. Aby utworzyć etykietę, należy skorzystać z klasy JLabel. Konstruktor 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 wykorzystanie klasy JLabel jest widoczny na listingu 8.38, natomiast efekt jego działania na
rysunku 8.25.
Listing 8.38.
import javax.swing.*;
public class Aplikacja extends JFrame {
public Aplikacja() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(null);
JLabel label1 = new JLabel("Pierwsza etykieta");
label1.setBounds(100, 40, 120, 20);
JLabel label2 = new JLabel();
label2.setText("Druga etykieta");
label2.setBounds(100, 60, 120, 20);
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
Aplikacja wykorzystuje oba wymienione wyżej sposoby tworzenia etykiet. W pierwszym
przypadku (obiekt label1) już w konstruktorze jest przekazywany tekst, jaki ma być
wyświetlany. W drugim przypadku (obiekt label2) tworzona etykieta jest pusta, a tekst
jest jej przypisywany za pomocą metody setText. Rozmiary oraz położenie etykiet są
ustalane dzięki metodzie setBounds. Pierwsze dwa argumenty tej metody określają współrzędne x i y, natomiast dwa kolejne — szerokość oraz wysokość. Etykiety są dodawane do okna aplikacji za pomocą instrukcji:
add(label1);
add(label2);
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 przypadku 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 zastosowany 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.
406
Java. Praktyczny kurs
możliwe za pomocą metody setText. W odróżnieniu od etykiet przyciski powinny jednak
reagować na kliknięcia myszą, przy ich stosowaniu niezbędne będzie zatem zaimplementowanie interfejsu ActionListener. Przykładowa aplikacja zawierająca dwa przyciski,
taka, że po kliknięciu drugiego z nich nastąpi jej zamknięcie, jest widoczna na listingu
8.39, natomiast wygląd okna został zobrazowany na rysunku 8.26.
Listing 8.39.
import javax.swing.*;
import java.awt.event.*;
public class Aplikacja extends JFrame {
private JButton button1, button2;
public Aplikacja() {
ActionListener al = new ActionListener() {
public void actionPerformed(ActionEvent e) {
if(e.getSource() == button2)
dispose();
}
};
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(null);
button1 = new JButton("Pierwszy przycisk");
button1.setBounds(80, 40, 160, 20);
button1.addActionListener(al);
button2 = new JButton();
button2.setText("Drugi przycisk");
button2.setBounds(80, 80, 160, 20);
button2.addActionListener(al);
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
Rozdział 8.  Aplikacje i aplety
407
Pierwszy przycisk jest tworzony za pomocą konstruktora przyjmującego w argumencie obiekt klasy String, czyli po prostu ciąg znaków. W drugim przypadku do ustawiania
napisu wyświetlanego na przycisku wykorzystuje się metodę setText. Do ustalenia
położenia i rozmiarów przycisków używa się natomiast metody setBounds, której działanie jest identyczne z omawianym w przypadku przedstawionych wcześniej etykiet. Ponieważ przyciski muszą reagować na kliknięcia, tworzony jest nowy obiekt (al) anonimowej
klasy implementującej interfejs ActionListener. W metodzie actionPerformed sprawdzamy, czy źródłem zdarzenia był drugi przycisk (if(e.getSource() == button2)).
Jeśli tak, zamykamy okno i kończymy działanie aplikacji (wywołanie metody dispose).
Pola tekstowe
Istnieje kilka rodzajów pól tekstowych: JTextField, JPasswordField, JFormattedText
Field, JTextArea, JEditorPane i JTextPane. Wszystkie te klasy dziedziczą bezpośrednio 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.
Tabela 8.8. Konstruktory klasy JTextField
Konstruktor
Opis
JTextField()
Tworzy nowe, puste pole tekstowe.
JTextField(Document doc,
String text, int columns)
Tworzy nowe pole tekstowe o zadanej liczbie kolumn i zawartości,
zgodne z modelem dokumentu wskazanym przez argument doc.
JTextField(int columns)
Tworzy nowe pole tekstowe o zadanej liczbie kolumn.
JTextField(String text)
Tworzy nowe pole tekstowe zawierające wskazany tekst.
JTextField(String text,
int columns)
Tworzy nowe pole tekstowe o zadanej liczbie kolumn zawierające
wskazany tekst.
Listing 8.40.
import javax.swing.*;
import java.awt.event.*;
public class Aplikacja extends JFrame {
private JTextField textField;
private JButton button1;
public Aplikacja() {
ActionListener al = new ActionListener() {
public void actionPerformed(ActionEvent e) {
if(e.getSource() == button1)
408
Java. Praktyczny kurs
setTitle(textField.getText());
}
};
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(null);
textField = new JTextField();
textField.setBounds(100, 50, 100, 20);
button1 = new JButton("Kliknij!");
button1.setBounds(100, 80, 100, 20);
button1.addActionListener(al);
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ń wykorzystujemy 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 monitorować zmiany dokumentu (obiektu klasy Document) powiązanego z komponentem JText
Field. Trzeba zatem skorzystać z interfejsu DocumentListener. Najpierw tworzy się nowy obiekt implementujący ten interfejs, a następnie przekazuje się go jako argument metody setDocumentListener wywołanej na rzecz obiektu zwróconego przez wywołanie
Rozdział 8.  Aplikacje i aplety
409
getDocument klasy JTextField. Przy założeniu, że obiektem implementującym interfejs
DocumentListener jest dl, a pole tekstowe reprezentuje obiekt textField, wywołanie po-
winno mieć postać:
textField.getDocument().addDocumentListener(dl)
W interfejsie DocumentListener zdefiniowane zostały trzy metody:
 changedUpdate — wywoływana po zmianie atrybutu lub atrybutów;
 insertUpdate — wywoływana przy wstawianiu treści do dokumentu;
 removeUpdate — wywoływana przy usuwaniu treści z dokumentu.
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.*;
public class Aplikacja extends JFrame {
private JTextField textField;
public Aplikacja() {
DocumentListener dl = new DocumentListener() {
public void changedUpdate(DocumentEvent e) {
setTitle(textField.getText());
}
public void insertUpdate(DocumentEvent e) {
setTitle(textField.getText());
}
public void removeUpdate(DocumentEvent e) {
setTitle(textField.getText());
}
};
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(null);
textField = new JTextField();
textField.setBounds(100, 50, 100, 20);
textField.getDocument().addDocumentListener(dl);
add(textField);
setSize(320, 200);
setVisible(true);
}
public static void main(String args[]) {
//kod metody main
}
}
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.
Tabela 8.9. Konstruktory klasy JTextArea
Konstruktor
Opis
JTextArea()
Tworzy pusty komponent JTextArea.
JTextArea(Document doc)
Tworzy nowe pole tekstowe zgodne z modelem dokumentu
wskazanym przez argument doc.
JTextArea(Document doc,
String text, int rows,
int columns)
Tworzy nowe pole tekstowe zgodne z modelem dokumentu
wskazanym przez argument doc, o zadanej zawartości oraz
określonej liczbie wierszy i kolumn.
JTextArea(int rows,
int columns)
Tworzy pusty komponent JTextArea o określonej liczbie wierszy
i kolumn.
JTextArea(String text)
Tworzy komponent JTextArea zawierający tekst określony
przez argument text.
JTextArea(String text,
int rows, int columns)
Tworzy komponent JTextArea zawierający tekst określony przez
argument text, o liczbie wierszy i kolumn określonej argumentami
rows i columns.
Listing 8.42.
import javax.swing.*;
import java.awt.event.*;
import java.io.*;
public class Aplikacja extends JFrame {
private JTextArea textArea;
private JButton button1;
public Aplikacja() {
ActionListener al = new ActionListener() {
public void actionPerformed(ActionEvent e) {
if(e.getSource() == button1)
save();
}
};
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(null);
textArea = new JTextArea();
textArea.setBounds(30, 30, 260, 200);
this.add(textArea);
button1 = new JButton("Zapisz");
button1.setBounds(100, 240, 100, 20);
button1.addActionListener(al);
add(textArea);
add(button1);
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 przycisku zapewnia obiekt klasy anonimowej implementującej interfejs ActionListener,
dokładnie tak samo jak w przypadku wcześniej prezentowanych przykładów. Po kliknię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ą zaznaczanie opcji. Konstruktory klasy zostały przedstawione w tabeli 8.10, a program wyświetlający sześć pól typu JCheckBox pokazano na listingu 8.43. Wygląd okna takiej aplikacji
jest widoczny na rysunku 8.29.
412
Java. Praktyczny kurs
Tabela 8.10. Konstruktory klasy JCheckBox
Konstruktor
Opis
JCheckBox()
Tworzy niezaznaczone pole wyboru, bez ikony i przypisanego tekstu.
JCheckBox(Action a)
Tworzy nowe pole wyboru o właściwościach wskazanych
przez argument a.
JCheckBox(Icon icon)
Tworzy niezaznaczone pole wyboru z przypisaną ikoną.
JCheckBox(Icon icon,
boolean selected)
Tworzy pole wyboru z ikoną, jego stan jest określony
przez argument selected.
JCheckBox(String text)
Tworzy niezaznaczone pole wyboru z przypisanym tekstem.
JCheckBox(String text,
boolean selected)
Tworzy pole wyboru z przypisanym tekstem, którego stan jest
określony przez argument selected.
JCheckBox(String text,
Icon icon)
Tworzy niezaznaczone pole wyboru z przypisaną ikoną oraz tekstem.
JCheckBox(String text,
Icon icon, boolean selected)
Tworzy pole wyboru, z przypisaną ikoną i tekstem, którego stan
jest określony przez argument selected.
Listing 8.43.
import javax.swing.*;
import java.awt.*;
public class Aplikacja extends JFrame {
public Aplikacja() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new GridLayout(3, 2));
JCheckBox
JCheckBox
JCheckBox
JCheckBox
JCheckBox
JCheckBox
cb1
cb2
cb3
cb4
cb5
cb6
=
=
=
=
=
=
new
new
new
new
new
new
JCheckBox("Opcja
JCheckBox("Opcja
JCheckBox("Opcja
JCheckBox("Opcja
JCheckBox("Opcja
JCheckBox("Opcja
1");
2");
3");
4");
5");
6");
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
}
}
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. rozkł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 powierzchnia 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ł zobrazowany w kodzie widocznym na listingu 8.44.
Tabela 8.11. Konstruktory klasy JComboBox
Konstruktor
Opis
JComboBox()
Tworzy pustą listę.
JComboBox(ComboBoxModel aModel)
Tworzy nową listę z modelem danych wskazanym przez
argument aModel.
JComboBox(Object[] items)
Tworzy listę zawierającą dane znajdujące się w tablicy items.
JComboBox(Vector<?> items)
Tworzy listę zawierającą dane znajdujące się w kolekcji items.
Listing 8.44.
import javax.swing.*;
import java.awt.event.*;
public class Aplikacja extends JFrame {
JComboBox<String> cmb;
JLabel label;
414
Java. Praktyczny kurs
Tabela 8.12. Wybrane metody klasy JComboBox
Deklaracja metody
Opis
void addItem(Object anObject)
Dodaje nową pozycję do listy.
Object getItemAt(int index)
Pobiera element listy znajdujący się pod wskazanym indeksem.
int getItemCount()
Pobiera liczbę elementów listy.
int getSelectedIndex()
Pobiera indeks zaznaczonego elementu.
Object getSelectedItem()
Pobiera zaznaczony element.
void insertItemAt(Object
anObject, int index)
Wstawia nowy element we wskazanej pozycji listy.
boolean isEditable()
Zwraca true, jeśli istnieje możliwość edycji listy.
void removeAllItems()
Usuwa wszystkie elementy listy.
void removeItem
(Object anObject)
Usuwa wskazany element listy.
void removeItemAt(int anIndex)
Usuwa element listy znajdujący się pod wskazanym indeksem.
void setEditable
(boolean aFlag)
Ustala, czy lista ma mieć możliwość edycji.
void setSelectedIndex
(int anIndex)
Zaznacza element listy znajdujący się pod wskazanym
indeksem.
void setSelectedItem
(Object anObject)
Zaznacza wskazany element listy.
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);
label = new JLabel("Wybierz pozycję z listy");
label.setBounds(70, 10, 190, 20);
cmb = new JComboBox<String>();
cmb.setBounds (70, 40, 190, 20);
cmb.addItem("Wybierz książkę...");
cmb.addItem("Java. Ćwiczenia praktyczne");
cmb.addItem("Java. Ćwiczenia zaawansowane");
cmb.addItem("Java. Tablice informatyczne");
cmb.addItem("Java. Leksykon kieszonkowy");
cmb.addActionListener(al);
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 listy 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 reprezentowany przez zmienną al). Jest w nim oczywiście zdefiniowana tylko jedna metoda —
actionPerformed. Za pomocą wywołania cmb.getSelectedIndex() pobiera ona najpierw 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ż zaznaczony 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);
416
Java. Praktyczny kurs
Ćwiczenia do samodzielnego wykonania
Ć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, zapisywanie, modyfikację treści).
Skorowidz
A
adapter MouseInputAdapter, 390
aplety, 351, 354
konstrukcja, 355
parametry, 356
tworzenie, 352
aplikacja, 351, 386
Eclipse, 6
jEdit, 6
NetBeans, 6
Notepad++, 6
argumenty
finalne, 162
konstruktorów, 111
metod, 100
ASCII, 270
B
blok
default, 56
else, 48
finally, 192
switch, 55
try…catch, 165, 169–171, 180
błąd
arytmetyczny, 179
implementacji interfejsu, 234
kompilacji, 31, 39, 127, 143, 197, 256
w pętli while, 274
w programie, 275
bufor, 315
C
ciąg
formatujący, 289
znaków quit, 330
czcionki, 359
D
deklaracja, 24
metody, 120
tablicy, 77, 80, 85
typu uogólnionego, 345
zmiennej typu klasowego, 93
deklaracje
proste, 24
wielu zmiennych, 25
dekrementacja, 35
domyślna strona kodowa, 293
domyślne wartości typów danych, 99
dookreślanie typu, 341
dostęp
chroniony, protected, 133
do katalogu, 298
do klasy wewnętrznej, 253
do klasy zewnętrznej, 245
do obiektu, 248
do składowych, 261
prywatny, private, 131
publiczny, public, 130
dynamiczna zmiana pojemności, 327
dynamiczny wektor elementów, 328
działania arytmetyczne, 34
działanie
etykietowanej instrukcji break, 72
etykietowanej instrukcji continue, 70, 72
konstruktorów odziedziczonych, 126
operatora dekrementacji, 37
operatora inkrementacji, 36
pętli for, 77
przeciążonej metody, 203
dziedziczenie, 121, 122
interfejsów, 235, 238
po klasie zewnętrznej, 252
dzielenie przez zero, 173
dźwięki, 383
418
Java. Praktyczny kurs
E
edytor tekstowy, 6
elementy tablic, 76
elipsa, 369
enkapsulacja, 134
etykiety, 71
puste, 405
tekstowe, 404, 405
F
figury geometryczne, 366
finalne
argumenty metod, 162
klasy, 156
metody, 161
pola, 157, 159
formatowanie, 290
G
grafika, 366
H
hermetyzacja, 134
hierarchia
klas, 224
wyjątków, 176
if…else if, 51
Math.sqrt(), 50
return, 97
switch, 53–57
System.out.print, 33, 87
System.out.println, 28
throw, 183, 190
instrukcje sterujące, 47
interfejs, 222, 224
ActionListener, 393, 397
AudioClip, 383
DocumentListener, 408
Drawable, 224, 228, 229
Iterable, 333–335
Iterator, 335
jako typ zwracany, 254
MouseListener, 376–380, 385, 390, 401
MouseMotionListener, 380
WindowListener, 387
interfejsy funkcjonalne, 261, 264
J
jawna konwersja typu, 197
JDK, Java Development Kit, 6
JRE, Java Runtime Environment, 6, 10
K
I
iloczyn logiczny, 43
implementacja
interfejsu, 223
interfejsu MouseListener, 401
wielu interfejsów, 230
importowanie pakietu, 271
inferowanie typu, 341
informacje
o kliknięciach, 379
o stanie załadowania obrazu, 375
inicjalizacja
obiektu, 110
pól finalnych, 160
tablicy, 80, 85
inkrementacja, 35
instalacja
JDK, 9
w systemie Linux, 11
w systemie Windows, 10
instrukcja, 23
break, 55, 66–68
continue, 68–71
if…else, 47
katalogi, 298, 304
pobranie zawartości, 298
tworzenie, 302
usuwanie, 304
klasa, 17, 91
ActionEvent, 394, 398
Aplet, 365
Applet, 352, 383
ArrayList, 328
BufferedInputStream, 321
BufferedOutputStream, 321
BufferedReader, 272, 320
CheckBoxMenu, 398
Color, 363
Component, 403
Console, 288, 289
Dane, 118
DivideByZeroException, 190
Double, 278
Exception, 183
File, 295, 302
FileInputStream, 316
FileOutputStream, 318
FileReader, 316, 318
FileWriter, 318, 411
Font, 359
FontMetrics, 361
Skorowidz
Frame, 386
GeneralException, 189
Graphics, 362, 366
GraphicsEnvironment, 360
ImageObserver, 375
InputStream, 270, 271, 272
Integer, 277
JApplet, 352, 374
JButton, 405
JCheckBox, 411
JCheckBoxMenuItem, 398, 399
JComboBox, 413, 415
JFrame, 386
JLabel, 404
JMenu, 391, 396
JMenuBar, 391
JMenuItem, 391, 392
JPopupMenu, 400
JTextArea, 410
JTextComponent, 407
JTextField, 407, 409
Main, 94
Matcher, 301
MouseEvent, 377, 382
MouseInputAdapter, 390
MouseMotionAdapter, 390
MyFilenameFilter, 300, 301
MyMouseListener, 380
MyWindowAdapter, 389
Object, 201
OutputStreamWriter, 293
Pattern, 301
Polygon, 372
PrintStream, 291
PrintWriter, 293, 294
Punkt, 93, 103
Punkt3D, 124
RandomAccessFile, 307–312
Reader, 272
RuntimeException, 176, 177
Scanner, 286–288
Shape, 208, 213
Stack, 331
StreamTokenizer, 279–286
SwingUtilities, 387
Tablica, 167
uruchomieniowa, 108
WindowAdapter, 389
klasy
abstrakcyjne, 212, 223
anonimowe, 257, 264
bazowe, 122
chronione, 251
finalne, 156
kontenerowe, 328
pakietowe, 141, 249, 250
419
pochodne, 225
potomne, 122
prywatne, 251
publiczne, 141, 250
statyczne, 260
statyczne zagnieżdżone, 239, 260
wewnętrzne, 141, 238, 240, 246, 249
wewnętrzne lokalne, 244
zagnieżdżone, 254
zewnętrzne, 241, 245
klasyfikacja klas, 250
kod
ASCII, 270
pośredni, 15
Unicode, 293
kodowanie, 292
kolory, 362
koło, 369
komenda
java, 13
javac, 13
komentarz, 19
blokowy, 20
liniowy, 21
kompilacja, 14, 15
kompilacja wyrażenia regularnego, 301
kompilator, 6, 15
kompilator javac, 7, 14
komponenty, 402
komunikat o błędzie, 175, 237
konflikt nazw, 232, 237, 238
koniec pliku, 314
konsola, 288
konsola systemowa, 12
konstrukcja apletu, 355
konstruktor, 110, 212
argumenty, 111
bezargumentowy, 111
domyślny, 93, 218
klasy bazowej, 125
klasy potomnej, 125
przeciążanie, 112
wywoływanie metod, 115, 219
konstruktory klasy
JCheckBox, 412
ComboBox, 413
JTextArea, 410
JTextField, 407
OutputStreamWriter, 293
kontenery, 323, 339
kontrola typów, 330, 337, 348
konwersja typów prostych, 195, 196, 199
kopiowanie plików, 312, 314, 320
kroje pisma, 358, 359
420
Java. Praktyczny kurs
L
liczba, 277
linie, 368
lista czcionek, 359, 361
listy rozwijane, 413
M
maszyna wirtualna, 6
menu, 391
kaskadowe, 396, 397
kontekstowe, 400, 401
umożliwiające zaznaczanie, 399
metoda
actionPerformed, 393, 408
actionPreformed, 395
add, 330, 392
addActionListener, 379
addMouseListener, 379, 385
addWindowListener, 387–390
clearRect, 352
createNewFile, 302
delete, 304
dispose, 390, 395
draw, 213
drawImage, 373
drawOval, 369
drawPolygon, 371
drawRectangle, 370
drawShape, 215
drawString, 386
exists, 303, 310
fillOval, 369
fillPolygon, 371
fillRectangle, 370
finalize, 118
get, 326, 343
getActionCommand, 395
getAllFonts, 360
getAudioClip, 384
getAvailableFontFamilyNames, 360
getButton, 377
getCodeBase, 372
getDocumentBase, 372
getFontMetrics, 362
getHeight, 362
getImage, 372, 373
getItem, 397
getLocalGraphicsEnvironment, 360
getMessage(), 174
getParameter, 357
getSource, 394, 399
getValues, 347
getX, 377
getY, 377
hasNext, 335
hasNextInt, 287
imageUpdate, 374
invokeLater, 387
list, 298
main, 106
mkdir, 303
mkdirs, 303
mouseClicked, 378, 380, 385
mouseDragged, 380
next, 335
nextLine, 287
nextToken, 280–282
paint, 362
parseDouble, 278
parseInt, 277
play, 385
printf, 289
read, 270, 271, 313
readLine, 272, 275
remove, 335
resize, 325, 327
save, 411
set, 325, 326
setActionCommand, 395
setBounds, 405
setColor, 362
setDocumentListener, 408
setFont, 362
setJMenuBar, 392
setLayout, 405
setSize, 386
setText, 405
show, 345, 401
size, 325
start, 286
stringWidth, 362
super, 127
System.gc, 118
toString, 202
windowClosing, 389
write, 313
metody, 95
argumenty, 100
domyślne, 227, 229
finalne, 161
interfejsu DocumentListener, 409
interfejsu Iterable, 334
interfejsu MouseListener, 376
interfejsu MouseMotionListener, 380
interfejsu WindowListener, 388
klasy anonimowej, 259
klasy ArrayList, 329
klasy Component, 403, 404
klasy Console, 289
klasy File, 296, 297, 298
klasy FileInputStream, 316
klasy FileOutputStream, 318
Skorowidz
421
klasy FileReader, 317
klasy FileWriter, 318
klasy Graphics, 366
klasy InputStream, 270
klasy JComboBox, 414
klasy PrintStream, 291, 292
klasy RandomAccessFile, 307, 308
klasy Scanner, 286
klasy Stack, 331
prywatne, 210
przeciążanie, 108, 144
przesłanianie, 146
statyczne, 152, 277
wywołanie polimorficzne, 208, 220, 259
wywoływane w konstruktorach, 115, 219
zwracanie wyniku, 97
mnożenie liczb, 284
model RGB, 363
mysza, 376
N
narzędzia, 6
narzędzia JDK, 11
nawias
kątowy, 343
klamrowy, 47, 95, 170
kwadratowy, 75, 80, 82
okrągły, 170, 196
nazwa
klasy, 92
pakietu, 138
pliku, 92
zmiennych, 26
negacja logiczna, 43
nieprawidłowa implementacja interfejsów, 234
niewłaściwy typ danych, 340, 344
niszczenie obiektu, 121
O
obiekt, 17, 94
file, 298
InputStreamReader, 272
jako argument, 102
klasy wewnętrznej, 246, 249
klasy zewnętrznej, 249
outp, 294
strTok, 284
System.in, 269, 272, 287
obsługa
dźwięków, 383
konsoli, 288
menu, 391, 394
myszy, 376
okna, 390
przycisków, 405
zdarzeń, 389, 394, 397
odczyt danych z pliku, 309
odniesienie, 27
odśmiecacz, 120
odtwarzanie dźwięków, 383
odwołanie
do nieistniejącego elementu, 166
do prywatnego pola, 246
do pustej zmiennej obiektowej, 179
odzyskiwanie pamięci, 118
okno wyboru składników JDK, 10
opcja encoding, 31, 293
operacje
na plikach, 306
na tablicach, 74
na zmiennych, 33
operator
+, 28
dekrementacji, 35
inkrementacji, 35
kropki, 94
new, 81, 85, 93
warunkowy, 57
operatory
arytmetyczne, 34
bitowe, 41, 42
logiczne, 42, 43
porównywania, 44, 45
przypisania, 44
P
pakiet, 137, 250
java.awt, 352, 358
java.io, 271
java.util, 328
javax.swing, 352, 402
JDK, 6
parametr
mode, 308
this, 388
parametry
apletu, 356
zdarzenia, 381
pasek menu, 391
pętla, 59
do…while, 63, 64
for, 59–61
foreach, 65, 333
while, 62, 63, 273
pętle zagnieżdżone, 70, 82
pierwiastki równania kwadratowego, 284
plik, 304
index.html, 354
Main.class, 15
Main.java, 14, 95
422
pliki
dźwiękowe, 383
kopiowanie, 312, 314, 320
odczyt danych, 309, 316
o dostępie swobodnym, 307
operacje strumieniowe, 316
tworzenie, 302
usuwanie, 304
zapis danych, 310, 318
pobranie
tokena, 281
zawartości katalogu, 298
pola, 91, 149
finalne, 157
finalne typów prostych, 158
finalne typów referencyjnych, 159
interfejsów, 226
JCheckBox, 411
niezainicjowane, 98
statyczne, 153
tekstowe, 407, 408
pole
nval, 281
sval, 281
ttype, 280
polecenie
chcp, 30
cmd, 12
import, 138
javac, 14
polimorficzne wywoływanie metod, 208, 220, 259
polimorfizm, 195, 206
polskie znaki, 292
ponowne zgłaszanie wyjątków, 187
poprawność danych, 165
późne wiązanie, 204, 206
praklasa, 202
priorytety operatorów, 45
problem kontroli typów, 337
programowanie obiektowe, 91, 195
przechowywanie wielu danych, 323
przechwytywanie wielu wyjątków, 177
przeciążanie
konstruktorów, 112
metod, 108
przeglądanie kontenerów, 333
przekroczenie zakresu wartości, 41
przesłanianie
metod, 144–149
pól, 146, 149, 151
przyciski, 405
przypisanie, 24
punkty, 368
Java. Praktyczny kurs
R
referencja, reference, 27
referencje do obiektu, 102, 114, 207
reprezentacja liczb, 42
RGB, 363
rodzaje
klas wewnętrznych, 246, 249
pól tekstowych, 407
wyjątków, 173
rodziny czcionek, 361
rozmiar tablicy, 78, 326
równanie kwadratowe, 50, 284, 286
rysowanie
elips, 369
figur, 366
kół, 369
linii, 368
prostokątów, 368, 370
punktów, 368
wielokątów, 370, 371
rzeczywisty typ obiektu, 204
rzutowanie
na interfejs, 256
na typ Object, 201
na typy interfejsowe, 254
na typy klasowe, 257
obiektów, 195
typów obiektowych, 128, 197, 201, 341
w górę, 206
S
sekcja finally, 190
sekwencja ucieczki, 32
sekwencje znaków specjalnych, 32
składnia
ExtendedOutside.Inside, 253
Outside.Inside, 253
składowe
klas wewnętrznych, 242
klasy, 92
RGB, 364
statyczne, 152
skrypt startowy powłoki, 13
słowo kluczowe
else, 47
extends, 235
final, 156
implements, 226
package, 137
super, 211
this, 114, 116
void, 96, 100
Skorowidz
423
specyfikator dostępu, 129
private, 131
protected, 133
public, 130
sprawdzanie poprawności danych, 165
stała
EXIT_ON_CLOSE, 387
napisowa, 276
stałe klasy
Color, 363
MouseEvent, 382
stan zmiennej iteracyjnej, 60
standard
CP1250, 292
CP852, 293
Unicode, 30, 293
standardowe
wejście, 269
wyjście, 279
statyczność klasy wewnętrznej, 260
sterta, heap, 94
stos, stack, 94
stosowanie uogólnień, 341
strona kodowa, 293
strona kodowa 1250, 30
struktura
programu, 13
tablicy, 74
tablicy dwuwymiarowej, 80
strumieniowe operacje na plikach, 316
strumień wejściowy, 269
suma logiczna, 43
symbol T, 343
system plików, 295
system wejścia-wyjścia, 269
Ś
ścieżka dostępu do katalogu, 298
średnik, 258
środowisko
programistyczne, 6
uruchomieniowe, 7, 10
T
tablice, 74
dwuwymiarowe, 80
dynamiczne, 326, 328
nieregularne, 84, 89
tekst, 271
terminal, 12
testowanie apletów, 354
tokeny, 279, 280
tworzenie
apletu, 352
aplikacji, 386
etykiet, 405
interfejsów, 222
katalogów, 302
klas wewnętrznych, 239
klasy anonimowej, 257–259
list rozwijalnych, 413
menu, 392
nieregularnej tablicy dwuwymiarowej, 86
obiektów, 105
obiektów klas wewnętrznych, 248
obiektu, 96, 98
obiektu strTok, 281
obiektu wyjątku, 183
pakietów, 137
paska menu, 394
plików, 302
tablicy, 80, 82
wielu klas wewnętrznych, 240
własnych wyjątków, 188
tymczasowa zmienna referencyjna, 395
typ
boolean, 19
byte, 18
char, 19
double, 19
float, 19
int, 18
long, 18
Object, 201
short, 18
typy
arytmetyczne całkowitoliczbowe, 18
arytmetyczne zmiennopozycyjne, 19
danych, 16
generyczne, 336
obiektowe, 26, 197
proste, primitive types, 18
uogólnione, 336
U
Unicode, 293
uogólnianie metod, 345
uogólnienia, 341
uruchamianie apletu, 354
usuwanie
katalogów, 304
komentarzy, 256
plików, 304
W
wartości
domyślne pól, 98
parametru mode, 308
pola ttype, 280
424
wartość
aktualnego tokena, 280
null, 27, 120, 275
warunek zakończenia pętli, 274
wbudowane typy danych, 18
wczesne wiązanie, 208
wczytywanie
danych, 277, 283, 286, 288
grafiki, 372
tekstu, 271, 273, 277
wersje Javy, 7
wiązanie
czasu wykonania, 206
dynamiczne, 206
wielokąty, 370
wiersz
poleceń, 12
tekstu, 273
wirtualna maszyna Javy, 15
właściwość length, 78, 83
wprowadzanie, Patrz wczytywanie
współrzędne
biegunowe, 134
kliknięcia, 380
wybór składników JDK, 10
wyjątek, exception, 165, 169
ArithmeticException, 175, 179, 181
ArrayIndexOutOfBoundsException, 76, 165,
169, 324
ClassCastException, 205, 206, 337
DivideByZeroException, 189
FileNotFoundException, 308, 310
IOException, 271, 303
NullPointerException, 179, 181, 276
NumberFormatException, 277, 278, 365
PatternSyntaxException, 300
UnsupportedEncodingException, 293, 294
UnsupportedOperationException, 335
wyjątki własne, 182, 188
wykonanie programu, 15
wykorzystanie
kontenerów, 339
obiektów, 140
pakietów, 139
wyrażenia
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
Java. Praktyczny kurs
wywołania
konstruktorów, 216
metod, 115, 219
metod klas pochodnych, 204
metod klasy wewnętrznej, 247
metod nieistniejących, 225
metod przesłoniętych, 147
złożone konstruktorów, 219
Z
zagnieżdżanie bloków try…catch, 180, 186
zakończenie działania programu, 274
zakres
wartości zmiennej, 40
typów arytmetycznych, 18
typów zmiennoprzecinkowych, 19
zapis do pliku, 310, 318
zaznaczanie pozycji, 399
zdarzenia
myszy, 377
związane z oknem, 387
zgłaszanie
wyjątku, 182
ponowne wyjątku, 185
zmiana
czcionki, 31
strony kodowej, 30
zmienna środowiskowa
CLASSPATH, 139, 140
PATH, 13
zmienne, 23, 102
referencyjne, 94
typów odnośnikowych, 26
znacznik
<applet>, 353
<object>, 353
znaczniki formatujące printf, 290
znak %, 289
znaki
polskie, 29
specjalne, 32
zwracanie
wielu wartości, 346
wyniku, 97
Descargar