Download as pdf or txt
Download as pdf or txt
You are on page 1of 425

Wszelkie prawa zastrzeżone.

Nieautoryzowane rozpowszechnianie całości


lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione.
Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie
książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie
praw autorskich niniejszej publikacji.

Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi


bądź towarowymi ich właścicieli.

Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte


w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej
odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne
naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION
nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe
z wykorzystania informacji zawartych w książce.

Redaktor prowadzący: Ewelina Burska


Projekt okładki: Studio Gravite/Olsztyn
Obarek, Pokoński, Pazdrijowski, Zaprucki

Wydawnictwo HELION
ul. Kościuszki 1c, 44-100 GLIWICE
tel. 32 231 22 19, 32 230 98 63
e-mail: helion@helion.pl
WWW: http://helion.pl (księgarnia internetowa, katalog książek)

Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie/pkjav4_ebook
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.

Listingi wykorzystane w książce można znaleźć pod adresem:


ftp://ftp.helion.pl/przyklady/pkjav4.zip

ISBN: 978-83-283-0925-8

Copyright © Helion 2015

 Poleć książkę na Facebook.com  Księgarnia internetowa


 Kup w wersji papierowej  Lubię to! » Nasza społeczność
 Oceń książkę

b4ac40784ca6e05cededff5eda5a930f
b
Spis treści
Wstęp .............................................................................................. 5
Rozdział 1. Podstawy ......................................................................................... 9
Instalacja JDK .................................................................................................................. 9
Instalacja w systemie Windows ................................................................................ 10
Instalacja w systemie Linux ..................................................................................... 11
Przygotowanie do pracy z JDK ................................................................................ 11
Podstawy programowania ............................................................................................... 13
Lekcja 1. Struktura programu, kompilacja i wykonanie ........................................... 13
Lekcja 2. Podstawy obiektowości i typy danych ...................................................... 16
Lekcja 3. Komentarze ............................................................................................... 19
Rozdział 2. Instrukcje języka ............................................................................ 23
Zmienne .......................................................................................................................... 23
Lekcja 4. Deklaracje i przypisania ........................................................................... 24
Lekcja 5. Wyprowadzanie danych na ekran ............................................................. 27
Lekcja 6. Operacje na zmiennych ............................................................................. 33
Instrukcje sterujące ......................................................................................................... 47
Lekcja 7. Instrukcja warunkowa if…else ................................................................. 47
Lekcja 8. Instrukcja switch i operator warunkowy ................................................... 53
Lekcja 9. Pętle .......................................................................................................... 59
Lekcja 10. Instrukcje break i continue ...................................................................... 66
Tablice ............................................................................................................................ 74
Lekcja 11. Podstawowe operacje na tablicach .......................................................... 74
Lekcja 12. Tablice wielowymiarowe ........................................................................ 79
Rozdział 3. Programowanie obiektowe. Część I ................................................. 91
Podstawy ........................................................................................................................ 91
Lekcja 13. Klasy, pola i metody ............................................................................... 91
Lekcja 14. Argumenty i przeciążanie metod .......................................................... 100
Lekcja 15. Konstruktory ......................................................................................... 110
Dziedziczenie ............................................................................................................... 121
Lekcja 16. Klasy potomne ...................................................................................... 122
Lekcja 17. Specyfikatory dostępu i pakiety ............................................................ 129
Lekcja 18. Przesłanianie metod i składowe statyczne ............................................ 144
Lekcja 19. Klasy i składowe finalne ....................................................................... 156

b4ac40784ca6e05cededff5eda5a930f
b
4 Java. Praktyczny kurs

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

b4ac40784ca6e05cededff5eda5a930f
b
Wstęp

Krótka historia Javy


Trudno znaleźć osobę interesującą się zagadnieniami informatycznymi, która nie słyszała-
by o Javie. Java jest językiem, który w bardzo krótkim czasie zdobył popularność i uzna-
nie 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óżnorod-
nych 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 kuchen-
kami mikrofalowymi, a więc praktycznie każdym urządzeniem, które zawiera mikropro-
cesor. Stąd właśnie wywodzi się jedna z największych zalet Javy, czyli przenośność
— ten sam program, przynajmniej teoretycznie, będzie bowiem działał na różnych
urządzeniach i platformach sprzętowo-systemowych. Będzie go więc można urucho-
mić i na komputerze PC, i na Macintoshu, i w systemie Windows, i w Uniksie.

Takie były właśnie początki Javy w roku 1990, czyli dwadzieścia kilka lat temu. Język zo-
stał opracowany przez zespół kierowany przez Jamesa Goslinga w firmie Sun Microsys-
tems, choć w rzeczywistości prace były prowadzone na Uniwersytecie Stanford. Pro-
jekt ten był gotowy bardzo szybko, już w roku 1991, jednak tak naprawdę aż do roku
1994 nie udało się go spopularyzować. Z tego też powodu prace zostały zawieszone.

Historia informatyki pełna jest jednak przypadków. Były to lata ogromnej ekspansji i wręcz
eksplozji internetu. Nagle okazało się, że Oak przecież doskonale sprawdzałby się w tak
różnorodnym środowisku, jakim jest globalna sieć. W ten oto sposób w roku 1995 ujrzała
światło dzienne Java, a to, co nastąpiło później, zaskoczyło chyba wszystkich, łącznie
z twórcami tego języka. Java niezwykle szybko, nawet jak na technologię informatyczną,
została zaakceptowana przez społeczność internautów i programistów na całym świe-
cie. Z pewnością nie można tu pominąć wpływu kampanii marketingowej producenta,
nie osiągnęłaby ona jednak sukcesu, gdyby nie niewątpliwe zalety tego języka. Java

b4ac40784ca6e05cededff5eda5a930f
b
6 Java. Praktyczny kurs

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 pro-
gramistó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 wirtual-
na. Wszystkie przykłady prezentowane w tej książce są oparte na pakiecie JDK (Java
Development Kit). Można skorzystać z wersji sygnowanej przez oficjalnego producenta
Javy — firmę Oracle1 — lub rozwijanej na zasadach wolnego oprogramowania wersji
OpenJDK. Wersja oficjalna dostępna jest pod adresami http://java.sun.com (po wpisaniu
tego adresu nastąpi przekierowanie do domeny oracle.com) i http://www.java.com,
a OpenJDK pod adresem http://openjdk.java.net/. Najlepiej korzystać z jak najnowszej
wersji JDK, tzn. 6 (1.6), 7 (1.7), 8 (1.8) lub wyższej (o ile taka będzie dostępna2),
choć podstawowe przykłady będą działać nawet na wiekowej już wersji 1.1. Jeśli będą
stosowane funkcje dostępne jedynie w konkretnej wersji środowiska, będzie to zaznacza-
ne w tekście.

Do testowania apletów przedstawionych w rozdziale 8. można użyć dowolnej przeglądar-


ki 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 me-
chanizm 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 wpi-
sywanie tekstu programów. Może to być nawet tak prosty produkt jak np. Notatnik
z Windowsa. Lepszym rozwiązaniem byłby jednak bardziej rozbudowany produkt oferu-
jący takie udogodnienia jak kolorowanie składni i numerowanie wierszy kodu. Dla śro-
dowiska Windows można polecić np. doskonały i darmowy Notepad++ (http://notepad-
-plus-plus.org/). Dobrym rozwiązaniem będzie też wieloplatformowy jEdit (http://www.
jedit.org/), który został napisany w… Javie.

Istnieje również możliwość zastosowania zintegrowanych graficznych środowisk progra-


mistycznych, 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 roz-
wiązanie pozwoli najpierw poznać dobrze sam język, a dopiero potem bardziej za-
awansowane ś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.

b4ac40784ca6e05cededff5eda5a930f
b
Wstęp 7

Wersje Javy
Pierwsza wersja Javy pojawiła się w roku 1995 i nosiła numer 1.0, większą popularność
zdobyła jednak dopiero usprawniona wersja 1.1 z roku 1997. Po kolejnych dwóch latach
(rok 1999) światło dzienne ujrzała Java 1.2, która ze względu na znaczne usprawnienia
(zawierała już wtedy ok. 60 pakietów i ponad 1500 klas) została nazwana Platformą Java
2 (ang. Java 2 Platform). W ramach projektu Java 2 powstały trzy wersje narzędzi
JDK i JRE3: 1.2 (1999), 1.3 (2000) i 1.4 (2002), a każda z nich miała od kilku do kil-
kunastu podwersji. Kolejnym krokiem w rozwoju projektu była wersja 1.5 (rok 2004),
która, jak się wydaje, ze względów czysto marketingowych została przemianowana
na 5.0; określa się ją również mianem Java 5. W 2006 roku pojawiła się Java 6 (1.6)
zawierająca nowe rozwiązania, takie jak obsługa typów uogólnionych (ang. generics).
Zrezygnowano też wtedy z używania określenia Java 2 Platform. W roku 2010 pojawiła się
kolejna wersja — 7 (1.7). Najnowsza w chwili opracowywania materiałów do tej książki
była wersja 8 (czyli inaczej 1.8) z roku 2014. Dodatkowo JDK występuje w trzech
osobnych edycjach: standardowej (ang. Standard Edition), dla dużych organizacji (ang.
Enterprise Edition) i dla urządzeń mobilnych (ang. Mobile Edition); są one oznaczane
skrótami SE, EE i ME. Określenie Java SE 8 oznacza w związku z tym platformę Java
Standard Edition w wersji 8. Warto też zauważyć, że wewnętrzna numeracja narzędzi
(widoczna np. w niektórych opcjach kompilatora javac) wciąż bazuje na „starej”, logicz-
nej numeracji (Java 2 5.0 oznacza to samo co Java 1.5, Java 6 — to samo co Java 1.6,
Java 7 — to samo co Java 1.7, a Java 8 — to samo co Java 1.8).

3
Java Runtime Environment — środowisko uruchomieniowe potrzebne do uruchamiania programów
w Javie.

b4ac40784ca6e05cededff5eda5a930f
b
8 Java. Praktyczny kurs

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 1.
Podstawy
Przed wypłynięciem na szerokie wody programowania w Javie trzeba zapoznać się z pod-
stawami. W tym rozdziale zostanie pokazane, jak wygląda struktura programu w Javie,
czym jest i jak przebiega kompilacja oraz jak taki program uruchomić. Przedstawione
zostaną podstawy obiektowości, czyli najważniejsze zagadnienia z dziedziny progra-
mowania obiektowego, oraz pojęcie typu danych. Znajdzie się tu także krótkie omówie-
nie typów danych występujących w Javie. Okaże się, w jaki sposób umieścić w progra-
mie komentarz, co, wbrew pozorom, jest ważną czynnością, ponieważ pozwala na
dokumentowanie kodu, poprawia jego czytelność i ułatwia analizę.

Czytelnicy, którzy znają podstawy programowania, mogą spokojnie pominąć ten roz-
dział i przejść do drugiego, aby zobaczyć, jak w Javie wyglądają typowe konstrukcje
programistyczne, takie jak pętle i instrukcje warunkowe, deklaracje zmiennych oraz
tablice. Natomiast osoby, które dopiero zaczynają swoją przygodę z programowaniem,
powinny przeczytać ten rozdział dokładnie — nie jest on długi i stanowi wprowadze-
nie w świat Javy. Nie należy się przejmować, jeśli nie wszystko od razu będzie całkiem
jasne. Z wieloma pojęciami i konstrukcjami programistycznymi trzeba się po prostu
oswoić, wykonując praktyczne przykłady. Temu przecież służy ta książka.

Instalacja JDK
Aby móc wykonywać prezentowane w książce przykłady, trzeba najpierw zainstalo-
wać w systemie pakiet programistyczny nazywany JDK, czyli Java Development Kit.
Instalacja przebiega typowo, tak jak w przypadku dowolnej innej aplikacji dla danego
systemu operacyjnego, więc nikomu z pewnością nie przysporzy problemów. Poniżej
znajdują się krótkie opisy tego procesu dla JDK 8 i systemów Windows oraz Linux.

b4ac40784ca6e05cededff5eda5a930f
b
10 Java. Praktyczny kurs

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 Files\Java\jdk1.8.0). Po
kliknięciu Next rozpocznie się właściwy proces instalacji.

Rysunek 1.1.
Okno wyboru
składników JDK

Jeżeli w oknie wyboru JDK nie została wyłączona opcja instalacji JRE (ang. Java
Runtime Environment), czyli środowiska uruchomieniowego pozwalającego na uru-
chamianie aplikacji i apletów napisanych w Javie, a JRE nie było wcześniej instalowane
w systemie jako osobny produkt, w trakcie instalacji JDK na ekranie pojawi się okno
pozwalające na wybór katalogu docelowego JRE (rysunek 1.2). Po kliknięciu Next
JRE również zostanie zainstalowane.

Rysunek 1.2.
Okno wyboru
składników JDK

1
Uwaga: Java 8 oficjalnie nie współpracuje z systemami Windows poniżej wersji 7.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 1.  Podstawy 11

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. /usr/java), wydając jedno z poleceń:
cp ./jdk-8-linux-i586.tar.gz /usr/java/
mv ./jdk-8-linux-i586.tar.gz /usr/java/

Następnie można przejść do tego katalogu, wydając komendę:


cd /usr/java

oraz rozpakować plik, wpisując polecenie:


tar xvfz jdk-8-linux-i586.tar.gz

Wtedy w bieżącym katalogu (w omawianym przykładzie będzie to /usr/java/) zostanie


utworzony podkatalog o nazwie jdk1.8.0, w którym znajdą się pliki pakietu.

Alternatywnie można od razu rozpakować plik do wybranego katalogu, korzystając


z opcji C archiwizatora tar, np.:
tar xvfz jdk-8-linux-i586.tar.gz –C /usr/java

W drugim przypadku (pakiet RPM) do dyspozycji jest plik o nazwie:


jdk-wersja-linux-i586-rpm

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

b4ac40784ca6e05cededff5eda5a930f
b
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 Linuksie

Jeśli pracujemy w systemie Windows, wciskamy na klawiaturze kombinację klawiszy


Windows+R2, w polu Otwórz (w niektórych wersjach systemu — Uruchom) wpisujemy
cmd lub cmd.exe3 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
Start4. Można też w dowolny inny sposób uruchomić aplikację cmd.exe. Pojawi się
wtedy okno wiersza poleceń (wiersza polecenia), w którym będzie można wydawać
komendy. Jego wygląd dla systemów z rodziny Windows 8 został przedstawiony na
rysunku 1.5 (w innych wersjach wygląda ono bardzo podobnie).

Rysunek 1.4.
Uruchamianie wiersza
poleceń w systemie
Windows

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
Files\Java\jdk1.8.0\bin lub /usr/java/jdk1.8.0/bin/ — zależnie od użytego systemu).

2
Klawisz funkcyjny Windows jest też opisywany jako Start.
3
W starszych systemach (Windows 98, Me) należy uruchomić aplikację command.exe
(Start/Uruchom/command.exe).
4
W systemach Vista i 7 pole Uruchom standardowo nie jest dostępne w menu startowym, ale można je
do niego dodać, korzystając z opcji Dostosuj.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 1.  Podstawy 13

Dzięki temu nie będzie trzeba za każdym razem podawać pełnej ścieżki dostępu, aby
uruchomić kompilator czy też inne narzędzie. Ta czynność powinna zostać wykonana
automatycznie, jeśli został wykorzystany instalator w Windowsie lub pakiet RPM
w Linuksie. Jeżeli jednak w wierszu poleceń nie są rozpoznawane komendy javac i java,
aktualizację zmiennej PATH trzeba przeprowadzić samodzielnie lub też za każdym razem
przy wywoływaniu tych poleceń podawać pełną ścieżkę dostępu, np. w Windowsie:
C:\Program Files\Java\jdk1.8.0\bin\javac

lub w Linuksie:
/usr/java/jdk1.8.0/bin/javac

Aktualizacja zmiennej środowiskowej PATH dla bieżącej sesji konsoli w systemie Win-
dows polega na wydaniu polecenia o ogólnej postaci:
path=%path%;ścieżka_dostępu

np.:
path=%path%;c:\Program Files\java\jdk1.8.0\bin

Aby natomiast wykonać tę operację w Linuksie w przypadku powłoki systemowej


bash, trzeba wydać polecenie:
PATH=$PATH:/ścieżka_dostępu

np.:
PATH=$PATH:/usr/java/jdk1.8.0/bin/

Jeżeli zmiany ścieżki mają być zapisane na stałe, należy skorzystać z odpowiednich
mechanizmów systemowych. W Linuksie jest to dopisanie odpowiedniego polecenia do
skryptu startowego powłoki (np. .bash_profile), w Windowsie w oknie właściwości sys-
temu należy odnaleźć panel zmiennych środowiskowych i tam wprowadzić modyfikację
do zmiennej PATH.

Podstawy programowania
Lekcja 1. Struktura programu, kompilacja
i wykonanie
Większość kursów programowania zaczyna się od napisania prostego programu, któ-
rego zadaniem jest jedynie wyświetlenie napisu na ekranie komputera. To bardzo dobry
początek pozwalający oswoić się ze strukturą kodu, wyglądem instrukcji oraz proce-
sem kompilacji. Jak zatem wyglądałby taki program w Javie? Jest zaprezentowany na
listingu 1.1.

b4ac40784ca6e05cededff5eda5a930f
b
14 Java. Praktyczny kurs

Listing 1.1.
class Main {
public static void main(String args[]) {
System.out.println("To jest napis.");
}
}

Kod widoczny na listingu to tak zwany kod źródłowy, czyli zapisany w języku progra-
mowania. Aby taki program wykonać, najpierw trzeba przetworzyć go na instrukcje zro-
zumiałe dla danego procesora (ściślej: dla danej platformy sprzętowo-systemowej).
Proces ten nazywamy kompilacją. Kompilacja z kolei jest wykonywana przez program
nazywany kompilatorem.

Pakiet JDK oczywiście zawiera odpowiedni kompilator — javac (ang. java compiler).
Program z listingu 1.1 należy zapisać w pliku o nazwie Main.java, a następnie podać
tę nazwę jako parametr dla kompilatora wywołanego z wiersza poleceń, pisząc:
javac Main.java

Należy przy tym zwrócić uwagę, aby to polecenie wydawać, gdy katalogiem bieżą-
cym jest ten z plikiem Main.java (aby zmienić katalog bieżący, używa się polecenia cd,
np. cd c:\java\). Jeżeli plik jest w innym katalogu niż bieżący, trzeba dodatkowo
podać jego lokalizację. Przykładowo jeśli Main.java jest zapisany na dysku c: w ka-
talogu java, to komenda kompilująca może mieć postać:
javac c:\java\Main.java

Jeśli po wykonaniu polecenia javac Main.java na ekranie pojawi się komunikat o nie-
rozpoznanym poleceniu javac (rysunki 1.6 i 1.7), należy postąpić zgodnie z opisem
przedstawionym w punkcie „Przygotowanie do pracy z JDK” lub przy kompilacji podać
pełną ścieżkę dostępu do kompilatora. Komenda będzie miała wtedy postać (o ile
kompilator znajduje się w lokalizacji c:\Program Files\java\jdk1.8.0\bin):
c:\Program Files\java\jdk1.8.0\bin\javac Main.java

lub (o ile konieczne jest również podanie pełnej ścieżki dostępu do pliku Main.java):
c:\Program Files\java\jdk1.8.0\bin\javac c:\java\Main.java

Rysunek 1.6. Komunikat o nierozpoznanym poleceniu javac w Windows XP

b4ac40784ca6e05cededff5eda5a930f
b
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 zdziwienia, napis: To jest
napis. (rysunek 1.8).

Rysunek 1.8.
Kompilacja i wykonanie
pierwszego programu
w Javie

Zarówno przy pisaniu kodu źródłowego, jak i przy wydawaniu komend w wierszu pole-
ceń należy zwrócić uwagę na wielkość wpisywanych liter. Jakakolwiek pomyłka w więk-
szości przypadków spowoduje, że kompilacja się nie uda.

W tym miejscu pada zazwyczaj pytanie, czemu do uruchomienia skompilowanego pro-


gramu używa się polecenia 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 Main.class to polecenie oznaczające: uruchom wirtualną ma-
szynę Javy i przekaż jej do wykonania kod zawarty w pliku Main.class.

b4ac40784ca6e05cededff5eda5a930f
b
16 Java. Praktyczny kurs

Każdy program w Javie składa się ze zbioru tzw. klas (to pojęcie zostanie bliżej wyjaśnio-
ne w lekcji 2. oraz w rozdziale 3.). Każda klasa powinna być zapisana w oddzielnym
pliku o nazwie takiej samej jak nazwa klasy5 i rozszerzeniu java. Zatem klasa o nazwie
Main musi znajdować się w pliku o nazwie Main.java, natomiast b-kod tej klasy musi
znajdować się w pliku Main.class. Na potrzeby pierwszych dwóch rozdziałów przyj-
miemy, że każdy program musi mieć dokładnie taką strukturę jak ta przedstawiona na li-
stingu 1.2. Jej dokładne znaczenie będzie wyjaśnione dopiero w rozdziale 3. Na razie
trzeba uwierzyć na słowo, że wykonywanie takiego programu rozpocznie się w miej-
scu oznaczonym // i właśnie tam należy wpisywać instrukcje przeznaczone do wykonania
(instrukcję można traktować jako polecenie dla komputera wydane w danym języku
programowania).

Listing 1.2.
class Main {
public static void main(String args[]) {
//tutaj instrukcje do wykonania
}
}

Ćwiczenia do samodzielnego wykonania

Ćwiczenie 1.1.
Napisz program wyświetlający na ekranie napis: To jest mój drugi program. Skom-
piluj go i uruchom.

Ćwiczenie 1.2.
Zapisz kod z listingu 1.1 w pliku o rozszerzeniu innym niż java, np. txt. Spróbuj dokonać
kompilacji tego pliku, zaobserwuj, co się stanie.

Lekcja 2. Podstawy obiektowoś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.

5
O tym, że nie jest to tak do końca prawda, przekonamy się w rozdziałach 3. i 5. podczas omawiania
rodzajów klas. Na razie przyjmijmy, że każda klasa musi znajdować się w oddzielnym pliku zgodnym
z jej nazwą.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 1.  Podstawy 17

Klasy i obiekty
Każdy program w Javie składa się z klas, które są z kolei opisami obiektów. To podsta-
wowe pojęcia związane z programowaniem obiektowym. Będzie o tym mowa w roz-
dziale 3. Zanim jednak pozna się dokładnie klasy i obiekty, lepiej najpierw zapoznać
się z podstawowymi konstrukcjami języka Java. Osoby, które nie zetknęły się dotychczas
z programowaniem obiektowym, mogą potraktować obiekt jako pewien byt programi-
styczny. Może on przechowywać dane i wykonywać operacje, czyli różne zadania. Klasa
to z kolei definicja, opis takiego obiektu.

Skoro klasa definiuje obiekt, jest również jego typem. Czym jest typ obiektu? Oto jedna
z definicji: „Typ jest przypisany zmiennej, wyrażeniu lub innemu bytowi programistycz-
nemu (danej, obiektowi, funkcji, procedurze, operacji, metodzie, parametrowi, modułowi,
wyjątkowi, zdarzeniu). Specyfikuje on rodzaj wartości, które może przybierać ten byt.
(…) Jest to również ograniczenie kontekstu, w którym odwołanie do tego bytu może być
użyte w programie”6. Innymi słowy, typ obiektu określa po prostu, czym jest dany
obiekt.

Obiektem może być tak naprawdę wszystko. W życiu realnym tym mianem określimy
stół, krzesło, komputer, dom, samochód, radio… Każdy z obiektów ma pewne swoje cechy,
właściwości, które go opisują: wielkość, kolor, powierzchnię, wysokość. Co więcej,
każdy obiekt może składać się z innych obiektów (rysunek 1.9). Na przykład miesz-
kanie składa się z poszczególnych pomieszczeń, z których każde może być obiektem;
w każdym pomieszczeniu mamy z kolei inne obiekty: sprzęty domowe, meble itd.

Rysunek 1.9.
Obiekt może zawierać
inne obiekty

Obiekty, oprócz tego, że mają właściwości, mogą wykonywać różne funkcje, zadania.
Innymi słowy, każdy obiekt ma przypisany pewien zestaw poleceń, które potrafi wyko-
nywać. Na przykład samochód wypełnia polecenia „uruchom silnik”, „zgaś silnik”, „skręć
w prawo”, „przyspiesz” itp. Funkcje te składają się na interfejs udostępniany przez
ten samochód. Dzięki interfejsowi można wpływać na zachowanie samochodu i wy-
dawać mu polecenia.

W programowaniu jest bardzo podobnie. Za pomocą klas programista stara się opisać
obiekty, ich właściwości, zbudować konstrukcje, interfejs, dzięki któremu będzie można

6
K. Subieta, Wytwarzanie, integracja i testowanie systemów informatycznych, PJWSTK, Warszawa 1997.

b4ac40784ca6e05cededff5eda5a930f
b
18 Java. Praktyczny kurs

wydawać polecenia realizowane potem przez obiekty. Obiekt powstaje jednak dopie-
ro w trakcie działania programu jako instancja (wystąpienie, egzemplarz) danej klasy.
Obiektów danej klasy może być bardzo dużo. Przykładowo: jeśli klasą będzie Samochód,
to instancją tej klasy będzie konkretny egzemplarz o danym numerze seryjnym.

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 ist-
nieje 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 wa-
runków w instrukcjach if, a także w pętlach i innych konstrukcjach programistycznych.
Zostanie to pokazane na konkretnych przykładach już w rozdziale 2.

Lekcja 3. Komentarze
W większości języków programowania dostępna jest konstrukcja komentarza, która
służy do opisywania kodu źródłowego w języku naturalnym. Innymi słowy, służy do
wyrażenia tego, co programista miał na myśli, stosując daną instrukcję programu. Choć
początkowo komentowanie kodu źródłowego programu może wydawać się zupełnie
niepotrzebne, okazuje się, że jest to bardzo pożyteczny nawyk. Bardzo często bowiem
bywa tak, że po pewnym czasie sam programista ma problemy z analizą napisanego
przez siebie programu, nie mówiąc już o innych osobach, które miałyby wprowadzać
poprawki czy modyfikacje.

8
W tabeli podano wartości przybliżone. Zapis 3,4e38 (notacja wykładnicza, naukowa) oznacza
3,4×1038.

b4ac40784ca6e05cededff5eda5a930f
b
20 Java. Praktyczny kurs

W Javie istnieją dwa rodzaje komentarzy, oba zapożyczone z języków C i C++. Są to:
 komentarz blokowy,
 komentarz liniowy (wierszowy).

Komentarz blokowy
Komentarz blokowy rozpoczyna się od znaków /* i kończy znakami */. Wszystko, co
znajduje się pomiędzy tymi znakami, jest traktowane przez kompilator jako komen-
tarz i pomijane w procesie kompilacji. Przykład takiego komentarza jest widoczny na
listingu 1.3.

Listing 1.3.
class Main{
public static void main(String args[]){
/*
To mój pierwszy program w Javie
Wyświetla on na ekranie napis
*/
System.out.println("To jest napis.");
}
}

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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 1.  Podstawy 21

Listing 1.5.
class Main {
public static void main(String args[]) {
/*
Komentarzy blokowych nie
/*w tym miejscu wystąpi błąd*/
wolno zagnieżdżać
*/
System.out.println ("To jest napis.");
}
}

Wbrew pozorom to nie jest całkiem hipotetyczna sytuacja. Może się bowiem zdarzyć,
że programista nie zauważy, że w komentarz blokowy został ujęty większy fragment
kodu, i w jego środku ponownie zastosuje ten typ komentarza (choć trudno o to przy
korzystaniu z dobrego edytora programistycznego z kolorowaniem składni — wtedy
blok komentarza jest wyraźnie widoczny). Ważne, aby w takiej sytuacji od razu rozpoznać
typ błędu i wprowadzić odpowiednie poprawki.

Komentarz liniowy (wierszowy)


Komentarz liniowy zaczyna się od znaków // i obowiązuje do końca danej linii pro-
gramu. 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 ko-
mentarza 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 przy-
datne. Komentarz liniowy może natomiast znaleźć się w środku komentarza blokowego:
/*
//ta konstrukcja jest poprawna
*/

b4ac40784ca6e05cededff5eda5a930f
b
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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.
Instrukcje języka
Java, tak jak każdy język programowania, jest wyposażona w szereg podstawowych in-
strukcji, które pozwalają programiście na wykonywanie najróżniejszych operacji. Ten
rozdział jest im w całości poświęcony. Przedstawione zostanie pojęcie zmiennej oraz to,
w jaki sposób należy deklarować zmienne i jakie operacje można na nich wykonywać.
Okaże się, jak wykorzystywać typy danych prezentowane w rozdziale 1. (przy opisie
zmiennych nie pojawi się jednak dokładne omówienie zmiennych obiektowych, które
zostaną opisane bliżej dopiero w kolejnym rozdziale).

Następnie omówimy występujące w Javie instrukcje, które sterują wykonywaniem pro-


gramu. 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, nato-
miast typ określa, jakiego rodzaju dane zmienna może przechowywać (zmienne moż-
na więc potraktować jak nazwane „pudełka” przechowujące dane). Podstawowe typy
danych zostały przedstawione w lekcji 2. (w rozdziale 1.). Lekcja 4. jest poświęcona
problemowi deklaracji i przypisywania wartości zmiennym, lekcja 5. — wyświetlaniu
wartości zmiennych (ale także znaków specjalnych i napisów) na ekranie, natomiast
najdłuższa w tym podrozdziale lekcja 6. pokaże, jakie operacje (arytmetyczne, logiczne,
bitowe) można wykonywać na zmiennych.

b4ac40784ca6e05cededff5eda5a930f
b
24 Java. Praktyczny kurs

Lekcja 4. Deklaracje i przypisania


Lekcja 4. jest poświęcona deklaracjom zmiennych oraz przypisywaniu danych zade-
klarowanym 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ć zadekla-
rowana. Deklaracja polega na podaniu typu oraz nazwy zmiennej. Ogólnie taka konstruk-
cja 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 in-
strukcją).

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 po-
zwala na przechowywanie liczb całkowitych w zakresie od –232 (–2 147 483 648) do
232–1 (2 147 483 647), można zatem przypisać tej zmiennej dowolną liczbę mieszczą-
cą się w tym przedziale. Przypisanie takie odbywa się za pomocą znaku równości. Jeśli
więc chcemy, aby zmienna liczba zawierała wartość 100, napiszemy:
liczba = 100;

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.

b4ac40784ca6e05cededff5eda5a930f
b
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 mo-
mencie, 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;
}
}

b4ac40784ca6e05cededff5eda5a930f
b
26 Java. Praktyczny kurs

Powstały tutaj w sumie trzy zmienne: jedna typu int (liczba1) oraz dwie typu byte
(liczba2, liczba3). Przypomnijmy, że typ byte pozwala na przechowywanie wartości
całkowitych z zakresu od –128 do 127 (lekcja 2.). Jedynie zmienna liczba3 została zaini-
cjowana już w trakcie deklaracji i otrzymała wartość 100. Inicjacja zmiennych liczba1
i liczba2 odbyła się po deklaracji i otrzymały one wartości odpowiednio: 10000 i 25.

Nazwy zmiennych
Przy nazywaniu zmiennych2 obowiązują pewne zasady, których należy przestrzegać.
Nazwa taka może składać się z liter (zarówno małych, jak i wielkich), cyfr oraz znaku
podkreślenia3, nie może jednak zaczynać się od cyfry. Do tej zasady trzeba się bez-
względnie stosować, gdyż umieszczenie innych znaków w nazwie (np. dwukropka, wy-
krzyknika itp.) spowoduje natychmiast błąd kompilacji (rysunek 2.1).

Rysunek 2.1.
Niezgodna z zasadami
nazwa zmiennej
powoduje błąd
kompilacji

Można wykorzystać dowolne znaki będące literami, również te spoza ścisłego alfabetu ła-
cińskiego. Dopuszczalne jest zatem stosowanie wszelkich znaków narodowych (w tym
oczywiście polskich). To, czy będą one używane, zależy wyłącznie od indywidualnych
preferencji programisty i (lub) od specyfiki danego projektu programistycznego4.

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ę wiel-
kimi 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. refe-
rence types, object types), deklaruje się w sposób bardzo podobny do zmiennych za-
prezentowanych 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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 27

w przypadku typów prostych tworzy się zmienną, z której od razu można korzystać.
W przypadku typów referencyjnych zostanie w ten sposób zadeklarowane jedynie odnie-
sienie, inaczej referencja (ang. reference), któremu domyślnie zostanie przypisana
wartość pusta, nazywana w Javie null (w Pascalu stosowane jest słowo nil). Zmien-
nej referencyjnej po deklaracji należy przypisać odniesienie do utworzonego oddziel-
ną instrukcją obiektu. Dopiero wtedy można zacząć z niej korzystać.

Dla osób, które nie zetknęły się z językami obiektowymi takimi jak C++ czy Object
Pascal, jest to zapewne mało zrozumiałe. Pierwsze praktyczne przykłady zostaną
przedstawione już w tym rozdziale, w lekcji omawiającej tablice, natomiast dokład-
niejsze wyjaśnienie znajdzie się w rozdziale 3. opisującym podstawy programowania
obiektowego.

Ćwiczenia do samodzielnego wykonania

Ćwiczenie 4.1.
Zadeklaruj i jednocześnie zainicjuj dwie zmienne typu short. Nazwy zmiennych i przypi-
sywane wartości możesz wybrać dowolnie. Pamiętaj o zasadach nazewnictwa zmiennych
oraz zakresie wartości, jakie mogą być reprezentowane przez typ short.

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, wy-
maga 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 rzeczy-
wiś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.

b4ac40784ca6e05cededff5eda5a930f
b
28 Java. Praktyczny kurs

Zmodyfikujmy zatem program z listingu 2.2 tak, aby deklaracja i inicjalizacja odbywały
się w jednej linii, oraz wyświetlmy wartość zmiennej liczba na ekranie. Zadanie to można
zrealizować za pomocą kodu przedstawionego na listingu 2.4. Wynik jego działania za-
prezentowano z kolei na rysunku 2.2.

Listing 2.4.
class Main {
public static void main(String args[]) {
int liczba = 100;
System.out.println(liczba);
}
}

Rysunek 2.2.
Wyświetlenie na ekranie
wartości zmiennej typu int

W jaki sposób poradzić sobie, kiedy chce się jednocześnie mieć na ekranie zdefiniowany
przez siebie ciąg znaków oraz wartość danej zmiennej? Można dwukrotnie użyć instrukcji
System.out.println. Lepszym pomysłem jest jednak zastosowanie operatora + (plus)
w postaci:
System.out.println("napis" + nazwa_zmiennej);

Konkretny przykład zastosowania takiej konstrukcji jest widoczny na listingu 2.5, a wynik
jego działania na rysunku 2.3.

Listing 2.5.
class Main {
public static void main(String args[]) {
int liczba = 100;
System.out.println("Zmienna liczba ma wartość " + liczba);
}
}

Rysunek 2.3.
Zastosowanie operatora +
do jednoczesnego
wyświetlenia napisu
i wartości zmiennej

Jeśli wartość zmiennej ma znaleźć się w środku napisu albo też w jednej linii mają
być wyświetlone wartości kilku zmiennych, wystarczy kilkukrotnie użyć operatora +

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 29

i w ten sposób złożyć ciąg znaków, który ma się znaleźć na ekranie, z mniejszych
kawałków. Możliwa jest zatem konstrukcja w postaci:
System.out.println("napis1" + zmienna1 + "napis2" + zmienna2);

Załóżmy więc, że w programie zostaną zadeklarowane dwie zmienne typu short o na-
zwach pierwszaLiczba oraz drugaLiczba, o wartościach początkowych równych 25 i 75.
Zadaniem będzie wyświetlenie napisu: Wartość zmiennej pierwszaLiczba to 25,
a wartość zmiennej drugaLiczba to 75. Program wykonujący opisane czynności jest
przedstawiony na listingu 2.6.

Listing 2.6.
class Main {
public static void main(String args[]) {
short pierwszaLiczba = 25;
short drugaLiczba = 75;
System.out.println(
"Wartość zmiennej pierwszaLiczba to " +
pierwszaLiczba +
", a wartość zmiennej drugaLiczba to " +
drugaLiczba +
"."
);
}
}

Pewnym zaskoczeniem może być rozbicie instrukcji wyświetlającej dane aż na siedem li-
nii. Powód jest prosty: pełna linia ze względu na swoją długość nie zmieściłaby się ani na
ekranie, ani na wydruku. Lepiej więc samodzielnie podzielić ją na mniejsze części,
aby poprawić czytelność listingu. Przy takim podziale najlepiej kierować się zasadą,
że nie wolno przedzielić łańcucha znaków ujętego w cudzysłów, natomiast w każdym
innym miejscu, gdzie występuje spacja, można zamiast niej postawić znak końca linii
(przejść do nowego wiersza po wciśnięciu klawisza Enter).

Polskie znaki na konsoli Windows


Jeśli środowiskiem, z którego korzystamy, jest system Windows, przy próbie uzyska-
nia 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ć te-
stowy 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 sys-
temu 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: ĄĆĘŁŃÓŚŹŻ");
}
}

b4ac40784ca6e05cededff5eda5a930f
b
30 Java. Praktyczny kurs

Rysunek 2.4.
Uzyskanie polskich
znaków na konsoli
Windows wymaga
dodatkowych zabiegów

Skąd problem? Otóż podczas pisania treści programu np. w Notatniku polskie znaki
są kodowane w standardzie Windows 1250 (strona kodowa 1250). W Javie jest stosowany
system Unicode. Z kolei na konsoli Windows jest standardowo wykorzystywana stro-
na kodowa 852. Z przekodowaniem znaków z kodu źródłowego ze strony kodowej
1250 na standard Unicode kompilator radzi sobie sam, przyjmuje on jednak, że wyprowa-
dzanie znaków ma się odbywać również przy użyciu strony kodowej 1250 (a nie 852).
To właśnie powoduje, że zamiast polskich liter widać nieprzyjemne krzaczki (choć
w niektórych wersjach JDK i Windows, np. w Java 8 i Windows 8, kodowanie jest
poprawne).

Co zatem można w tej sprawie zrobić? Jednym ze sposobów jest zastosowanie niestan-
dardowego sposobu wyprowadzania znaków na ekran (za pomocą dodatkowych obiek-
tów). Ponieważ wymaga to wykonania szeregu instrukcji, które byłyby w tej chwili nie-
zrozumiałe, sposób ten będzie pokazany dopiero w rozdziale 6., w którym omówimy
system wejścia-wyjścia. Na szczęście drugim rozwiązaniem, i to bardzo prostym w reali-
zacji, jest zmiana strony kodowej oraz czcionki stosowanej w konsoli (wierszu poleceń)
systemu Windows.

W tym celu, po pierwsze, wydajemy polecenie chcp (ang. change code page), podając
numer strony kodowej, czyli: chcp 1250 (rysunek 2.5), a następnie z menu systemo-
wego konsoli wybieramy pozycję Właściwości oraz zakładkę Czcionka (rysunek 2.6).
Rodzaj czcionki należy zmienić na True Type (np. czcionka Lucida Console). Od tej
chwili polskie znaki będą wyświetlane poprawnie. Warto jednak pamiętać, że o ile zmiany
czcionki tym sposobem można dokonać na stałe (czyli każde kolejne okno będzie korzy-
stało z tego ustawienia, jeśli taka opcja została wybrana przy zatwierdzaniu zmian), o tyle
ustawienie strony kodowej za pomocą polecenia chcp dotyczy tylko okna bieżącego.

Rysunek 2.5.
Polecenie chcp pozwala
na zmianę strony
kodowej konsoli

Trzeba też wiedzieć, że kody źródłowe programów (np. treści listingów zapisywane
w plikach z rozszerzeniem .java) również muszą być zapisane w odpowiednim kodowa-
niu. Dla Windowsa kompilator przyjmuje standardowo, że ma do czynienia z kodowa-
niem CP 1250 (Windows-1250). Jeżeli jest to inny standard, np. UTF-8, przy kompilacji
mogą pojawić się komunikaty o błędach podobne do przedstawionych na rysunku 2.7.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 31

Rysunek 2.6.
Okno Właściwości
pozwala na zmianę
rodzaju używanej
czcionki

Rysunek 2.7. Niewłaściwe kodowanie pliku z kodem źródłowym spowodowało błędy kompilacji

W takiej sytuacji najlepiej wskazać kompilatorowi, z jakim standardem ma do czynie-


nia, 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

b4ac40784ca6e05cededff5eda5a930f
b
32 Java. Praktyczny kurs

Znaki specjalne
Wiadomo już, że aby wyświetlić na ekranie napis, należy ująć go w znaki cudzysłowu
oraz skorzystać z instrukcji System.out.println, np. System.out.println("napis").
Może więc nasunąć się pytanie: w jaki sposób wyprowadzić na ekran sam znak cudzy-
słowu, skoro jest on częścią instrukcji? Odpowiedź jest prosta: należy go poprzedzić
znakiem ukośnika, tak jak zaprezentowano na listingu 2.8.

Listing 2.8.
class Main {
public static void main(String args[]) {
System.out.println("Oto znak cudzysłowu: \"");
}
}

Jest to tak zwana sekwencja ucieczki (ang. escape sequence). Zaczyna się ona od znaku \
(ang. backslash, lewy ukośnik7), po którym występuje określenie znaku specjalnego,
jaki ma być wyświetlony na ekranie. Powstaje jednak kolejny problem: w jaki sposób
wyświetlić teraz sam znak ukośnika \? Odpowiedź jest dokładnie taka sama jak w po-
przednim przypadku: należy poprzedzić go dodatkowym znakiem \. Taka konstrukcja
będzie zatem wyglądać następująco:
System.out.println("Oto ukośnik: \\");

W ten sam sposób można wyprowadzić również inne znaki specjalne, takie jak znak
nowej linii czy znak apostrofu8. Stosowane w praktyce sekwencje znaków specjalnych są
przedstawione w tabeli 2.1.

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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 33

Instrukcja System.out.print
Do wyświetlania danych na ekranie, oprócz znanej nam już dobrze instrukcji System.out.
println, można wykorzystać również bardzo podobną instrukcję System.out.print.
Jej działanie jest analogiczne, z tą różnicą, że nie następuje przejście do nowego wiersza.
Zatem wykonanie instrukcji w postaci:
System.out.println("napis1");
System.out.println("napis2");

spowoduje wyświetlenie 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 zmiennoprze-
cinkowe, np. 14.5 i 24.45. Wyświetl wartości tych zmiennych na ekranie w dwóch
wierszach. Nie korzystaj z instrukcji System.out.println.

Ćwiczenie 5.2.
Napisz program, który wyświetli na ekranie zbudowany ze znaków alfanumerycznych na-
pis widoczny na rysunku 2.8. Pamiętaj o wykorzystaniu sekwencji znaków specjalnych.

Rysunek 2.8.
Efekt wykonania
ćwiczenia 5.2

Lekcja 6. Operacje na zmiennych


Na zmiennych typów prostych (czyli tych poznanych w lekcji 2., z wyjątkiem typów obiek-
towych) 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 po-
mocą operatora plus zapisywanego jako +, a odejmowania — za pomocą operatora minus
zapisywanego jako -. W tej lekcji zostanie pokazane, w jaki sposób są wykonywane

b4ac40784ca6e05cededff5eda5a930f
b
34 Java. Praktyczny kurs

operacje na zmiennych i jakie rządzą nimi prawa. Przedstawione zostaną operatory aryt-
metyczne, bitowe, logiczne, przypisania i porównywania, a także ich priorytety, czyli to,
które z nich są silniejsze, a które słabsze.

Operatory arytmetyczne
Operatory arytmetyczne służą, jak nietrudno się domyślić, do wykonywania operacji aryt-
metycznych, czyli znanego wszystkim dobrze mnożenia, dodawania itp. W tej grupie wy-
stępują jednak również mniej znane operatory, takie jak operator inkrementacji i de-
krementacji. Wszystkie one są zebrane w tabeli 2.2.

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 wykonywa-
nia typowych działań znanych z lekcji matematyki. Jeśli zatem trzeba dodać do siebie
dwie zmienne lub liczbę do zmiennej, używa się operatora +, gdy konieczne jest mnożenie
— operatora * itp. Oczywiście operacje arytmetyczne wykonuje się na zmiennych
arytmetycznych. Załóżmy zatem, że w kodzie są trzy zmienne typu całkowitoliczbowego
int i chcemy wykonać na nich kilka prostych operacji. Zobrazowano to na listingu 2.9;
dla zwiększenia czytelności opisu poszczególne linie zostały ponumerowane.
Listing 2.9.
class Main {
public static void main(String args[]) {
/*1*/ int a, b, c;
/*2*/ a = 10;
/*3*/ b = 20;
/*4*/ System.out.println("a = " + a + ",b = " + b);
/*5*/ c = b - a;
/*6*/ System.out.println("b - a = " + c);
/*7*/ c = a / 2;
/*8*/ System.out.println("a / 2 = " + c);
/*9*/ c = a * b;
/*10*/ System.out.println("a * b = " + c);
/*11*/ c = a + b;
/*12*/ System.out.println("a + b = " + c);
}
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 35

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 wspo-
mniane wcześniej, nie oznacza on obliczania procentów, ale dzielenie modulo, czyli
resztę z dzielenia. Przykładowo: działanie 10 % 3 da w wyniku 1. Trójka zmieści się
bowiem w dziesięciu trzy razy, pozostawiając resztę 1 (3×3 = 9, 9+1 = 10). Podobnie
21%8 = 5, bo 2×8 = 16, 16+5 = 21.

Inkrementacja i dekrementacja
Operatory inkrementacji, czyli zwiększania (++), oraz dekrementacji, czyli zmniejsza-
nia (--), są z pewnością nieobce osobom znającym języki takie jak C, C++ czy PHP,
nowością będą natomiast dla programujących w Pascalu. Operator ++ zwiększa po
prostu wartość zmiennej o 1, a -- zmniejsza ją o 1. To jednak nie wszystko. Mogą one
bowiem występować w formie przedrostkowej (preinkrementacyjnej) lub przyrostkowej
(postinkrementacyjnej). Przykładowo: dla zmiennej o nazwie x forma przedrostkowa
będzie miała postać ++x, natomiast przyrostkowa — x++.

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 po-
czą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++);

b4ac40784ca6e05cededff5eda5a930f
b
36 Java. Praktyczny kurs

/*3*/ System.out.println (++x);


/*4*/ System.out.println (x);
/*5*/ y = x++;
/*6*/ System.out.println (y);
/*7*/ y = ++x;
/*8*/ System.out.println (y);
/*9*/ System.out.println (++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);

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 37

Rysunek 2.11.
Ilustracja działania
operatora dekrementacji

/*5*/ y = x--;
/*6*/ System.out.println (y);
/*7*/ y = --x;
/*8*/ System.out.println (y);
/*9*/ System.out.println (--y);
}
}

W linii 1. powstały zmienne x i y. Pierwszej z nich przypisano wartość 3, dokładnie tak jak
w programie z listingu 2.10. W linii 2. zastosowano formę przyrostkową operatora --,
zatem najpierw wartość zmiennej x (x = 3) pojawi się na ekranie, a dopiero potem
będzie zmniejszona o 1 (x = 2). W linii 3. jest dokładnie odwrotnie, to znaczy przez zasto-
sowanie formy przedrostkowej najpierw wartość zmiennej x zmniejsza się o 1 (x = 1),
a dopiero potem pojawia się na ekranie. W linii 4. jedyną operacją jest ponowne wy-
świetlenie wartości x (x = 1).

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 war-
tość 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 wi-
doczny na listingu 2.12.

b4ac40784ca6e05cededff5eda5a930f
b
38 Java. Praktyczny kurs

Listing 2.12.
class Main {
public static void main(String args[]) {
int a, b;
a = 9;
b = a / 2;
System.out.println("a = " + a);
System.out.println("Wynik działania a / 2 = " + b);
b = 8 / 3;
System.out.println("b = 8 / 3");
System.out.println("Wartość b to " + b);
}
}

Zmienne a i b są typu int i mogą przechowywać liczby całkowite. Zmiennej a przypisa-


no 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ąpie-
nia trudnych do wykrycia błędów w aplikacjach.

Czemu jednak kompilator nie ostrzega o tym, że taka konwersja zostanie dokonana?
Przede wszystkim wynik większości działań jest znany dopiero w trakcie działania pro-
gramu, zazwyczaj nie ma więc możliwości sprawdzenia już na etapie kompilacji, czy
wynik jest właściwego typu. Nie można też dopuścić, by program zgłaszał błędy lub
ostrzeżenia za każdym razem, kiedy wynik działania nie będzie dokładnie takiego typu
jak zmienna, której jest przypisywany. Dlatego też istnieją zdefiniowane w języku pro-
gramowania ogólne zasady konwersji typów, które są stosowane automatycznie (tzw.
konwersje automatyczne), kiedy tylko jest to możliwe. Jedna z takich reguł mówi właśnie,
że jeśli wynikiem operacji jest wartość zmiennoprzecinkowa, a wynik ten ma być
przypisany zmiennej całkowitoliczbowej, to część ułamkowa zostaje odrzucona.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 39

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 li-
stingu 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 ra-
zem bowiem następuje próba bezpośredniego przypisania liczby ułamkowej zmiennej
typu int, która takich wartości przechowywać nie może.

Rysunek 2.13. Próba przypisania wartości ułamkowej zmiennej typu int kończy się błędem kompilacji

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ę spo-
dziewać, ż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);
}
}

b4ac40784ca6e05cededff5eda5a930f
b
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 auto-
matyczną konwersją liczby zmiennoprzecinkowej, tak aby mogła zostać przypisana
zmiennej typu int (listing 2.12). Jeśli przypisanie wartości zmiennej jest wynikiem ob-
liczeń wykonanych w trakcie działania programu, kompilator nie będzie w stanie ostrzec
programisty, że przekracza dopuszczalny zakres wartości. Taka sytuacja została zo-
brazowana 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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 41

wartości nie powoduje błędu, ale „zawinięcie” liczby. Zobrazowano to na rysunku 2.16.
Arytmetyka wygląda w tym przypadku następująco:
INT_MAX+1 = INT_MIN
INT_MAX+2 = INT_MIN+1
INT_MAX+3 = INT_MIN+2,

jak również:
INT_MIN–1 = INT_MAX
INT_MIN–2 = INT_MAX+1
INT_MIN–3 = INT_MAX+2,

gdzie INT_MIN to minimalna wartość, jaką może przyjąć zmienna typu int, czyli –231,
a INT_MAX to wartość maksymalna, czyli 231–1.

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 pod-
stawowe informacje na temat systemów liczbowych. W systemie dziesiętnym, z którego
korzystamy na co dzień, używanych jest dziesięć cyfr, od 0 do 9. W systemie dwójkowym
będą z kolei wykorzystywane jedynie dwie cyfry: 0 i 1 (w systemie szesnastkowym cyfry
od 0 do 9 i dodatkowo litery od A do F, a w systemie ósemkowym cyfry od 0 do 7). Ko-
lejne liczby są budowane z tych dwóch cyfr dokładnie tak samo jak w systemie dziesięt-
nym; przedstawiono to w tabeli 2.3. Widać wyraźnie, że np. 4 dziesiętnie to 100 dwójkowo,
a 10 dziesiętnie to 1010 dwójkowo.

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.

b4ac40784ca6e05cededff5eda5a930f
b
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 za-
tem mamy przykładowe wyrażenie 0 < 1, to ma ono wartość logiczną true (prawda),
jako że niewątpliwie zero jest mniejsze od jedności, z kolei wyrażenie 2 > 5 ma wartość
logiczną false (fałsz), bo dwa nie jest większe od pięciu. Operacje logiczne to znane
ze szkoły logiczne AND (iloczyn), OR (suma) oraz NOT (negacja). Przedstawiono je w ta-
beli 2.5.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 43

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

b4ac40784ca6e05cededff5eda5a930f
b
44 Java. Praktyczny kurs

Operatory przypisania
Operacje przypisania są dwuargumentowe i powodują przypisanie argumentu prawo-
stronnego argumentowi lewostronnemu. Najprostsza taka operacja była już stosowana
wielokrotnie, odbywa się ona przy użyciu operatora = (równa się). Napisanie a = 5
oznacza, że zmiennej a ma być przypisana wartość 5.

Oprócz prostego operatora = w Javie występuje szereg operatorów złożonych, tzn. takich,
w których przypisaniu towarzyszy dodatkowa operacja arytmetyczna lub bitowa. Przykła-
dowo istnieje operator +=, który oznacza: przypisz argumentowi z lewej strony war-
tość wynikającą z dodawania argumentu znajdującego się z lewej strony i argumentu
znajdującego się z prawej strony operatora.

Choć brzmi to z początku nieco zawile, w rzeczywistości jest bardzo proste i znacznie
upraszcza niektóre konstrukcje programistyczne. Przykładowy zapis:
a += b

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). Schematycz-
nie 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).

b4ac40784ca6e05cededff5eda5a930f
b
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 przed-
stawione w tabeli 2.119. Im wyższa pozycja w tabeli, tym wyższy priorytet operatora.
Operatory znajdujące się na jednym poziomie (w jednym wierszu) mają ten sam priorytet.

9
Tabela nie przedstawia wszystkich operatorów występujących w Javie, ale jedynie omówione w bieżącej
lekcji.

b4ac40784ca6e05cededff5eda5a930f
b
46 Java. Praktyczny kurs

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. Wy-
konaj działanie a % b % c. Wynik tego działania przypisz czwartej zmiennej o nazwie
wynik, jej wartość wyświetl na ekranie. Spróbuj tak dobrać wartości zmiennych, aby
wynikiem nie było 0.

Ćwiczenie 6.2.
Zadeklaruj zmienną typu int o dowolnej nazwie. Przypisz jej wartość 256. Wykonaj trzy
działania: przesunięcie bitowe w prawo o dwa miejsca, przesunięcie bitowe w lewo o dwa
miejsca oraz przesunięcie bitowe w prawo o dwa miejsca z wypełnieniem zerami. Wynik
wszystkich trzech działań wyświetl na ekranie. Zaobserwuj zachodzące prawidłowości.

Ćwiczenie 6.3.
Zadeklaruj dwie zmienne typu int. Przypisz im dowolne wartości oraz wykonaj na nich
działania sumy bitowej oraz iloczynu bitowego. Wyświetl wyniki działań na ekranie.

Ćwiczenie 6.4.
Zadeklaruj zmienną typu int o dowolnej nazwie i przypisz jej dowolną wartość cał-
kowitą. Dwukrotnie wykonaj na niej operację XOR, wykorzystując jako drugi argument
również dowolną liczbę całkowitą. Użyj operatora przypisania ^=. Zaobserwuj otrzymany
wynik i zastanów się, czemu ma on właśnie taką postać.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 47

Ćwiczenie 6.5.
Umieść w programie zmienną a o wartości 55 i zmienną b o wartości 88 oraz instrukcję
wykonującą następujące działanie:
a += (b - 3 ^ 4 << 2 | 8 - b >>> 8 * 2) - (a & a - 1);

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 spraw-


dzenia, 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.

b4ac40784ca6e05cededff5eda5a930f
b
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. Na-
stępnie za pomocą instrukcji if sprawdzamy, czy jest ona większa od 0. Wykorzystu-
jemy w tym celu operator porównywania < (lekcja 6.). Ponieważ zmienna liczba
otrzymała stałą wartość równą 15, która na pewno jest większa od 0, zostanie oczywiście
wyświetlony napis: Zmienna liczba jest większa od zera. Jeśli przypiszemy jej
wartość ujemną lub równą 0, to zostanie wykonany blok else.

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  
x1  i x2  . Taka liczba warunków sprawia, że to zadanie świetnie
2 A 2 A
nadaje się do przećwiczenia działania instrukcji if…else. Jedyną niedogodnością pro-
gramu 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 wczyty-
wanie 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

b4ac40784ca6e05cededff5eda5a930f
b
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, odzwier-


ciedlają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ów-
nanie nie jest kwadratowe. Jeśli jednak A jest różne od 0, możemy przystąpić do obli-
czenia delty. Wynik obliczeń przypisujemy zmiennej o nazwie delta. Zmienna ta jest
typu double, to znaczy może przechowywać liczby zmiennoprzecinkowe. To ko-
nieczne, 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 sto-
sowny komunikat na ekranie. Gdy jednak delta nie jest mniejsza od 0, przystępujemy
do sprawdzenia kolejnych warunków. Jeżeli delta jest równa 0, można od razu obli-

b4ac40784ca6e05cededff5eda5a930f
b
50 Java. Praktyczny kurs

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. Nie-
znana nam do tej pory instrukcja Math.sqrt(delta) powoduje obliczenie pierwiastka
kwadratowego (drugiego stopnia) z wartości zawartej w zmiennej delta.

Rysunek 2.17.
Program obliczający
rozwiązania równania
kwadratowego

Zagnieżdżanie instrukcji if sprawdza się dobrze w tak prostym przykładzie jak po-
wyższy, jednak z każdym kolejnym poziomem staje się coraz bardziej nieczytelne.
Nadmiernemu zagnieżdżaniu można zapobiec przez zastosowanie nieco zmodyfiko-
wanej instrukcji w postaci if…else if. Załóżmy, że mamy znaną już z listingu 2.18
konstrukcję w postaci:
if(warunek1){
instrukcje1
}
else{
if(warunek2){
instrukcje2
}
else{
if(warunek3){
instrukcje3
}
else{
instrukcje4
}
}
}

Innymi słowy, trzeba sprawdzić po kolei warunki warunek1, warunek2 i warunek3 i w za-
leżności od tego, które są prawdziwe, wykonać instrukcje instrukcje1, instrukcje2,
instrukcje3 lub instrukcje4. Zatem instrukcje1 są wykonywane, kiedy warunek1

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 51

jest prawdziwy; instrukcje2 — kiedy warunek1 jest fałszywy, a warunek2 prawdzi-


wy; 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 prze-
ciwnym razie, jeśli prawdziwy jest warunek3, wykonaj instrukcje3; w przeciwnym
razie wykonaj instrukcje4.

Taka konstrukcja pozwala uprościć nieco przykładowy kod z listingu 2.18 obliczający
pierwiastki równania kwadratowego. Zamiast sprawdzać, czy delta jest mniejsza, więk-
sza, czy równa 0, za pomocą zagnieżdżonej instrukcji if, łatwiej skorzystać z instrukcji
if…else if. Zostało to zobrazowane na listingu 2.19.

Listing 2.19.
class Main {
public static void main (String args[]) {
//deklaracja zmiennych
int A = 1, B = 1, C = -2;

//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) {

b4ac40784ca6e05cededff5eda5a930f
b
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 frag-
mentu w postaci:
if(delta < 0){
//instrukcje
}
else if(delta == 0){
//instrukcje

b4ac40784ca6e05cededff5eda5a930f
b
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 posta-
ciach. 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 ope-
rator 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 wy-
mienionych językach. Z kolei osoby dopiero rozpoczynające naukę czy też znające np.
Pascala powinny wnikliwie zapoznać się z przedstawionym materiałem.

b4ac40784ca6e05cededff5eda5a930f
b
54 Java. Praktyczny kurs

Instrukcja switch
Instrukcja switch pozwala w wygodny i przejrzysty sposób sprawdzić ciąg warunków
i wykonywać różny kod w zależności od tego, czy są one prawdziwe, czy fałszywe.
W najprostszej postaci może być ona odpowiednikiem ciągu if…else if, w którym jako
warunek jest wykorzystywane porównywanie zmiennej z wybraną liczbą. Daje ona
programiście dodatkowe możliwości, jak choćby wykonania tego samego kodu dla
kilku warunków. Taki oto przykładowy zapis:
if(liczba == 1){
instrukcje1
}
else if (liczba == 2){
instrukcje2
}
else if(liczba == 3){
instrukcje3
}
else{
instrukcje4
}

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ść aryt-
metyczna (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;

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 55

Należy ją rozumieć jako: sprawdź wartość wyrażenia wyrażenie; jeśli jest to wartość1,
wykonaj instrukcje1 i przerwij wykonywanie bloku switch (instrukcja break); jeżeli
jest to wartość2, wykonaj instrukcje2 i przerwij wykonywanie bloku switch; jeśli
jest to wartość3, wykonaj instrukcje3 i przerwij wykonywanie bloku switch; jeżeli
w żadnym z powyższych przypadków dopasowanie nie nastąpiło, wykonaj instrukcje4
i zakończ blok switch. Oczywiście w praktyce liczba bloków case nie jest ograniczona
i może ich być dowolnie wiele.

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 dynamicz-
nie w samej instrukcji. Jest to widoczne w przykładzie na listingu 2.21.

Listing 2.21.
class Main {
public static void main (String args[]) {
int liczba1 = 2;
int liczba2 = 1;
switch(liczba1 * 5 / (liczba2 + 1)){
case 5 :
System.out.println("Wartość wyrażenia to 5.");
break;
case 15 :
System.out.println("Wartość wyrażenia to 15.");
break;
default :
System.out.println("Wartość wyrażenia nie jest równa ani 5, ani 15.");
}

b4ac40784ca6e05cededff5eda5a930f
b
56 Java. Praktyczny kurs

}
}

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 zgod-
ność 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 obli-
gatoryjny 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 wykonywa-
nie danego bloku case i tym samym powoduje opuszczenie instrukcji switch. Jej przypad-
kowe pominięcie może doprowadzić do nieoczekiwanych wyników i błędów w progra-
mie. Aby przekonać się, w jaki sposób działa instrukcja switch bez instrukcji break,
wystarczy zmodyfikować listing 2.20, usuwając z niego wszystkie słowa break. Powstanie
wtedy kod widoczny na listingu 2.22.

Listing 2.22.
class Main {
public static void main (String args[]) {
int liczba = 25;
switch(liczba){
case 25:
System.out.println("liczba = 25");
case 15:
System.out.println("liczba = 15");
default:
System.out.println("Zmienna liczba nie jest równa ani 15, ani 25.");
}
}
}

Wynik jego działania może zadziwić — jest widoczny na rysunku 2.18. Ten program
raczej nie jest w stanie podać, jaką tak naprawdę wartość ma zmienna liczba, praw-
da? Jak zatem działa przedstawiony kod? Otóż jeśli w którymś z bloków (przypadków)
case zostanie wykryta zgodność z wyrażeniem występującym za switch, zostaną wy-
konane wszystkie dalsze instrukcje, aż do napotkania instrukcji break (a dokładniej:
dowolnej instrukcji powodującej przerwanie działania bloku switch) lub dotarcia do
końca instrukcji switch.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 57

Rysunek 2.18.
Wynik braku instrukcji
break w bloku switch

W kodzie z listingu 2.22 zgodność jest stwierdzona już w pierwszym przypadku (case
25), zostaje zatem wykonana instrukcja System.out.println("liczba = 25"). Ponieważ
jednak nie występuje po niej break, w dalszej kolejności zostaje wykonana instrukcja
System.out.println("liczba = 15") (nie ma znaczenia, że należy ona do przypadku
case 15!). Po tej instrukcji również nie ma break, zatem zostanie wykonana instrukcja
występująca po default, czyli System.out.println("Zmienna liczba nie jest równa
ani 15, ani 25.").

Ktoś może spytać: czemu w takim razie break nie jest obligatoryjny, tak jak np. w języ-
ku C#11? Chodzi o to, że takie właśnie zachowanie bloku switch dla wprawnego pro-
gramisty może być ułatwieniem przy pisaniu kodu (bo pozwala na wykonanie wielu
bloków case dla jednej wartości wyrażenia). Właściwość tę można sprawnie wykorzy-
stać przy tworzeniu aplikacji. Początkujący powinni jednak zapamiętać, że po każdym
przypadku case należy umieścić instrukcję break, co pozwoli uniknąć niespodziewanych
problemów.

Operator warunkowy
Operator warunkowy ma postać:
warunek ? wartość1 : wartość2

Oznacza ona: jeżeli warunek jest prawdziwy, podstaw za wartość wyrażenia wartość1,
w przeciwnym wypadku podstaw wartość2. Sprawdźmy w praktyce, jak może wyglądać
jego wykorzystanie — zobrazowano to w kodzie widocznym na listingu 2.23.

Listing 2.23.
class Main {
public static void main (String args[]){
int liczba1 = 10;
int liczba2;
liczba2 = liczba1 < 0 ? -1 : 1;
System.out.println(liczba2);
}
}

11
W języku C# każdy przypadek case musi kończyć się instrukcją przekazującą sterowanie poza blok
switch. Najczęściej jest to instrukcja break, ale można ją również zastąpić np. przez instrukcję goto.

b4ac40784ca6e05cededff5eda5a930f
b
58 Java. Praktyczny kurs

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 wy-
rażeniu wartość –1, w przeciwnym razie (zmienna liczba1 większa lub równa 0) przy-
pisz 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ść po-
czątkową. Napisz instrukcję, która w przypadku gdy wartość zmiennej jest mniejsza od
zera, zamieni tę wartość na dodatnią (zachowa się jak wartość bezwzględna w mate-
matyce). Użyj operatora warunkowego.

Ćwiczenie 8.5.
Napisz instrukcję switch badającą wartość pewnej zmiennej typu całkowitoliczbowego
i wyświetlającą komunikaty w następujących sytuacjach: a) zmienna ma wartość 1, 4,
8; b) zmienna ma wartość 2, 3, 7; c) wszystkie inne przypadki. Postaraj się całość zapisać
możliwie krótko.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 59

Lekcja 9. Pętle
Pętle są konstrukcjami programistycznymi, które pozwalają na wykonywanie powta-
rzających się czynności. Przykładowo aby wyświetlić na ekranie dziesięć razy dowol-
ny napis, najłatwiej skorzystać właśnie z odpowiedniej pętli. Oczywiście można też
dziesięć razy napisać w kodzie programu System.out.println("napis"), jednak będzie
to z pewnością niezbyt wygodne. W tej lekcji zostaną przedstawione wszystkie wy-
stępujące w Javie postacie pętli, czyli pętle for, while oraz do…while, a także wpro-
wadzona w Java 5.0 (1.5) pętla typu foreach. Omówione zostaną także różnice wy-
stępujące między nimi oraz przykłady wykorzystania różnych typów pętli.

Pętla for
Pętla for ma ogólną postać:
for (wyrażenie początkowe; wyrażenie warunkowe; wyrażenie modyfikujące){
instrukcje do wykonania
}

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 war-
tość 0 (int i = 0), następnie tak długo, jak długo wartość i jest mniejsza od 10 (i < 10),
wykonuj instrukcję System.out.println("Pętle w Javie") oraz zwiększaj wartość i o 1
(i++). Dzięki temu na ekranie pojawi się dziesięć razy napis Pętle w Javie (rysunek 2.19).
Zmienna i jest nazywana zmienną iteracyjną, czyli kontrolującą kolejne przebiegi (ite-
racje) pętli.

Jeśli chcemy zobaczyć, jak zmienia się stan zmiennej i w trakcie kolejnych przebiegów,
możemy zmodyfikować instrukcję wyświetlającą napis, tak aby podawała i tę informację.
Wystarczy drobna poprawka w postaci:
for(int i = 0; i < 10; i++){
System.out.println("[i = " + i + "] Pętle w Javie");
}

b4ac40784ca6e05cededff5eda5a930f
b
60 Java. Praktyczny kurs

Rysunek 2.19.
Wynik działania prostej
pętli typu for

Po jej wprowadzeniu w każdej linii będzie wyświetlana również wartość i, tak jak zostało
to przedstawione na rysunku 2.20.

Rysunek 2.20.
Niewielka modyfikacja
kodu pozwala podejrzeć
stan zmiennej iteracyjnej

Wyrażenie początkowe to w powyższym przykładzie int i = 0, wyrażenie warun-


kowe 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");

b4ac40784ca6e05cededff5eda5a930f
b
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 warun-
kowym. 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óbuj-
my dokonać takiego zabiegu również z wyrażeniem początkowym. Jest to prosty zabieg
techniczny. Schematycznie taka konstrukcja wygląda następująco:
wyrażenie początkowe;
for (; wyrażenie warunkowe;){
instrukcje do wykonania
wyrażenie modyfikujące;
}

Jak może to wyglądać w praktyce, zobrazowano na listingu 2.26. Całe wyrażenie począt-
kowe zostało przeniesione po prostu przed pętlę. To jest nadal w pełni funkcjonalny
odpowiednik programu z listingu 2.24. Ponownie należy zwrócić uwagę na umiejsco-
wienie średników pętli for. Oba są niezbędne do prawidłowego działania kodu.

Listing 2.26.
class Main {
public static void main (String args[]) {
int i = 0;
for(; i < 10;){
System.out.println("Pętle w Javie");
i++;
}
}
}

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 modyfi-
kują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");
}
}
}

b4ac40784ca6e05cededff5eda5a930f
b
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 odpowied-
nikiem 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 (wska-
zó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 odpowied-
nikami. 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. Zo-


baczmy 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 ekra-
nie (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");

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 63

}
}
}

Należy jednak przy tym zwrócić uwagę, że choć programy z listingów 2.28 i 2.29
wykonują to samo zadanie, nie są to w pełni funkcjonalne odpowiedniki. Można to
zauważyć po dodaniu instrukcji wyświetlającej stan zmiennej i w obu wersjach kodu.
Wystarczy zmodyfikować instrukcję System.out.println("Pętle w Javie") tak samo
jak w przypadku pętli for: System.out.println("[i = " + i + "] Pętle w Javie").
Wynik działania kodu, kiedy zmienna i jest modyfikowana wewnątrz pętli (tak jak na li-
stingu 2.28), będzie taki sam jak na rysunku 2.20, natomiast wynik jego działania, kiedy
zmienna ta jest modyfikowana w wyrażeniu warunkowym (tak jak na listingu 2.29),
przedstawiony jest na rysunku 2.21.

Rysunek 2.21.
Stan zmiennej i, kiedy
jest modyfikowana
w wyrażeniu
warunkowym

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 spowo-
dowałaby poważne konsekwencje w działaniu programu. Dobrym ćwiczeniem do sa-
modzielnego 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 samo-
dzielnego 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);

b4ac40784ca6e05cededff5eda5a930f
b
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świe-
tlenia 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 waru-
nek, a dopiero potem są wykonywane instrukcje. W przypadku pętli do…while jest dokład-
nie odwrotnie — najpierw są wykonywane instrukcje, a dopiero potem jest sprawdza-
ny warunek. Dlatego też tym razem trzeba sprawdzić, czy i jest mniejsze od 9. Gdyby
pozostawić starą postać wyrażenia warunkowego: i++ < 10, napis zostałby wyświetlony
jedenaście razy.

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 naj-
mniej 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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 65

}
while(i < 0);

Warunek w tej postaci jest ewidentnie fałszywy, jako że zmienna i już w trakcie inicjacji
jest równa 0 (nie może być więc jednocześnie mniejsza od 0!). Mimo to po wykona-
niu powyższego kodu na ekranie pojawi się jeden napis Pętle w Javie. Jest to najlepszy
dowód na to, że warunek jest sprawdzany nie przed przebiegiem pętli, ale dopiero po
każdym przebiegu.

Pętla typu foreach


Począwszy od Javy w wersji 5.0 (1.5), dostępny jest nowy rodzaj pętli. To pętla na-
zywana 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 war-
tość kolejnej komórki. Pętla będzie działała tak długo, aż zostaną przejrzane wszyst-
kie elementy tablicy (lub kolekcji) typu typ bądź też zostanie przerwana za pomocą
jednej z instrukcji pozwalających na taką operację (lekcja 10.). Przykład użycia takiej
pętli został przedstawiony na listingu 2.31.

Listing 2.31.
class Main {
public static void main (String args[]) {
int tab[] = {1, 2, 3, 4, 5, 4, 3, 2, 1};
for(int val: tab){
System.out.println(val);
}
}
}

Ć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.

b4ac40784ca6e05cededff5eda5a930f
b
66 Java. Praktyczny kurs

Ćwiczenie 9.2.
Nie zmieniając żadnej instrukcji wewnątrz pętli, zmodyfikuj kod z listingu 2.28 w taki
sposób, aby był funkcjonalnym odpowiednikiem kodu z listingu 2.29.

Ćwiczenie 9.3.
Wykorzystując pętlę while, napisz program, który wyświetli liczby całkowite od 1 do 20
niepodzielne przez 3.

Ćwiczenie 9.4.
Zmodyfikuj kod z listingu 2.30 w taki sposób, aby w wyrażeniu warunkowym pętli
do…while zamiast operatora < wykorzystać <=.

Ćwiczenie 9.5.
Napisz program, który wyświetli na ekranie liczby od 1 do 20 i zaznaczy przy każdej
z nich, czy jest to liczba parzysta, czy nieparzysta. Zrób to:
a) wykorzystując pętlę for,
b) wykorzystując pętlę while,
c) wykorzystując pętlę do…while.

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ółpra-
cują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 instruk-
cje w przypadku prostych pętli pojedynczych, a także w przypadku pętli dwu- lub wie-
lokrotnie zagnieżdżonych. Omówiony zostanie również temat etykiet, które dodatko-
wo 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. Znacze-
nie break w języku programowania jest zgodne z nazwą, w języku angielskim break
znaczy „przerywać”. Dokładnie tak zachowywała się ta konstrukcja w przypadku in-
strukcji switch, tak też zachowuje się w przypadku pętli — po prostu przerywa ich
wykonanie. Dzięki temu można np. tak zmodyfikować pętlę for, aby wyrażenie wa-
runkowe znalazło się wewnątrz niej. Kod realizujący takie zadanie został przedstawiony
na listingu 2.32.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 67

Listing 2.32.
class Main {
public static void main (String args[]) {
for(int i = 0; ; i++){
System.out.println("Pętle w Javie " + i);
if(i == 9){
break;
}
}
}
}

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łe-
go pętli. Wyrażenie początkowe przenosi się przed pętlę, a modyfikujące i warunko-
we do jej wnętrza. Zostało to przedstawione na listingu 2.33. Tego typu konstrukcja
niegdyś była tylko ciekawostką programistyczną, raczej niestosowaną w praktycz-
nych rozwiązaniach, obecnie jednak coraz częściej widuje się ją w różnych projektach
programistycznych, gdzie pełni rolę zamiennika dla pętli while.

Listing 2.33.
class Main {
public static void main (String args[]) {
int i = 0;
for(; ;){
System.out.println("Pętle w Javie " + i);
if(i++ >= 9){
break;
}
}
}
}

Należy pamiętać, że instrukcja break przerywa działanie pętli, w której się znajduje.
Jeśli zatem mamy zagnieżdżone pętle for, a instrukcja break występuje w pętli wewnętrz-
nej, zostanie przerwana jedynie pętla wewnętrzna. Pętla zewnętrzna nadal będzie
działać. Spójrzmy na kod znajdujący się na listingu 2.34: to właśnie dwie zagnieżdżone
pętle for.

b4ac40784ca6e05cededff5eda5a930f
b
68 Java. Praktyczny kurs

Listing 2.34.
class Main {
public static void main (String args[]) {
for(int i = 0; i < 3; i++){
for(int j = 0; j < 3; j++){
System.out.println(i + " " + j);
}
}
}
}

Wynikiem działania takiego programu będzie ciąg liczb widoczny na rysunku 2.22.
Pierwszy pionowy ciąg liczb określa stan zmiennej i, natomiast drugi — stan zmien-
nej j. Ta konstrukcja działa w ten sposób, że w każdym przebiegu pętli zewnętrznej
są wykonywane trzy przebiegi pętli wewnętrznej (a zatem instrukcja wyświetlająca
stan zmiennych wykonywana jest w sumie dziewięć razy). Stąd też ciągi liczb, które
pojawiają się na ekranie.

Rysunek 2.22.
Wynik działania dwóch
zagnieżdżonych pętli
typu for

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 ze-
wnętrznej (zostało to zobrazowane na rysunku 2.23). Tym samym po uruchomieniu
programu znikną ciągi liczb wyświetlane, kiedy i było równe 2 (rysunek 2.24). Instrukcja
break powoduje bowiem przejście do kolejnej iteracji zewnętrznej pętli.

Zastosowanie instrukcji break nie ogranicza się oczywiście jedynie do pętli typu for.
Może być ona również stosowana w połączeniu z pozostałymi rodzajami pętli, po-
dobnie jak instrukcja continue, która zostanie przedstawiona już za chwilę.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 69

Rysunek 2.23.
Instrukcja break
powoduje przerwanie
wykonywania pętli
wewnętrznej

Rysunek 2.24.
Zastosowanie
instrukcji warunkowej
w połączeniu z break

Instrukcja continue
O ile instrukcja break powodowała przerwanie wykonywania pętli oraz jej opuszczenie,
o tyle instrukcja continue powoduje przejście do kolejnej iteracji. A zatem jeśli we-
wnątrz pętli znajdzie się instrukcja continue, bieżąca iteracja (przebieg) zostanie
przerwana oraz rozpocznie się kolejna (chyba że bieżąca iteracja była ostatnią). Zo-
baczmy, jak to działa, na konkretnym przykładzie. Na listingu 2.35 jest widoczna pę-
tla for, która wyświetla liczby całkowite z zakresu 1 – 20 podzielne przez 2 (por.
ćwiczenia do samodzielnego wykonania z lekcji 9.).
Listing 2.35.
class Main {
public static void main (String args[]) {
for(int i = 1; i <= 20; i++){
if(i % 2 != 0) continue;
System.out.println(i);
}
}
}

Wynik działania tego programu jest widoczny na rysunku 2.25. Sama pętla jest skon-
struowana w dobrze nam znany sposób. W środku znajduje się instrukcja warunkowa
sprawdzająca warunek i % 2 != 0, czyli badająca, czy reszta z dzielenia i przez 2 jest
różna od 0. Jeśli warunek jest prawdziwy (reszta jest różna od 0), oznacza to, że wartość
zawarta w i nie jest podzielna przez 2, wykonywana jest zatem instrukcja continue.
Jak wiadomo, zgodnie z tym, co zostało napisane powyżej, powoduje ona rozpoczęcie
kolejnej iteracji pętli, czyli zwiększenie wartości zmiennej i o 1 i przejście na początek
pętli (do pierwszej instrukcji). Tym samym jeśli wartość i jest niepodzielna przez 2,
nie zostanie wykonana znajdująca się za warunkiem instrukcja System.out.println(i),
zatem dana wartość nie pojawi się na ekranie, a to właśnie było naszym celem.

b4ac40784ca6e05cededff5eda5a930f
b
70 Java. Praktyczny kurs

Rysunek 2.25.
Wynik działania
programu
wyświetlającego liczby
z zakresu 1 – 20
podzielne przez 2

Rzecz jasna to zadanie można wykonać bez użycia instrukcji continue (podpunkt „Ćwi-
czenia do samodzielnego wykonania”), ale bardzo dobrze ilustruje ono istotę jej działania.
Schemat działania instrukcji continue dla pętli for pokazano na rysunku 2.26.

Rysunek 2.26.
Schemat działania
instrukcji continue
dla pętli typu for

Instrukcja continue w przypadku pętli zagnieżdżonych działa tak jak instrukcja break,
to znaczy jej działanie dotyczy tylko pętli, w której się znajduje. A zatem jeśli jest umiesz-
czona w pętli wewnętrznej, powoduje przejście do kolejnej iteracji pętli wewnętrznej,
a jeśli znajduje się w pętli zewnętrznej — do kolejnej iteracji pętli zewnętrznej.
Schemat tego działania został przedstawiony na rysunku 2.27.

Rysunek 2.27.
Sposób działania
instrukcji continue
w przypadku
zagnieżdżonych
pętli for

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 71

Etykiety
Instrukcje break i continue oprócz tego, że można je wykorzystać w postaci omówionej
powyżej, występują również w połączeniu z etykietami. Postacie te nazywa się czasami
etykietowanym break (ang. labeled break) oraz etykietowanym continue (ang. labeled
continue). Etykieta to wyznaczone miejsce w kodzie programu. Ustala się ją poprzez
podanie jej nazwy zakończonej znakiem dwukropka, np.:
etykieta1:

Teoretycznie taką etykietę można umieścić w dowolnym miejscu między instrukcja-


mi, choć aby mogła zostać użyta, wolno wstawić ją jedynie przed instrukcją rozpo-
czynają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 instruk-
cją iteracji (pętlą). Widać to na listingu 2.36 — jest to modyfikacja przykładu z za-
gnieżdżonymi pętlami for, który był omawiany podczas objaśniania działania zwykłej
instrukcji break.

Listing 2.36.
class Main {
public static void main (String args[]) {
etykieta1:
for(int i = 0; i < 3; i++){
for(int j = 0; j < 3; j++){
if(i == 1) break etykieta1;
System.out.println(i + " " + j);
}
}
}
}

Do kodu została dodana etykieta o nazwie etykieta1 umieszczona prawidłowo tuż przed
pętlami for. W wewnętrznej pętli for znajduje się instrukcja break etykieta1, która
oznacza: przerwij działanie wszystkich pętli aż do etykiety etykieta1. Innymi słowy,
przerwij działanie pętli i idź do etykiety etykieta1 (dokładniej: do bloku oznaczonego
tą etykietą). Instrukcja ta zostanie wykonana, kiedy zmienna i osiągnie wartość 1, zatem
po uruchomieniu programu na ekranie pojawią się tym razem jedynie trzy rzędy liczb
(rysunek 2.28).

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:

b4ac40784ca6e05cededff5eda5a930f
b
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ętrz-
nej (gdzie zmienną iteracyjną jest i). Ogólny schemat działania etykietowanej instruk-
cji break jest widoczny na rysunku 2.29. Widać wyraźnie, że po napotkaniu break na-
stę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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 73

Dlatego też jeśli zmodyfikujemy program z listingu 2.36 tak, aby zamiast instrukcji
break etykieta1 występowała instrukcja continue etykieta1, jak jest to widoczne na
listingu 2.37, wynik jego działania będzie taki jak na rysunku 2.31. Jest to najlepszy
dowód, że inaczej niż etykietowane break, etykietowane continue powoduje ponowne
wejście do pętli. W przeciwnym wypadku w pierwszej kolumnie nie pojawiłyby się
cyfry 2 (por. rysunek 2.28).

Listing 2.37.
class Main {
public static void main (String args[]) {
etykieta1:
for(int i = 0; i < 3; i++){
for(int j = 0; j < 3; j++){
if(i == 1) continue etykieta1;
System.out.println(i + " " + j);
}
}
}
}

Rysunek 2.31.
Wynik działania
programu z listingu 2.37

Ćwiczenia do samodzielnego wykonania

Ćwiczenie 10.1.
Napisz program, który wyświetli na ekranie nieparzyste liczby z zakresu 1 – 20. Wyko-
rzystaj pętlę for i instrukcję continue.

Ćwiczenie 10.2.
Napisz program, który wyświetli na ekranie nieparzyste liczby z zakresu 1 – 20. Wyko-
rzystaj pętlę while i instrukcję continue.

Ćwiczenie 10.3.
Napisz program, który wyświetli na ekranie liczby z zakresu 1 – 30 niepodzielne
przez 3. Wykonaj trzy warianty ćwiczenia:

b4ac40784ca6e05cededff5eda5a930f
b
74 Java. Praktyczny kurs

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 po-
został bez zmian, ale nie było potrzeby używania instrukcji continue.

Tablice
Tablica to stosunkowo prosta struktura danych pozwalająca na przechowanie uporządko-
wanego zbioru elementów danego typu. Obrazowo zostało to przedstawione na rysun-
ku 2.32. Jak widać, struktura ta składa się z ponumerowanych kolejno komórek (nu-
meracja zaczyna się od 0). Każda taka komórka może przechowywać pewną porcję
danych. To, jakiego rodzaju będą to dane, jest określone przez typ tablicy. Jeśli zostanie
zadeklarowana tablica typu całkowitoliczbowego (int), będzie ona mogła zawierać liczby
całkowite. Jeżeli będzie to natomiast typ znakowy (char), poszczególne komórki będą
mogły zawierać różne znaki.

Rysunek 2.32.
Schematyczna struktura
tablicy

Lekcja 11. Podstawowe operacje na tablicach


Tablice to struktury danych występujące w większości popularnych języków progra-
mowania. Nie mogło ich zatem zabraknąć również w Javie. W tej lekcji zostaną przed-
stawione 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.

b4ac40784ca6e05cededff5eda5a930f
b
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ć sa-
mą 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 de-
klaracja to jednak nie wszystko, powstała dopiero zmienna o nazwie nazwa_tablicy,
dzięki której będzie można odwoływać się do tablicy, ale samej tablicy jeszcze wcale nie
ma! Trzeba ją dopiero utworzyć, korzystając z operatora new w postaci:
new typ_tablicy[liczba_elementów];

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 in-
strukcje. W przypadku prostych programów najczęściej wykorzystuje się jednak sposób
pierwszy, to znaczy jednoczesną deklarację zmiennej tablicowej i utworzenie tablicy.

Od razu warto zobaczyć, jak to wygląda w praktyce. Zadeklarujemy tablicę liczb cał-
kowitych (typu int) o nazwie tab i wielkości jednego elementu. Temu elementowi
przypiszemy dowolną wartość, a następnie wyświetlimy ją na ekranie. Kod realizujący
to zadanie jest widoczny na listingu 2.38.

Listing 2.38.
class Main {
public static void main (String args[]) {
int tab[] = new int[1];
tab[0] = 10;
System.out.println("Pierwszy element tablicy ma wartość: " + tab[0]);
}
}

b4ac40784ca6e05cededff5eda5a930f
b
76 Java. Praktyczny kurs

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łko-
witą. 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 (po-
dobnie 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 kwa-
dratowym 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 nieist-
niejącego elementu tablicy. Przykładowo: jeśli zadeklarujemy tablicę 10-elementową,
ale zapomnimy, że tablice są indeksowane od 0, i spróbujemy odwołać się do elementu
o indeksie 10 (element o takim indeksie oczywiście nie istnieje). Taki scenariusz zo-
stał zrealizowany we fragmencie kodu przedstawionym na listingu 2.39.

Listing 2.39.
class Main {
public static void main (String args[]) {
int tab[] = new int[10];
tab[10] = 5;
System.out.println("Dziesiąty element tablicy ma wartość: " + tab[10]);
}
}

Taki program da się bez problemu skompilować, jednak próba jego uruchomienia przy-
niesie rezultat widoczny na rysunku 2.33. Choć wygląda to bardzo groźnie, tak na-
prawdę nie stało się nic strasznego. Wykonywanie programu zostało rzecz jasna
przerwane, jednak najważniejsze jest to, że próba nieprawidłowego odwołania się do
tablicy została wykryta przez środowisko uruchomieniowe (maszynę wirtualną Javy)
i samo odwołanie, które mogłoby naruszyć stabilność systemu, nie nastąpiło. Zamiast
tego został wygenerowany tak zwany wyjątek (wyjątkami zajmiemy się w rozdziale 4.)
o nazwie ArrayIndexOutOfBoundsException (indeks tablicy poza zakresem) i program
zakończył działanie.

Rysunek 2.33. Próba odwołania się do nieistniejącego elementu tablicy

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 77

To bardzo ważna cecha Javy. Analogicznie działający program w C czy C++ (tzn.
odwołujący się do nieistniejących elementów tablic) potrafi poczynić spore spustoszenie
w systemie. Jego działanie jest też zupełnie nieprzewidywalne. Mimo wystąpienia
błędu może on dalej działać poprawnie (to bardzo optymistyczne założenie), może dzia-
łać, ale dawać nieprawidłowe wyniki albo może nastąpić zamknięcie programu przez
system operacyjny, a w ekstremalnym przypadku także naruszenie stabilności samego
systemu operacyjnego14. W Javie sprawa jest prosta — jeśli nastąpi przekroczenie zakresu
tablicy, system wygeneruje wyjątek.

Ważną sprawą jest inicjalizacja tablicy, czyli przypisanie jej komórkom wartości po-
czątkowych. W przypadku niewielkich tablic takiego przypisania można dokonać,
ujmując żądane wartości w nawias klamrowy. Nie trzeba wtedy korzystać z operatora
new. Środowisko Javy utworzy tablicę samodzielnie i zapisze w jej kolejnych komór-
kach podane wartości. Schematycznie deklaracja taka wygląda następująco:
typ_tablicy nazwa_tablicy[] = {wartość1, wartość2, ... , wartośćn}

Przykładowo aby zadeklarować 6-elementową tablicę liczb całkowitych typu int i przypi-
sać jej kolejnym komórkom wartości od 1 do 6, należy zastosować konstrukcję:
int tablica[] = {1, 2, 3, 4, 5, 6}

O tym, że taka konstrukcja jest prawidłowa, można przekonać się, uruchamiając kod
widoczny na listingu 2.40, gdzie ta tablica została zadeklarowana. Do wyświetlenia jej
zawartości na ekranie została użyta pętla typu for (por. lekcja 9.) i instrukcja System.out.
println. Wynik działania tego programu jest widoczny na rysunku 2.34.

Listing 2.40.
class Main {
public static void main (String args[]) {
int tablica[] = {1, 2, 3, 4, 5, 6};
for(int i = 0; i < 6; i++){
System.out.println("tab[" + i + "] = " + tablica[i]);
}
}
}

Rysunek 2.34.
Wynik działania pętli for
użytej do wyświetlenia
zawartości tablicy

14
Nie powinno się to jednak zdarzyć w żadnym z nowoczesnych systemów operacyjnych.

b4ac40784ca6e05cededff5eda5a930f
b
78 Java. Praktyczny kurs

Właściwość length
Kiedy rozmiar tablicy jest większy, zamiast stosować przedstawione wyżej przypisa-
nie w nawiasie klamrowym, lepiej do wypełnienia jej danymi wykorzystać zwyczajną
pętlę. A zatem jeśli mamy np. 20-elementową tablicę liczb typu int i chcemy zapisać
w każdej z jej komórek liczbę 15, możemy wykorzystać w tym celu konstrukcję:
for(int i = 0; i < 20, i++){
tab[i] = 15;
}

Gdy jednak pisze się w ten sposób, łatwo o pomyłkę. Może się np. zdarzyć, że progra-
mista zmieni rozmiar tablicy, ale zapomni zmodyfikować pętlę. Nierzadko zdarza się też
sytuacja, kiedy rozmiar tablicy nie jest z góry znany i zostaje ustalony dopiero w trakcie
działania programu. Na szczęście ten problem został rozwiązany w bardzo prosty
sposób. Dzięki temu, że tablice w Javie są obiektami, każda z nich ma przechowującą
jej rozmiar właściwość length, która może być tylko odczytywana. A zatem przy
użyciu konstrukcji w postaci:
nazwa_tablicy.length

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łkowi-
tymi 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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 79

Ćwiczenie 11.2.
Napisz program, w którym zostanie utworzona 10-elementowa tablica liczb typu int.
Użyj pętli for do wypełnienia jej danymi w taki sposób, aby w kolejnych komórkach
znalazły się liczby od 10 do 100 (czyli 10, 20, 30 itd.). Zawartość tablicy wyświetl na
ekranie.

Ćwiczenie 11.3.
Napisz program, w którym zostanie utworzona 20-elementowa tablica typu boolean.
Komórkom o indeksie parzystym przypisz wartość true, a o indeksie nieparzystym —
false (zero możesz uznać za wartość parzystą). Zawartość tablicy wyświetl na ekranie.

Ćwiczenie 11.4.
Spróbuj rozwiązać ćwiczenie 11.3 w taki sposób, aby zapisywanie danych w tablicy
odbywało się za pomocą jednej instrukcji przypisania (korzystającej z operatora =).

Ćwiczenie 11.5.
Napisz program, w którym zostanie utworzona 100-elementowa tablica liczb typu int.
Komórkom o indeksach 0, 10, 20, … , 90 przypisz wartość 0, komórkom 1, 11, 21, … , 91
wartość 1, komórkom 2, 12, 22, … , 92 wartość 2 itd.

Ćwiczenie 11.6.
Utwórz 26-elementową tablicę typu char. Zapisz w kolejnych komórkach małe litery
alfabetu od a do z.

Ćwiczenie 11.7.
Zmodyfikuj program z listingu 2.41 w taki sposób, aby do wypełnienia tablicy danymi
została wykorzystana pętla typu while.

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.

b4ac40784ca6e05cededff5eda5a930f
b
80 Java. Praktyczny kurs

Tablice dwuwymiarowe
W lekcji 11. zostały przedstawione podstawowe tablice jednowymiarowe, to znaczy takie,
które są wektorami elementów, czyli strukturami zaprezentowanymi na rysunku 2.32. Ta-
blice nie muszą być jednak jednowymiarowe, wymiarów może być więcej, np. dwa. Wte-
dy taka struktura wygląda tak jak na rysunku 2.35. Widać wyraźnie, że do wyznaczenia
konkretnej komórki trzeba tym razem podać dwie liczby określające rząd oraz kolumnę.

Rysunek 2.35.
Przykład tablicy
dwuwymiarowej

Taką tablicę trzeba zadeklarować oraz utworzyć. Odbywa się to tak samo jak w przy-
padku tablic jednowymiarowych. Jeśli schematyczna deklaracja tablicy jednowymia-
rowej wyglądała:
typ nazwa_tablicy[]

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 jednowy-
miarowych:
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 wi-
doczny na listingu 2.42. Tablica ta będzie wtedy wyglądać tak jak na rysunku 2.36.

15
Możliwa jest oczywiście również deklaracja w postaci typ[][] nazwa_tablicy.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 81

Rysunek 2.36.
Schematyczna postać
tablicy dwuwymiarowej
po wypełnieniu danymi

Listing 2.42.
class Main {
public static void main (String args[]) {
int tab[][] = {
{1, 2, 3, 4},
{5, 6, 7, 8}
};
for(int i = 0; i < 2; i++){
for(int j = 0; j < 4; j++){
System.out.print("tab[" + i + "," + j + "] = ");
System.out.println(tab[i][j]);
}
}
}
}

Do wyświetlenia zawartości tablicy na ekranie zostały użyte dwie pętle typu for. Pętla
zewnętrzna ze zmienną iteracyjną i przebiega kolejne wiersze tablicy, a wewnętrzna
ze zmienną iteracyjną j — kolejne komórki w danym wierszu. Aby odczytać zawartość
konkretnej komórki, stosuje się odwołanie:
tab[i][j]

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ć:

b4ac40784ca6e05cededff5eda5a930f
b
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 zasto-
sować instrukcję:
int tab[][] = new int[2][4];

Taką tablicę można wypełnić danymi przy użyciu zagnieżdżonych pętli for, tak jak
zostało to zaprezentowane na listingu 2.43.

Listing 2.43.
class Main {
public static void main (String args[]) {
int tab[][] = new int[2][4];
int licznik = 1;
for(int i = 0; i < 2; i++){
for(int j = 0; j < 4; j++){
tab[i][j] = licznik++;
}
}
for(int i = 0; i < 2; i++){
for(int j = 0; j < 4; j++){
System.out.print("tab[" + i + "," + j + "] = ");
System.out.println(tab[i][j]);
}
}
}
}

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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 83

Właściwość length
Zaprezentowany na listingu 2.43 sposób wypełniania tablicy (oraz wyświetlania jej za-
wartości na ekranie) ma jeden mankament. W przypadku obu pętli zakładamy, że roz-
miar tablicy jest znany. Jak sobie poradzić w sytuacji, kiedy rozmiar pozostaje nieznany?
W przypadku tablic jednowymiarowych mamy do dyspozycji właściwość length. Czy
istnieje ona również w tablicach wielowymiarowych? Jeśli tak, to jakie ma znaczenie?

Odpowiedź na pierwsze pytanie brzmi: oczywiście tak, tablica wielowymiarowa rów-


nież 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 do-
kł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 zre-
alizować w sposób przedstawiony na listingu 2.44.

b4ac40784ca6e05cededff5eda5a930f
b
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ój-
kątną (rysunek 2.39a) lub też całkiem nieregularną (rysunek 2.39b). Przy tworzeniu
takich struktur czeka nas jednak więcej pracy niż w przypadku tablic regularnych,
gdyż prawdopodobnie każdy wiersz trzeba będzie tworzyć oddzielnie16.

Jak tworzyć tego typu struktury? Wiadomo już, że tablice wielowymiarowe to tak na-
prawdę tablice tablic jednowymiarowych (tablica dwuwymiarowa to tablica jednowymia-
rowa zawierająca szereg tablic jednowymiarowych, tablica trójwymiarowa to tablica

16
Zależy to od kształtu tablicy, który chce się uzyskać.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 85

Rysunek 2.39.
Przykłady
nieregularnych tablic
wielowymiarowych

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 ry-
sunku 2.39b, należy teraz utworzyć 4-elementową tablicę, która będzie mogła przecho-
wywać tablice jednowymiarowe liczb typu int. Trzeba więc użyć operatora new w postaci:
new int[4][]

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] zna-
lazła się tablica 4-elementowa, w tab[1] — tablica 2-elementowa, w tab[2] — tablica
1-elementowa, w tab[3] — tablica 3-elementowa (rysunek 2.40). Należy zatem wykonać
ciąg instrukcji:
tab[0] = new int[4];
tab[1] = new int[2];
tab[2] = new int[1];
tab[3] = new int[3];

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 czwar-
tym — 8, 9, 10, tak jak zostało to zobrazowane na rysunku 2.41. Zatem do wypełnie-
nia tablicy można użyć zagnieżdżonej pętli for, analogicznie do przypadku tablicy re-
gularnej 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.

b4ac40784ca6e05cededff5eda5a930f
b
86 Java. Praktyczny kurs

Rysunek 2.40.
Tworzenie nieregularnej
tablicy dwuwymiarowej

Rysunek 2.41.
Nieregularna tablica
wypełniona danymi

Listing 2.45.
class Main {
public static void main (String args[]) {
int[][] tab = new int[4][];
tab[0] = new int[4];
tab[1] = new int[2];
tab[2] = new int[1];
tab[3] = new int[3];

int licznik = 1;
for(int i = 0; i < tab.length; i++){
for(int j = 0; j < tab[i].length; j++){
tab[i][j] = licznik++;
}
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 2.  Instrukcje języka 87

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 poprzed-
nich akapitach). Jest ona wypełniana danymi tak, aby uzyskać w poszczególnych ko-
mó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 li-
stingu 2.44. Pętla zewnętrzna, ze zmienną iteracyjną i, odpowiada za przebieg po kolej-
nych 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 ze-
wnętrznej jest umieszczona instrukcja System.out.print("tab[" + i + "] = ") wy-
świetlająca numer aktualnie przetwarzanego wiersza tabeli, natomiast w pętli wewnętrznej
instrukcja System.out.print(tab[i][j] + " ") wyświetlająca zawartość komórek w da-
nym wierszu.

Rysunek 2.42.
Wyświetlenie danych
z tablicy nieregularnej

Spróbujmy teraz utworzyć tablicę, której struktura została przedstawiona na rysunku 2.39A.
Po wyjaśnieniach z ostatniego przykładu nie powinno to przysporzyć najmniejszych pro-
blemów. Wypełnijmy ją danymi tak samo jak w poprzednim przypadku — umieszczając
w komórkach kolejne wartości od 1 do 10. Deklaracja i inicjalizacja będą wyglądać
następująco:
int[][] tab = new int[4][];

Kolejne wiersze tablicy można utworzyć za pomocą serii instrukcji:


tab[0] = new int[4];
tab[1] = new int[3];
tab[2] = new int[2];
tab[3] = new int[1];

b4ac40784ca6e05cededff5eda5a930f
b
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] = new int[4];
tab[1] = new int[3];
tab[2] = new int[2];
tab[3] = new int[1];

można również wykorzystać odpowiednio skonstruowaną pętlę typu for. Skoro każdy
kolejny wiersz ma o jedną komórkę mniej, powinna być to pętla zliczająca od 4 do 1,
czyli powinna wyglądać następująco:
for(int i = 0; i < 4; i++){
tab[i] = new int[4 - i];
}

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 odpo-
wiednik czterech ręcznie napisanych instrukcji.

b4ac40784ca6e05cededff5eda5a930f
b
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 wyko-
rzystywane pętle typu while.

Ćwiczenie 12.3.
Utwórz tablicę liczb typu int zaprezentowaną na rysunku 2.43. Wypełnij kolejne ko-
mórki wartościami malejącymi od 10 do 1. Do utworzenia tablicy i wypełnienia jej
danymi wykorzystaj pętle typu for.

Rysunek 2.43.
Odwrócona tablica
trójkątna (ilustracja
do ćwiczenia 12.3)

Ćwiczenie 12.4.
Utwórz trójwymiarową tablicę dla wartości typu int (będzie to struktura, którą można
sobie wyobrazić jako prostopadłościan składający się z sześcianów; każdy sześcian będzie
pojedynczą komórką). Powinna umożliwiać przechowywanie trzydziestu wartości.
Poszczególne komórki wypełnij liczbami od 30 do 59. Zawartość wyświetl na ekranie.
Zastanów się, ile wersji tego typu tablicy można utworzyć.

Ćwiczenie 12.5.
Utwórz tablicę dwuwymiarową, w której liczba komórek w kolejnych rzędach będzie
równa dziesięciu kolejnym wartościom ciągu Fibonacciego, poczynając od elementu
o wartości 1 (1, 1, 2, 3, 5 itd.). Wartość każdej komórki powinna być jej numerem w da-
nym wierszu w kolejności malejącej (czyli dla wiersza o długości trzech komórek kolejne
wartości to 3, 2, 1). Zwartość tablicy wyświetl na ekranie.

b4ac40784ca6e05cededff5eda5a930f
b
90 Java. Praktyczny kurs

Ćwiczenie 12.6.
Zmodyfikuj kod z ćwiczenia 12.5 w taki sposób, aby w kolejnych rzędach znajdowała się
liczba komórek wynikająca z kolejnych wartości ciągu Fibonacciego, ale tylko tych
nieparzystych (czyli 1, 1, 3, 5, 13, 21 itd.).

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.
Programowanie
obiektowe. Część I
Każdy program w Javie składa się z jednej lub wielu klas. W przykładach przedstawio-
nych dotychczas była to tylko jedna klasa o nazwie Main. Przypomnijmy sobie nasz
pierwszy program wyświetlający na ekranie napis. Jego kod wyglądał następująco:
class Main {
public static void main(String args[]){
System.out.println("To jest napis");
}
}

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.

b4ac40784ca6e05cededff5eda5a930f
b
92 Java. Praktyczny kurs

Zajmiemy się tworzeniem klas, ich strukturą i deklaracjami, omówiony zostanie też
związek między klasą i obiektem. W tej lekcji zostaną również przedstawione składowe
klasy, czyli pola i metody, nie pominiemy także kwestii domyślnych wartości pól.
Okaże się też, jakie relacje występują między zadeklarowaną na stosie zmienną obiek-
tową (inaczej referencyjną, odnośnikową) a utworzonym na stercie obiektem.

Pierwsza klasa
Jak wiadomo z rozdziału 1., klasy są opisami obiektów, czyli bytów programistycznych,
które mogą przechowywać dane oraz wykonywać polecone przez programistę zadania.
Każdy obiekt jest instancją, czyli wystąpieniem jakiejś klasy. W związku z tym klasa
określa także typ danego obiektu. Przypomnijmy, że typ określa rodzaj wartości, jakie
może przyjmować dany byt programistyczny, na przykład zmienna. Jeśli np. mamy
zmienną typu int, to może ona przyjmować jedynie wartości typu int, czyli liczby
z zakresu od –2 147 483 648 do 2 147 483 647. Z obiektami jest podobnie: zmienna
obiektowa hipotetycznej klasy Punkt może przechowywać obiekty klasy (typu) Punkt1.
Klasa to zatem nic innego jak definicja nowego typu danych. Ponieważ dla osób nieobez-
nanych z programowaniem obiektowym to wszystko może brzmieć zawile, zobaczmy
od razu, jak to wygląda w praktyce.

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ładowy-
mi klasy). W przypadku klasy, która ma przechowywać dane dotyczące współrzędnych
x i y, wystarczą więc dwa pola typu int2. Pozostaje jeszcze wybór nazwy dla takiej
klasy. Występują tu takie same ograniczenia jak w przypadku nazewnictwa zmien-
nych (lekcja 4.), czyli nazwa klasy może składać się jedynie z liter (zarówno małych,
jak i dużych), cyfr oraz znaku podkreślenia, ale nie może zaczynać się od cyfry. Nie
zaleca się również stosowania w nazwach klas polskich znaków diakrytycznych, szcze-
gólnie że nazwa klasy musi3 być zgodna z nazwą pliku, w którym dana klasa została
zapisana (lekcja 1.).

Utworzoną klasę nazwiemy zatem, jakżeby inaczej, Punkt i będzie ona miała postać
widoczną na listingu 3.1. Kod ten należy zapisać w pliku o nazwie Punkt.java.

1
Jak zobaczymy w dalszej części książki, takiej zmiennej można również przypisać obiekty klas potomnych
lub nadrzędnych w stosunku do klasy Punkt.
2
Załóżmy, że chodzi np. o punkty ekranowe (piksele) i wystarczy, jeśli będą mogły przyjmować tylko
współrzędne całkowite.
3
Jak zostanie to wyjaśnione w dalszej części książki, to stwierdzenie dotyczy tylko klas publicznych.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 93

Tu ponownie trzeba zwrócić uwagę na to, że każdą klasę zapisuje się w oddzielnym pliku
o nazwie zgodnej z jej nazwą oraz rozszerzeniu java. A zatem klasę Punkt zapisuje się
w pliku Punkt.java, klasę Main w pliku Main.java, a klasę Cos w pliku Cos.java. W dalszej
części książki na jednym listingu będzie znajdowało się nawet kilka klas, ale należy je
również zapisywać w oddzielnych plikach w tym samym katalogu, chyba że zostanie
wyraźnie napisane, by zrobić inaczej. Dokładne wyjaśnienie tej kwestii znajdzie się
w lekcji 17.

Listing 3.1.
class Punkt {
int x;
int y;
}

Jak widać, klasa zawiera tylko dwa pola o nazwach x i y, które za pomocą wartości
całkowitych opisują współrzędne położenia punktu. Po zdefiniowaniu klasy Punkt można
zadeklarować zmienną typu Punkt. Robi się to w dokładnie taki sam sposób, w jaki
deklarowane były zmienne typów prostych (np. short, int, char), pisząc:
typ_zmiennej nazwa_zmiennej;

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 do-


myś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ów-
nie poprawna jest konstrukcja w postaci:
Punkt przykladowyPunkt;
przykladowyPunkt = new Punkt();

4
Osoby programujące w C++ powinny zwrócić na to uwagę, gdyż w tym języku już sama deklaracja
zmiennej typu klasowego powoduje wywołanie domyślnego konstruktora i utworzenie obiektu.

b4ac40784ca6e05cededff5eda5a930f
b
94 Java. Praktyczny kurs

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 przypi-
sanego 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 utwo-


rzymy zmienną odnośnikową klasy Punkt oraz obiekt klasy Punkt, przypiszemy po-
lom 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 po-
stać 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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 95

Od tej chwili zmienna punkt wskazuje na obiekt klasy Punkt, można się zatem posłu-
giwać nią tak jak samym obiektem. Pisząc punkt.x = 100, przypisujemy wartość 100
polu x, a pisząc punkt.y = 100, przypisujemy wartość 100 polu y. W ostatnich dwóch
liniach korzystamy z instrukcji System.out.println, aby wyświetlić wartość obu pól
na ekranie. Wystarczy teraz skompilować i uruchomić program. Efekt jest widoczny
na rysunku 3.2.

Rysunek 3.2.
Wynik działania klasy
Main z listingu 3.2

Nie trzeba oddzielnie kompilować klas Main i Punkt. Ponieważ w klasie Main jest
wykorzystywana klasa Punkt, wydanie polecenia javac Main.java spowoduje kompi-
lację zarówno pliku Main.java, jak i Punkt.java. Obie klasy muszą natomiast znaj-
dować się w tym samym katalogu.

Metody klas
Klasy oprócz pól przechowujących dane zawierają także metody, które wykonują za-
pisane przez programistę operacje. Definiuje się je w ciele (czyli wewnątrz) klasy
pomiędzy znakami nawiasu klamrowego. Każda metoda może przyjmować argumenty
(nazywane także parametrami; oba terminy często stosowane są zamiennie) oraz
zwracać wynik. Schematyczna deklaracja metody wygląda następująco:
typ_wyniku nazwa_metody(argumenty_metody) {
//instrukcje metody
}

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);
}

b4ac40784ca6e05cededff5eda5a930f
b
96 Java. Praktyczny kurs

Słowo void oznacza, że metoda nie zwraca żadnego wyniku, a brak argumentów po-
między znakami nawiasu okrągłego mówi, że metoda ta nie przyjmuje żadnych ar-
gumentów. Wewnątrz metody znajdują się dwie dobrze nam znane instrukcje, które
wyświetlają na ekranie współrzędne punktu. Jeśli umieścimy powyższy kod wewnątrz
klasy Punkt, będzie ona miała postać widoczną na listingu 3.3.

Listing 3.3.
class Punkt {
int x;
int y;
void wyswietlWspolrzedne() {
System.out.println("współrzędna x = " + x);
System.out.println("współrzędna y = " + y);
}
}

Po utworzeniu obiektu danej klasy można wywołać metodę (czyli wykonać zawarte
w niej instrukcje) w sposób identyczny z tym, w jaki odwołujemy się do pól klasy, tzn.
korzystając z operatora . (kropka). Jeśli zatem przykładowa zmienna punkt zawiera refe-
rencję do obiektu klasy Punkt, prawidłowym wywołaniem metody wyswietlWspolrzedne
będzie:
punkt.wyswietlWspolrzedne();

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. Wykorzystaj-
my teraz klasę Main do przetestowania nowej konstrukcji. Zmodyfikujemy program
z listingu 3.2 tak, aby korzystał z metody wyswietlWspolrzedne. Odpowiedni kod jest za-
prezentowany na listingu 3.4. Wynik jego działania łatwo przewidzieć (rysunek 3.3).

Listing 3.4.
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
punkt.x = 100;
punkt.y = 100;
punkt.wyswietlWspolrzedne();
}
}

Rysunek 3.3.
Wynik działania metody
wyswietlWspolrzedne
klasy Punkt

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 97

Zobaczmy teraz, w jaki sposób napisać metody, które będą potrafiły zwracać wyniki.
Typ wyniku należy podać przed nazwą metody, zatem jeśli ma ona zwracać liczbę typu
int, deklaracja powinna wyglądać następująco:
int nazwa_metody() {
//instrukcje metody
}

Sam wynik zwraca się natomiast przez zastosowanie instrukcji return. Najlepiej zo-
baczyć to na praktycznym przykładzie. Do klasy Punkt dodamy zatem dwie metody
— jedna będzie podawała wartość współrzędnej x, druga y. Nazwiemy je odpowiednio
pobierzX i pobierzY. Wygląd metody pobierzX będzie następujący:
int pobierzX() {
return x;
}

Przed nazwą metody znajduje się określenie typu zwracanego przez nią wyniku —
skoro jest to int, oznacza to, że metoda ta musi zwrócić jako wynik liczbę całkowitą
z przedziału określonego przez typ int (tabela 1.1 z rozdziału 1.). Wynik jest zwracany
dzięki instrukcji return. Zapis return x oznacza zwrócenie przez metodę wartości zapisa-
nej w polu x.

Jak łatwo się domyślić, metoda pobierzY będzie wyglądała analogicznie, z tym że będzie
w niej zwracana wartość zapisana w polu y. Pełny kod klasy Punkt po dodaniu tych
dwóch metod będzie wyglądał tak, jak to zostało przedstawione na listingu 3.5. Na li-
stingu 3.6 widać z kolei kod przykładowej klasy Main wykorzystującej nowe metody
klasy Punkt.

Listing 3.5.
class Punkt {
int x;
int y;
int pobierzX() {
return x;
}
int pobierzY() {
return y;
}
void wyswietlWspolrzedne() {
System.out.println("współrzędna x = " + x);
System.out.println("współrzędna y = " + y);
}
}

Listing 3.6.
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
punkt.x = 100;
punkt.y = 100;

b4ac40784ca6e05cededff5eda5a930f
b
98 Java. Praktyczny kurs

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 pod-
stawiana w miejscu wywołania metody. A zatem w tym przypadku w miejscu wystą-
pienia wywołania:
punkt.pobierzX()

zostanie podstawiona wartość 100 (pole x ma wartość 100). Tak samo będzie też przy
wywołaniu:
punkt.pobierzY()

(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

b4ac40784ca6e05cededff5eda5a930f
b
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ą przecho-
wywaną wartość.

Ćwiczenie 13.2.
Napisz kod przykładowej klasy Prostokat zawierającej cztery pola przechowujące
współrzędne czterech rogów prostokąta. Wykorzystaj obiekty klasy Punkt.

Ćwiczenie 13.3.
Do utworzonej w ćwiczeniu 13.2 klasy Prostokat dopisz metody zwracające wartości po-
szczególnych współrzędnych oraz metodę wyświetlającą wszystkie współrzędne.

Ćwiczenie 13.4.
Napisz przykładową klasę Main testującą zachowanie klas LiczbaCalkowita (z ćwi-
czenia 13.1) oraz Prostokat (z ćwiczenia 13.3).

Ćwiczenie 13.5.
Napisz klasę Prostokat przechowującą jedynie współrzędne lewego górnego i prawego
dolnego rogu (wystarczają one do jednoznacznego wyznaczenia prostokąta na płasz-
czyźnie). Dodaj metody podające współrzędne każdego rogu.

b4ac40784ca6e05cededff5eda5a930f
b
100 Java. Praktyczny kurs

Ćwiczenie 13.6.
Do klasy Prostokat z ćwiczenia 13.2 lub 13.3 dopisz metodę sprawdzającą, czy wpro-
wadzone współrzędne rzeczywiście definiują prostokąt (cztery punkty na płaszczyźnie
dają dowolny czworokąt, który nie musi być prostokątem).

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żli-
wiają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. Opisa-
ny 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 me-
tody to inaczej dane, które można jej przekazać. Metoda może mieć dowolną liczbę
argumentów umieszczonych w nawiasie okrągłym za jej nazwą. Poszczególne argu-
menty oddziela się znakiem przecinka. Schematycznie wygląda to następująco:
typ_wyniku nazwa_metody(typ_parametru_1 nazwa_parametru_1, typ_parametru_2
nazwa_parametru_2, ... , typ_parametru_n nazwa_parametru_n) {
/*treść metody*/
}

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: na-
piszemy dwie metody, ustawX i ustawY. Pierwsza będzie odpowiedzialna za przypisa-
nie 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ą na-
tomiast jeden argument typu int. W ciele każdej z metod następuje z kolei przypisa-
nie wartości przekazanej w parametrze odpowiedniemu polu: x w przypadku metody
ustawX oraz y w przypadku metody ustawY.

W podobny sposób można napisać metodę, która będzie jednocześnie ustawiała pola x
i y obiektów klasy Punkt. Oczywiście będzie przyjmowała dwa argumenty. Jak wiadomo,
w deklaracji należy oddzielić je przecinkiem, zatem będzie ona wyglądała następująco:

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 101

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);

b4ac40784ca6e05cededff5eda5a930f
b
102 Java. Praktyczny kurs

System.out.println("nowyPunkt:");
nowyPunkt.wyswietlWspolrzedne();
}
}

Na początku tworzymy dwa obiekty typu (klasy) Punkt, jeden z nich przypisujemy
zmiennej o nazwie punkt, drugi — zmiennej o nazwie nowy_punkt.

Tak naprawdę (jak już wiadomo z wcześniejszego opisu) obu zmiennym zostały przypi-
sane referencje do utworzonych na stercie obiektów, jednak od tego miejsca książki
niekiedy będzie stosowana również taka uproszczona, ale często spotykana termino-
logia, czyli utożsamianie zmiennej ze wskazywanym przez nią obiektem. Zatem je-
śli zostanie napisane na przykład, że zmiennej jest przypisywany obiekt, w rzeczy-
wistości będzie chodziło o to, że jest jej przypisywana referencja do tego obiektu.

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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 103

Parametrem (argumentem) jest w tej chwili obiekt punkt klasy Punkt. W ciele metody
następuje skopiowanie wartości pól z obiektu przekazanego jako argument do obiektu
bieżącego, czyli przypisanie polu x wartości zapisanej w punkt.x, a polu y wartości
zapisanej w punkt.y.

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);
}
}

b4ac40784ca6e05cededff5eda5a930f
b
104 Java. Praktyczny kurs

Aby lepiej uzmysłowić sobie sposób działania wymienionych metod, warto napisać
teraz krótki program, który będzie je wykorzystywał. Powstanie oczywiście kolejna
wersja klasy Main. Jest ona widoczna na listingu 3.11.

Listing 3.11.
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
Punkt drugiPunkt;

punkt.ustawX(100);
punkt.ustawY(200);

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 przypisu-


jemy 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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 105

ta jako argument przyjmuje obiekt klasy Punkt, w tym przypadku obiekt drugiPunkt.
Zatem po wykonaniu instrukcji wartości pól x i y obiektu trzeciPunkt będą takie same jak
pól x i y obiektu drugiPunkt (rysunek 3.7c). Nic więc dziwnego, że wynik działania
programu z listingu 3.11 jest taki jak zaprezentowany na rysunku 3.6. Z kolei na ry-
sunku 3.7 przedstawiono schematyczne zależności pomiędzy zmiennymi i obiektami
występującymi w klasie Main.

Rysunek 3.6.
Utworzenie trzech takich
samych obiektów
różnymi metodami

Rysunek 3.7. Kolejne etapy powstawania zmiennych i obiektów w programie z listingu 3.11

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).

b4ac40784ca6e05cededff5eda5a930f
b
106 Java. Praktyczny kurs

Metoda main
Każdy program musi zawierać punkt startowy, czyli miejsce, od którego zacznie się jego
wykonywanie. W Javie takim miejscem jest metoda o nazwie main i następującej deklaracji:
public static void main(String[] args) {
}

Jeśli w danej klasie znajdzie się metoda w takiej postaci, od niej właśnie zacznie się
wykonywanie kodu programu. Teraz powinno być już jasne, czemu w dwóch pierwszych
rozdziałach przykładowe programy miały schematyczną konstrukcję:
class Main {
public static void main(String args[]) {
//tutaj instrukcje do wykonania
}
}

Przy użyciu tej konstrukcji była po prostu tworzona pomocnicza klasa o nazwie Main,
a w niej metoda main, od której zaczynało się wykonywanie kodu aplikacji. Rzecz jasna
klasa ta mogłaby mieć dowolną inną nazwę, np. KlasaUruchomieniowa lub podobną.

Metoda main nie może zwracać żadnej wartości, czyli typem zwracanym musi być void.
Przed słowem void występują jeszcze dwa terminy, public i static, jednak nie będą
w tej chwili omawiane — przyjdzie na to czas w dalszej części książki. Warto natomiast
w tym miejscu zwrócić uwagę na argument metody main, którym jest, jak już wiadomo
z rozdziału 2., tablica typu String. Typ String opisuje obiekty, które są ciągami zna-
ków, czyli napisami. Innymi słowy, args to tablica ciągów znaków.

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 najwygod-
niejsze. Co więcej, nawet w ramach jednej aplikacji każda z klas wchodzących w jej
skład może mieć swoją własną metodę main. O tym, która z tych metod zostanie wy-
konana, decyduje to, która klasa będzie klasą uruchomieniową. Klasa uruchomieniowa to
ta klasa, której nazwę podaje się jako parametr dla maszyny wirtualnej. Czyli jeśli
uruchamiamy aplikację, pisząc:
java Main

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 107

Rysunek 3.8.
Program wyświetlający
parametry jego
wywołania

klasą uruchomieniową jest 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 umiesz-
czenie punktu startowego w dowolnej klasie, może to być także klasa Punkt. Dotychczas
do przetestowania działania obiektów klasy Punkt pisaliśmy dodatkową klasę Main, jed-
nak okazuje się, że nie jest to wcale potrzebne (zastosowanie dwóch klas jest jednak
bardziej czytelne dla osób początkujących).

Spójrzmy teraz ponownie na listingi 3.1 i 3.2 z lekcji 13. Na pierwszym z nich znajduje
się podstawowa wersja klasy Punkt, która zawiera jedynie pola x i y. Drugi zawiera
klasę Main, która testuje zachowanie obiektu klasy Punkt. Połączmy je zatem w taki spo-
sób, aby metoda main znalazła się w klasie Punkt, bez zmieniania samej treści tej meto-
dy. Rozwiązanie tego zadania jest przedstawione na listingu 3.13.

Listing 3.13.
class Punkt {
int x;
int y;

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.

b4ac40784ca6e05cededff5eda5a930f
b
108 Java. Praktyczny kurs

Rysunek 3.9.
Klasa Punkt jako klasa
uruchomieniowa

Przeciążanie metod
W trakcie pracy nad kodem klasy Punkt powstały dwie metody o takiej samej nazwie,
ale różnym kodzie. Chodzi oczywiście o metody ustawXY. Pierwsza wersja przyjmowała
jako argumenty dwie liczby typu int, a druga miała tylko jeden argument, którym był
obiekt klasy Punkt. Okazuje się, że takie dwie metody mogą współistnieć w klasie
Punkt i z obu z nich można korzystać w kodzie programu.

Ogólnie rzecz ujmując, w każdej klasie może istnieć dowolna liczba metod, które
mają takie same nazwy, o ile tylko różnią się argumentami. Mogą one — ale nie muszą —
różnić się również typem zwracanego wyniku. Taką technikę nazywa się przeciąża-
niem metod (ang. overloading). Skonstruujmy zatem taką klasę Punkt, w której znajdą
się obie wersje metody ustawXY. Kod tej klasy jest przedstawiony na listingu 3.14.

Listing 3.14.
class Punkt {
int x;
int y;

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. usta-
wiają nowe wartości w polach x i y. Ich działanie można przetestować poprzez dopisanie
w klasie Punkt metody main w przykładowej postaci przedstawionej na listingu 3.15.
Listing 3.15.
public static void main (String args[]) {
Punkt punkt = new Punkt();
Punkt drugiPunkt = new Punkt();

punkt.ustawXY(100, 100);
drugiPunkt.ustawXY(200,200);

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 109

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 tworzy-
my dwa obiekty klasy Punkt i przypisujemy je zmiennym punkt oraz drugiPunkt. Na-
stępnie korzystamy z pierwszej wersji przeciążonej metody ustawXY, aby przypisać
polom x i y pierwszego obiektu wartość 100, a polom x i y drugiego obiektu — 200.
Dalej wyświetlamy zawartość obiektu punkt na ekranie. Potem wykorzystujemy dru-
gą wersję metody ustawXY, aby zmienić zawartość pól obiektu punkt, tak by zawierały
wartości zapisane w obiekcie drugiPunkt. Następnie ponownie wyświetlamy wartości
pól obiektu punkt na ekranie.

Ć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 po-
przednią wartość zapisywanych pól. Zadaniem metody ustawX jest więc zmiana wartości
pola x i zwrócenie jego poprzedniej wartości. Metoda ustawY ma wykonywać analogiczne
czynności w stosunku do pola y.

Ćwiczenie 14.5.
Do klasy Punkt dopisz metodę ustawXY przyjmującą jako argument obiekt klasy Punkt.
Polom x i y należy przypisać wartości pól x i y przekazanego obiektu. Metoda ma nato-
miast zwrócić obiekt klasy Punkt zawierający stare wartości x i y.

b4ac40784ca6e05cededff5eda5a930f
b
110 Java. Praktyczny kurs

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 konstruk-
tor, 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 kon-
struktora, 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 przedsta-
wione w tabeli 3.1. Najczęściej jednak oczekuje się, by pola te zawierały jakieś konkretne
wartości. Przykładowo można byłoby życzyć sobie, aby każdy obiekt powstałej w lekcji
13. (listing 3.1) klasy Punkt otrzymywał współrzędne x = 1 i y = 1. Oczywiście można
po każdym utworzeniu obiektu przypisywać wartości tym polom, np.:
Punkt punkt = new Punkt();
punkt.x = 1;
punkt.y = 1;

Można też dopisać do klasy Punkt dodatkową metodę, na przykład o nazwie init,
initialize, inicjuj lub podobnej, w postaci:
void init() {
x = 1;
y = 1;
}

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 zaw-
sze 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 wygod-
niejszy mechanizm konstruktorów. Otóż konstruktor jest to specjalna metoda, która zo-
staje 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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 111

Dopiszmy zatem do klasy Punkt z listingu 3.1 (czyli jej najprostszej wersji) konstruktor,
który będzie przypisywał polom x i y każdego obiektu wartość 1. Wygląd takiej klasy
został zaprezentowany na listingu 3.16.

Listing 3.16.
class Punkt {
int x;
int y;
Punkt() {
x = 1;
y = 1;
}
}

Jak widać, wszystko jest tu zgodne z podanym wyżej schematem. Konstruktor nie
zwraca żadnej wartości i ma nazwę zgodną z nazwą klasy. Przed nazwą nie występuje
słowo void. W jego wnętrzu następuje proste przypisanie wartości polom obiektu. O tym,
że konstruktor działa, można się przekonać, pisząc dodatkową klasę Main i używając
w niej obiektu nowej klasy Punkt (można by też dopisać metodę main do klasy Punkt).
Taka przykładowa klasa Main jest widoczna na listingu 3.17.

Listing 3.17.
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
System.out.println("punkt.x = " + punkt.x);
System.out.println("punkt.y = " + punkt.y);
}
}

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 za-
wartoś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 (lek-
cja 14.), budowa takiego konstruktora byłaby więc następująca:

b4ac40784ca6e05cededff5eda5a930f
b
112 Java. Praktyczny kurs

class nazwa_klasy {
nazwa_klasy(typ1 argument1, typ2 argument2,..., typN argumentN) {
}
}

Jeśli konstruktor przyjmuje argumenty, przy tworzeniu obiektu należy je podać, czyli
zamiast stosowanej do tej pory konstrukcji:
nazwa_klasy zmienna = new nazwa_klasy()

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 pew-
noś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 po-
czątkowa współrzędna y — 200, należy zastosować konstrukcję:
Punkt punkt = new Punkt(100, 200);

Przeciążanie konstruktorów
Konstruktory, tak jak zwykłe metody, mogą być przeciążane, tzn. każda klasa może mieć
kilka konstruktorów, o ile tylko różnią się one przyjmowanymi argumentami. Do tej
pory powstały dwa konstruktory klasy Punkt: pierwszy bezargumentowy i drugi przyj-
mujący dwa argumenty typu int. Dopiszmy zatem jeszcze trzeci, który jako argument
będzie przyjmował obiekt klasy Punkt. Jego postać będzie następująca:
Punkt(Punkt punkt) {
x = punkt.x;
y = punkt.y;
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 113

Zasada działania jest chyba jasna: polu x jest przypisywana wartość pola x obiektu
przekazanego jako argument, natomiast polu y — wartość pola y tego obiektu. Można
teraz zebrać wszystkie trzy napisane dotychczas konstruktory i umieścić je w klasie
Punkt. Będzie ona wtedy miała postać przedstawioną na listingu 3.19.

Listing 3.19.
class Punkt {
int x;
int y;
Punkt() {
x = 1;
y = 1;
}
Punkt(int wspX, int wspY) {
x = wspX;
y = wspY;
}
Punkt(Punkt punkt) {
x = punkt.x;
y = punkt.y;
}
}

Taka budowa klasy Punkt pozwala na niezależne wywoływanie każdego z trzech kon-
struktorów, w zależności od tego, który z nich jest najbardziej odpowiedni w danej
sytuacji. Warto teraz na konkretnym przykładzie przekonać się, że tak jest w istocie.
Napiszmy więc klasę Main, w której powstaną trzy obiekty klasy Punkt, a każdy z nich
będzie tworzony za pomocą innego konstruktora. Taka przykładowa klasa jest wi-
doczna na listingu 3.20.

Listing 3.20.
class Main {
public static void main (String args[]) {
Punkt punkt1 = new Punkt();

System.out.println("punkt1:");
System.out.println("x = " + punkt1.x);
System.out.println("y = " + punkt1.y);
System.out.println("");

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);
}
}

b4ac40784ca6e05cededff5eda5a930f
b
114 Java. Praktyczny kurs

Pierwszy obiekt — punkt1 — jest tworzony za pomocą konstruktora bezargumento-


wego, 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 odzwier-
ciedlające wartości x i y. Oba pola otrzymują wartość 100. Konstruktor trzeci, zastoso-
wany 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 trak-
tować 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ć podob-
nie 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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 115

Otóż jeśli chcemy zaznaczyć, że chodzi o składową klasy (pole, metodę), korzystamy
z odwołania w postaci:
this.nazwa_pola

lub:
this.nazwa_metody

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 argu-
ment 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 przed-
stawionych 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 ar-
gument 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 prze-


kazanymi wartościami w argumentach x i y, z kolei trzeci skopiuje wartości argumen-
tó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;

b4ac40784ca6e05cededff5eda5a930f
b
116 Java. Praktyczny kurs

Punkt() {
ustawXY(1, 1);
}
Punkt(int x, int y) {
ustawXY(x, y);
}
Punkt(Punkt punkt) {
ustawXY(punkt);
}
void ustawXY(int wspX, int wspY) {
x = wspX;
y = wspY;
}
void ustawXY(Punkt punkt) {
x = punkt.x;
y = punkt.y;
}
}

Jest to w pełni funkcjonalny odpowiednik klasy z listingu 3.19. Można się o tym przeko-
nać, uruchamiając program testowy zamieszczony na listingu 3.20. Wynik jego działania
będzie dokładnie taki sam jak w poprzednim przypadku, czyli taki, jaki przedstawiono
na rysunku 3.11.

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 sytu-
acji, kiedy pola obiektu oraz argumenty metody mają takie same nazwy, występuje
problem z rozróżnieniem, czy chodzi o pola, czy o argumenty. Czy w tym przypadku
również istnieje taka wątpliwość i czy w związku z tym ten konstruktor jest na pewno po-
prawny? Oczywiście jest poprawny. Jeśli bowiem gdziekolwiek we wnętrzu metody
odwołujemy się do nazwy argumentu, to niezależnie od tego, czy istnieje pole o takiej
nazwie, czy nie, kompilator zawsze przyjmuje, że chodzi o argument. Można powiedzieć,
że w ciele metody argument ma większą siłę niż nazwa pola. Tak więc w powyższym
przykładzie, jeśli napiszemy x lub y, to na pewno chodzi o argumenty, a jeśli chcemy się
odwołać do pól, musimy napisać this.x lub this.y. Dzięki temu sytuacja jest jedno-
znaczna i zawsze wiadomo, o jakie odwołanie chodzi.

Z wnętrza konstruktora można również wywołać… inny konstruktor. Bywa to przydat-


ne 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 konstrukto-
ró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)

b4ac40784ca6e05cededff5eda5a930f
b
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 war-
tości argumentu liczba2 polu liczba2. Rzecz jasna, ponieważ argument ma taką samą
nazwę jak pole, wykorzystujemy znaną nam już dobrze konstrukcję ze słowem klu-
czowym this.

W tym miejscu pojawi się zapewne pytanie, czy nie można byłoby tu zastosować nastę-
pującej postaci konstruktora:
Dane(int liczba1, double liczba2) {
this(liczba1);
this(liczba2);
}

Wydaje się ona bardziej logiczna: skoro bowiem istnieje konstruktor przyjmujący je-
den argument typu int oraz drugi przyjmujący jeden argument typu double, w kon-
struktorze dwuargumentowym należałoby wywołać je oba (tak jak na listingu powyżej).
Niestety nie wolno nam tego zrobić. Zasada jest tutaj następująca: konstruktor można
wywołać jawnie tylko w innym konstruktorze i musi on być pierwszą wykonywaną
instrukcją. Oznacza to, że można wywołać tylko jeden konstruktor, a przed nim nie
powinna się znaleźć żadna inna instrukcja.

b4ac40784ca6e05cededff5eda5a930f
b
118 Java. Praktyczny kurs

Do przetestowania klasy Dane z listingu 3.22 można wykorzystać zawartą w niej me-
todę wyswietlDane. Taki przykładowy program jest widoczny na listingu 3.23. Wynik
jego działania można obejrzeć na rysunku 3.12.
Listing 3.23.
class Main {
public static void main (String args[]) {
Dane dane1 = new Dane(100);
Dane dane2 = new Dane(99.9);
Dane dane3 = new Dane(200, 88.8);

System.out.println("dane1:");
dane1.wyswietlDane();
System.out.println("");

System.out.println("dane2:");
dane2.wyswietlDane();
System.out.println("");

System.out.println("dane3:");
dane3.wyswietlDane();
}
}

Rysunek 3.12.
Wynik działania
programu z listingu 3.23

Metoda finalize
Osoby, które programowały w językach obiektowych, takich jak np. C++ czy Object
Pascal, zwykle zastanawiają się, jak w Javie wygląda destruktor i kiedy zwalniać pa-
mięć zarezerwowaną dla obiektów. Skoro bowiem operator new pozwala na utworzenie
obiektu, a tym samym na zarezerwowanie dla niego pamięci operacyjnej, logicznym
założeniem jest, że po jego wykorzystaniu pamięć tę należy zwolnić. Ponieważ jednak ta-
kie podejście, tzn. zrzucenie na barki programistów konieczności zwalniania przydzielonej
obiektom pamięci, powodowało powstawanie wielu błędów, w Javie zastosowano inne
rozwiązanie. Otóż za zwalnianie pamięci odpowiada maszyna wirtualna, a programista
praktycznie nie ma nad tym procesem kontroli7.

7
Wywołując metodę System.gc(), można jednak wymusić zainicjowanie procesu odzyskiwania pamięci.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 119

Zajmuje się tym tak zwany odśmiecacz (ang. garbage collector), który czuwa nad
optymalnym wykorzystaniem pamięci i uruchamia proces jej odzyskiwania w momencie,
kiedy wolna ilość oddana do dyspozycji programu zbytnio się zmniejszy. Jest to wy-
jątkowo wygodne podejście dla programisty, zwalnia go bowiem z obowiązku zarządza-
nia pamięcią. Zwiększa jednak nieco narzuty czasowe związane z wykonaniem programu,
bo sam proces odśmiecania musi zająć czas procesora. Niemniej dzisiejsze maszyny
wirtualne są na tyle dopracowane, że w większości przypadków nie ma najmniejszej
potrzeby zaprzątania sobie głowy tym problemem.

Należy jednak zdawać sobie sprawę, że Java jest w stanie automatycznie zarządzać
wykorzystywaniem pamięci, ale tylko tej, która jest alokowana standardowo, czyli za
pomocą operatora new. W nielicznych przypadkach, np. gdyby stworzony obiekt wykorzy-
stywał mechanizmy alokacji pamięci specyficzne dla danej platformy systemowej czy
też odwoływał się do modułów napisanych w innych językach programowania, o po-
sprzątanie systemu i zwolnienie zarezerwowanej pamięci trzeba byłoby zadbać samemu.

Java udostępnia w tym celu metodę finalize, która jest wykonywana zawsze, kiedy
obiekt jest niszczony, usuwany z pamięci. Wystarczy więc, że klasa będzie zawierała taką
metodę, a przy niszczeniu obiektu zostanie ona wykonana. Wewnątrz tej metody można
wykonać dowolne instrukcje sprzątające. Jej deklaracja wygląda następująco:
public void finalize() {
//tutaj treść metody
}

Koniecznie jednak należy sobie uświadomić jedną rzecz: nie ma żadnej gwarancji, że
ta metoda zostanie wykonana w trakcie działania programu! Przypomnijmy: proces
odzyskiwania pamięci, a więc niszczenia nieużywanych już obiektów, zaczyna się
wtedy, kiedy garbage collector „uzna” to za stosowne. Czyli wtedy, kiedy przyjmie,
że ilość wolnej pamięci dostępnej dla programu zbytnio się zmniejszyła. Może się
więc okazać, że pamięć zostanie zwolniona dopiero po zakończeniu pracy aplikacji.
Dlatego jeśli niezbędne jest wykonanie dodatkowych czynności porządkowych w jakimś
określonym miejscu podczas działania programu, lepiej napisać dodatkową metodę
sprzątającą i jawnie ją wywołać.

Żeby się przekonać, że metoda finalize jest faktycznie wykonywana, możemy napisać
prostą klasę. Niech nosi nazwę Test. Została przedstawiona razem z testową klasą Main
na listingu 3.24 (kod można zapisać w dwóch osobnych plikach Test.java i Main.java
lub też tylko w jednym — Main.java).

Listing 3.24.
class Test {
public void finalize() {
System.out.println("Niszczenie obiektu.");
}
}

class Main {
public static void main (String args[]) {
Test test = null;
for(int i = 0; i < 10; i++){

b4ac40784ca6e05cededff5eda5a930f
b
120 Java. Praktyczny kurs

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 przy-
pisujemy jej wartość pustą — null. Następnie w pętli dziesięć razy tworzymy nowy
obiekt klasy Test i przypisujemy referencję do niego zmiennej test. Należy zauważyć, że
każde takie przypisanie powoduje utracenie poprzedniej wartości zmiennej test (czyli
poprzedniej referencji), a tym samym oznaczenie wcześniej przypisanego obiektu jako
nieużywanego. To z kolei oznacza, że po wykonaniu całej pętli zmienna test będzie za-
wierała referencję do ostatnio przypisanego, dziesiątego obiektu, natomiast wszystkie po-
przednie obiekty zostaną utracone i będą mogły zostać poddane procesowi odśmiecania.

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 spraw-
dzić, co się stanie, jeśli pętle opóźniające zostaną usunięte, i jak będzie się wtedy za-
chowywał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 jednoargu-
mentowy konstruktor przyjmujący argument typu int. Polu klasy nadaj nazwę liczba,
tak samo nazwij argument konstruktora.

8
Dokładniej, należy tę instrukcję traktować jako sugestię dla maszyny wirtualnej, aby uruchomiła proces
odzyskiwania pamięci. Nie ma stuprocentowej gwarancji, że zostanie on uruchomiony dokładnie w chwili
wykonania tej instrukcji, niemniej odbędzie się to w najbliższym możliwym momencie, chyba że
aplikacja wcześniej zakończy działanie.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 121

Rysunek 3.13.
Metoda finalize jest
wykonywana podczas
niszczenia obiektu

Ćwiczenie 15.2.
Do klasy powstałej w ćwiczeniu 15.1 dopisz przeciążony konstruktor bezargumentowy
ustawiający jej pole na wartość –1.

Ćwiczenie 15.3.
Napisz klasę zawierającą dwa pola: pierwsze typu double i drugie typu char. Dopisz
cztery przeciążone konstruktory: pierwszy przyjmujący jeden argument typu double,
drugi przyjmujący jeden argument typu char, trzeci przyjmujący dwa argumenty —
pierwszy typu double, drugi typu char — i czwarty przyjmujący również dwa argumenty
— pierwszy typu char, drugi typu double.

Ćwiczenie 15.4.
Zmień kod klasy powstałej w ćwiczeniu 15.3 tak, aby w konstruktorach dwuargumento-
wych były wykorzystywane konstruktory jednoargumentowe.

Ćwiczenie 15.5.
Zmodyfikuj program z listingu 3.24 w taki sposób, aby przed wywołaniem odśmiecacza
w programie nie było odwołania do żadnego obiektu na stercie (liczba obiektów po-
winna pozostać niezmienna). Ilu napisów o niszczeniu obiektu możesz się spodziewać
po uruchomieniu programu? Sprawdź również zachowanie aplikacji po usunięciu
znajdujących się na jej końcu pętli opóźniających.

Dziedziczenie
Dziedziczenie to jeden z fundamentów programowania obiektowego. Umożliwia sprawne
i łatwe wykorzystywanie raz napisanego kodu czy budowanie hierarchii klas przejmują-
cych swoje właściwości. Ten podrozdział zawiera cztery lekcje przybliżające temat dzie-
dziczenia. W lekcji 16. zaprezentowane są podstawy, czyli sposoby tworzenia klas
potomnych oraz zachowania konstruktorów klasy bazowej i potomnej. W lekcji 17.

b4ac40784ca6e05cededff5eda5a930f
b
122 Java. Praktyczny kurs

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 podsta-
wowe 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 Punkt3D {
int x;
int y;
int z;
}

Do tej klasy należałoby dalej dopisać pełny zestaw metod, które znajdowały się w klasie
Punkt, takich jak pobierzX, pobierzY, ustawX, ustawY itd., oraz dodatkowe metody ope-
rujące na współrzędnej z. Zauważmy jednak, że w takiej sytuacji w dużej części po prostu
powtarza się już raz napisany kod. Czym bowiem będzie się różniła metoda ustawX
klasy Punkt od metody ustawX klasy Punkt3D? Oczywiście niczym. Po prostu Punkt3D
jest pewnego rodzaju rozszerzeniem klasy Punkt. Rozszerza ją o dodatkowe możliwości
(pola, metody), pozostawiając stare właściwości bez zmian. Zamiast więc pisać cał-
kiem od nowa klasę Punkt3D, lepiej spowodować, aby przejęła ona wszystkie możliwości
klasy Punkt, wprowadzając dodatkowo swoje własne pola i metody. Jest to tak zwane
dziedziczenie, czyli jeden z fundamentów programowania obiektowego. Powiemy, że
klasa Punkt3D dziedziczy po klasie Punkt9, czyli przejmuje jej pola i metody oraz dodaje
swoje własne. Wtedy klasę Punkt nazwiemy klasą bazową (nadrzędną, nadklasą, ang.
superclass), a klasę Punkt3D — klasą potomną (pochodną, podrzędną, podklasą, ang.
subclass).

W Javie dziedziczenie jest wyrażane za pomocą słowa extends, a cała definicja sche-
matycznie wygląda następująco:

9
Często spotyka się również potoczną formę „dziedziczyć z”, np. „Klasa Punkt3D dziedziczy z klasy Punkt”.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 123

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 do-
datkowo otrzymała pole typu int o nazwie z. Przekonajmy się, że tak jest w istocie. Niech
klasy Punkt i Punkt3D wyglądają tak jak na listingu 3.25 (najlepiej zapisać je w dwóch
plikach: Punkt.java i Punkt3D.java).

Listing 3.25.
class Punkt {
int x;
int y;
int pobierzX() {
return x;
}
int pobierzY() {
return y;
}
void ustawX(int wspX) {
x = wspX;
}
void ustawY(int wspY) {
y = wspY;
}
void ustawXY(int wspX, int wspY) {
x = wspX;
y = wspY;
}
void ustawXY(Punkt punkt) {
x = punkt.x;
y = punkt.y;
}
void wyswietlWspolrzedne() {
System.out.println("współrzędna x = " + x);
System.out.println("współrzędna y = " + y);
}
}

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),

b4ac40784ca6e05cededff5eda5a930f
b
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śl-
ne 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świe-
tlamy 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 rze-
czywiś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 prze-
cież dopisać metody operujące na nowym polu z. Na pewno przydatne będą ustawZ,
pobierzZ oraz ustawXYZ. Oczywiście metoda ustawZ będzie przyjmowała jeden argument
typu int i przypisywała jego wartość polu z, metoda pobierzZ będzie zwracała wartość

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 125

Rysunek 3.14.
Klasa Punkt3D przejęła
pola i metody klasy
Punkt

pola z, natomiast ustawXYZ będzie przyjmowała trzy argumenty typu int i przypisywała je
polom x, y i z. Łatwo się domyślić, że metody te będą wyglądały tak, jak zaprezentowano
na listingu 3.27. Można się również zastanowić nad dopisaniem metod analogicznych do
ustawXY, czyli metod ustawXZ oraz ustawYZ, to jednak będzie dobrym ćwiczeniem do sa-
modzielnego wykonania (podpunkt „Ćwiczenia do samodzielnego wykonania”).

Listing 3.27.
class Punkt3D extends Punkt {
int z;
void ustawZ(int wspZ) {
z = wspZ;
}
int pobierzZ() {
return z;
}
void ustawXYZ(int wspX, int wspY, int wspZ) {
x = wspX;
y = wspY;
z = wspZ;
}
}

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 konstrukto-
ró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.

b4ac40784ca6e05cededff5eda5a930f
b
126 Java. Praktyczny kurs

Co się stanie, jeśli dopiszemy je do kodu z listingu 3.25 i użyjemy razem z progra-
mem z listingu 3.26? Odpowiedni konstruktor z klasy Punkt zostanie wtedy wykonany
(dzieje się tak dlatego, że obiekt klasy Punkt3D, można powiedzieć, zawiera w sobie
obiekt klasy nadrzędnej Punkt). Niestety, co nie powinno być zaskoczeniem, żaden
z konstruktorów klasy Punkt nie zajmuje się polem z, którego w klasie Punkt po prostu
nie ma. Jeśli więc po opisanej zmianie skompilujemy i uruchomimy program testowy
z listingu 3.26, przekonamy się, że tak jest w istocie. Widać to na rysunku 3.15. Pola x i y
zostały zainicjowane wartościami 1, natomiast w polu z pozostała wartość domyślna 0.

Rysunek 3.15.
Efekt działania
konstruktorów
odziedziczonych
po klasie Punkt

Konstruktory dla klasy Punkt3D trzeba więc napisać samodzielnie. Nie jest to skompliko-
wane zadanie, konstruktory te są zaprezentowane na listingu 3.28.

Listing 3.28.
class Punkt3D extends Punkt {
int z;
Punkt3D() {
x = 1;
y = 1;
z = 1;
}
Punkt3D(int wspX, int wspY, int wspZ) {
x = wspX;
y = wspY;
z = wspZ;
}
Punkt3D(Punkt3D punkt) {
x = punkt.x;
y = punkt.y;
z = punkt.z;
}
/*
…pozostałe metody klasy Punkt3D…
*/
}

Jak widać, pierwszy konstruktor nie przyjmuje żadnych argumentów i przypisuje wszyst-
kim polom wartość 1. Drugi konstruktor przyjmuje trzy argumenty: wspX, wspY oraz wspZ,
wszystkie typu int, i przypisuje otrzymane wartości polom x, y i z. Trzeci konstruktor
otrzymuje jako argument obiekt klasy Punkt3D i kopiuje z niego wartości pól. Oczywiście
pozostałe metody klasy Punkt3D pozostają bez zmian, nie zostały one uwzględnione
na listingu, aby nie zaciemniać obrazu oraz nie marnować miejsca na stronie (są na-
tomiast uwzględnione w listingach dostępnych na serwerze FTP).

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 127

Przyglądając się dokładnie napisanym przed chwilą konstruktorom, nie da się nie za-
uważyć, że ich kod w znacznej części dubluje się z kodem konstruktorów klasy Punkt.
Dokładniej, są to te same instrukcje, uzupełnione dodatkowo o instrukcje operujące na
wartościach pola z. Przecież konstruktory:
Punkt3D(int wspX, int wspY, int wspZ) {
x = wspX;
y = wspY;
z = wspZ;
}

oraz:
Punkt(int wspX, int wspY) {
x = wspX;
y = wspY;
}

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ć konstruk-
tor klasy Punkt w klasie Punkt3D lub ogólniej — konstruktor klasy bazowej w konstrukto-
rze 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 kon-
struktorze 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 zobra-
zowania takiej konstrukcji doskonale nadaje się klasa Punkt3D. Spójrzmy na listing 3.29.

b4ac40784ca6e05cededff5eda5a930f
b
128 Java. Praktyczny kurs

Listing 3.29.
class Punkt3D extends Punkt {
int z;
Punkt3D() {
super();
z = 1;
}
Punkt3D(int wspX, int wspY, int wspZ) {
super(wspX, wspY);
z = wspZ;
}
Punkt3D(Punkt3D punkt) {
super(punkt);
z = punkt.z;
}
/*
…pozostałe metody klasy Punkt3D…
*/
}

W pierwszym konstruktorze wywołujemy bezargumentową metodę super, która powo-


duje wywołanie bezargumentowego konstruktora klasy bazowej. Taki konstruktor (bez-
argumentowy) 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 ar-
gument typu (klasy) Punkt3D, w klasie Punkt istnieje konstruktor przyjmujący jeden ar-
gument 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 sytu-
acji nastąpi tak zwane rzutowanie typu obiektu, które zostanie omówione dokładniej
dopiero w rozdziale 5. W tej chwili warto zapamiętać zasadę: w miejscu, gdzie powi-
nien być zastosowany obiekt pewnej klasy X, można zastosować również obiekt klasy
potomnej dla X10.

10
Istnieją jednak sytuacje, kiedy nie będzie to możliwe. Zajmiemy się dokładniej tym tematem w dalszej
części książki.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 129

Ć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 kolo-
rze. Kolor ma być określany dodatkowym polem o nazwie kolor i typie int (lub in-
nym). Dopisz metody ustawKolor i pobierzKolor, a także odpowiednie konstruktory.

Ćwiczenie 16.6.
Dopisz do klasy Punkt3D z listingu 3.29 konstruktor, który jako argument będzie przyj-
mował obiekt klasy Punkt. Wykorzystaj w tym konstruktorze metodę super.

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 ro-
dzajach, 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 pa-
kietowy, co oznacza, że dostęp do tej składowej mają wszystkie klasy wchodzące w skład
danego pakietu (pakiety zostaną omówione w dalszej części lekcji). Dostęp publiczny
jest określany słowem public, prywatny — słowem private, a chroniony — słowem
protected.

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() {

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 131

return x;
}
public int pobierzY() {
return y;
}
public void ustawX(int wspX) {
x = wspX;
}
public void ustawY(int wspY) {
y = wspY;
}
}

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 odczyty-
wać 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 kom-
pilacji będzie jedynie seria komunikatów o nieprawidłowych odwołaniach widoczna
na rysunku 3.17.

b4ac40784ca6e05cededff5eda5a930f
b
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 prywat-
nej składowej klasy: jest to taka składowa, która jest dostępna z wnętrza danej klasy,
czyli dostęp do niej mają wszystkie metody tej klasy. Wystarczy więc napisać publiczne
metody pobierające i ustawiające pola prywatne, a będzie można wykonywać na nich
operacje. W przypadku klasy Punkt z listingu 3.32 byłyby niezbędne metody ustawX,
ustawY oraz pobierzX i pobierzY. Taka klasa jest przedstawiona na listingu 3.34.

Listing 3.34.
class Punkt {
private int x;
private int y;
public int pobierzX() {
return x;
}
public int pobierzY() {
return y;
}
public void ustawX(int wspX) {
x = wspX;
}
public void ustawY(int wspY) {
y = wspY;
}
}

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);

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 133

należy napisać:
System.out.println("punkt.x = " + punkt.pobierzX());

Analogiczne konstrukcje należy zastosować przy dostępie do pola y. W tym miejscu warto
też zwrócić uwagę, że składowe prywatne nie będą dostępne nawet dla klas potom-
nych. Jeżeli z klasy Punkt z listingu 3.32 wyprowadzimy klasę Punkt3D w postaci:
class Punkt3D extends Punkt {
private int z;
}

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 je-
dynie 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ęp-
nia 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.

b4ac40784ca6e05cededff5eda5a930f
b
134 Java. Praktyczny kurs

znajduje się we wnętrzu, jest ukryte, dzięki temu można całkowicie zmienić wewnętrzną
konstrukcję klasy, nie zmieniając zupełnie sposobu korzystania z niej. Mówimy wtedy
o hermetyzacji lub enkapsulacji (ang. encapsulation).

To, że takie podejście jest przydatne, można pokazać nawet na przykładzie tak prostej
klasy, jaką jest nieśmiertelna klasa Punkt. Załóżmy, że ma ona postać widoczną na li-
stingu 3.34. Pola x i y są ukryte, operacje na współrzędnych można wykonywać wyłącznie
dzięki publicznym metodom: pobierzX, pobierzY, ustawX, ustawY. Program przedsta-
wiony na listingu 3.37 będzie zatem działał poprawnie.

Listing 3.37.
class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
punkt.ustawX(100);
punkt.ustawY(200);
System.out.println("punkt.x = " + punkt.pobierzX());
System.out.println("punkt.y = " + punkt.pobierzY());
}
}

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 wyko-
rzystują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łkowi-
cie 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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 135

Niewątpliwie trzeba zamienić pola x i y typu int na pola reprezentujące kąt i odległość
(tzw. moduł). Kąt najlepiej reprezentować za pomocą jego funkcji trygonometrycznej
— wybierzmy np. sinus. Nowe pola nazwijmy sinusalfa oraz modul. Zatem podstawowa
wersja nowej klasy Punkt będzie miała postać:
class Punkt {
private double sinusalfa;
private double modul;
}

Dopisać należy teraz wszystkie cztery metody. Aby to zrobić, niezbędne będą wzory
przekształcające wartości współrzędnych kartezjańskich (tzn. współrzędne [x, y]) na układ
biegunowy (czyli kąt i moduł) oraz wzory odwrotne, czyli przekształcające współ-
rzędne biegunowe na kartezjańskie. Wyprowadzenie tych wzorów nie jest skompliko-
wane, wystarczy znajomość podstawowych funkcji trygonometrycznych oraz twierdzenia
Pitagorasa. Ponieważ ta książka to kurs programowania w Javie, a nie lekcja matematy-
ki, zostaną podane w gotowej postaci12. I tak (dla oznaczeń jak na rysunku 3.18, bez
używania polskich liter):
y  modul  sin()

x  modul  1  sin2 ()

oraz:

modul  x 2  y 2

y
sin() 
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 do-
konujemy konwersji na typ int. Odpowiada za to konstrukcja (int) y (dokładniejszy opis
tego typu konstrukcji zostanie przedstawiony w dalszej części książki). Analogicznie
napiszemy metodę pobierzX, choć będzie trzeba wykonać nieco więcej obliczeń.
Metoda ta przyjmie następującą postać:

12
W celu uniknięcia umieszczania w kodzie klasy dodatkowych instrukcji warunkowych zaciemniających
sedno zagadnienia przedstawiony kod i wzory są poprawne dla dodatnich współrzędnych x. Uzupełnienie
klasy Punkt w taki sposób, aby możliwe było korzystanie z dowolnych wartości x, można potraktować
jako ćwiczenie do samodzielnego wykonania.

b4ac40784ca6e05cededff5eda5a930f
b
136 Java. Praktyczny kurs

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 oblicza-
jąca pierwiastek kwadratowy z przekazanego jej argumentu (czyli np. wykonanie instrukcji
Math.sqrt(4) da w wyniku 2). W naszym przypadku ten argument to 1 – sinusalfa *
2
sinusalfa, czyli 1 – sinusalfa , zgodnie z podanym wzorem na współrzędną x. Wyko-
nujemy mnożenie zamiast potęgowania, gdyż jest ono po prostu szybsze i wygodniejsze.

Pozostały jeszcze do napisania metody ustawX i ustawY. Pierwsza z nich będzie mieć
następującą postać:
public void ustawX(int wspX) {
int x = wspX;
int y = pobierzY();

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, nato-
miast y uzyskamy, wywołując odpowiednio przygotowaną metodę pobierzY. Dalsza część
metody ustawX to wykonanie działań zgodnych z podanymi wzorami. Podobnie jak
w przypadku pobierzX, zamiast potęgowania wykonujemy zwykłe mnożenie x * x i y * y.
Metoda ustawY będzie miała prawie identyczną postać, z tą różnicą, że skoro będzie jej
przekazywana wartość współrzędnej y, to trzeba uzyskać jedynie wartość x, czyli począt-
kowe instrukcje będą wyglądały tak:
int x = pobierzX();
int y = wspY;

Kiedy złożymy wszystkie napisane do tej pory elementy w jedną całość, uzyskamy
klasę Punkt w postaci widocznej na listingu 3.38. Teraz po uruchomieniu programu
z listingu 3.37 będzie można się przekonać, że wynik jego działania z nową klasą
Punkt jest dokładnie taki sam jak wtedy, gdy korzystaliśmy z jej poprzedniej postaci.
Mimo całkowitej wymiany wnętrza klasy Punkt program działa tak, jakby nic się nie
zmieniło (rysunek 3.19).

Listing 3.38.
class Punkt {
private double sinusalfa;
private double modul;
public int pobierzY() {
double y = modul * sinusalfa;
return (int) y;
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 137

Rysunek 3.19.
Zmiana implementacji
klasy Punkt nie wpłynęła
na działanie programu
z listingu 3.37

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 bi-
blioteka, zestaw powiązanych ze sobą tematycznie klas. Wraz z JDK programista
otrzymuje naprawdę pokaźną liczbę takich pakietów zawierających w sumie kilka (to
nie pomyłka) tysięcy klas. Aby więc sprawnie programować w Javie, trzeba zapoznać
się przynajmniej w podstawowym zakresie z jednostką biblioteczną, jaką jest pakiet.

Tworzenie pakietów
Do tworzenia pakietów służy słowo kluczowe package, po którym następuje nazwa pa-
kietu zakończona znakiem średnika. Schematycznie wygląda to więc następująco:
package nazwa_pakietu;

Ten zapis musi być umieszczony na początku pliku, przed nim nie mogą znajdować
się żadne inne instrukcje. Przed package mogą występować jedynie komentarze. Jeśli
zatem przykładowa klasa Punkt miałaby znajdować się w pakiecie o nazwie grafika,
plik powinien mieć następującą strukturę:
package grafika;

class Punkt {
/*

b4ac40784ca6e05cededff5eda5a930f
b
138 Java. Praktyczny kurs

…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 prawdopo-
dobieństwem, nazwy unikalnej w skali globu. Jeśli zatem mamy przykładową domenę
marcinlis.com i chcemy stworzyć pakiet o nazwie grafika, jego pełna nazwa powinna
brzmieć com.marcinlis.grafika. Z kolei wszystkie klasy tego pakietu będą musiały
być umieszczone w strukturze katalogów odpowiadających tej nazwie (rysunek 3.20).

13
Rzecz jasna, o ile posiadamy zarejestrowaną domenę internetową.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 139

Rysunek 3.20.
Struktura katalogów
dla pakietu o nazwie
com.marcinlis.grafika

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ą śro-
dowiskową 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ą przecho-
wywały informacje o punktach, ale w przypadku jednej z nich będą to punkty na
płaszczyźnie, a drugiej — w przestrzeni trójwymiarowej. Pierwsza z nich zostanie
umieszczona w pakiecie o nazwie grafika2d, a druga — w pakiecie grafika3d. Obie
klasy są widoczne na listingu 3.39.

Listing 3.39.
package grafika2d;

public class Punkt {


public int x;
public int y;
}

package grafika3d;

public class Punkt {


public int x;
public int y;
public int z;
}

Oczywiście klasę Punkt z pakietu grafika2d umieszczamy w pliku o nazwie Punkt w ka-
talogu grafika2d, a klasę Punkt z pakietu grafika3d — także w pliku Punkt, ale w katalo-
gu o nazwie grafika3d. Ścieżka dostępu do obu katalogów musi być określona w zmiennej

b4ac40784ca6e05cededff5eda5a930f
b
140 Java. Praktyczny kurs

ś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 li-


stingu 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 pa-
kietu. 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 pakieto-
wy, czyli usuwając słowa public. Przy próbie kompilacji programu testowego pojawi się
jedynie seria komunikatów o błędach.

Trzeba jeszcze wyjaśnić jedną kwestię — otóż do tej pory trzymaliśmy się zasady, że
w jednym pliku o rozszerzeniu java można umieścić tylko jedną klasę. To oczywiście
nieprawda albo też nie cała prawda. Otóż ta zasada powinna brzmieć: w jednym pliku
java (czyli w jednej jednostce kompilacji) może znaleźć się tylko jedna klasa o dostępie
publicznym. Wynika z tego, że w takim pliku można umieścić dodatkowo klasy o dostę-
pie pakietowym. I rzeczywiście tak jest.

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ę pu-
blicznie, mają one jedynie współpracować z klasą główną. Umieszcza się je wtedy ja-
ko 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 przedsta-
wionych przykładów, trzeba zapisać cały listing w jednym pliku o nazwie Punkt.java
— oczywiście w podkatalogu grafika, czyli podkatalogu o nazwie zgodnej z nazwą
pakietu.

b4ac40784ca6e05cededff5eda5a930f
b
142 Java. Praktyczny kurs

Listing 3.41.
package grafika;

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 ry-
sunku 3.22. Kiedy jednak usunięte zostaną odwołania do klasy Pomocnicza, kompilacja
przebiegnie bez problemów.

Listing 3.42.
import grafika.*;

class Main {
public static void main (String args[]) {
Punkt punkt = new Punkt();
Pomocnicza pomocnicza = new Pomocnicza();
}
}

15
W celu uproszczenia zadania można podkatalog grafika umieścić bezpośrednio w katalogu, w którym
znajduje się plik Main.java. Nie trzeba wtedy modyfikować zmiennej CLASSPATH.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 143

Rysunek 3.22.
Próba odwołania się
do klasy o dostępie
pakietowym kończy się
błędami kompilacji

Warto zatem podkreślić jeszcze raz: publiczna klasa Punkt znajdująca się w pakiecie
grafika ma pełny dostęp do pakietowej klasy Pomocnicza (obie znajdują się wręcz w tym
samym pliku), jednak klasa Main (spoza pakietu grafika) nie może odwoływać się do kla-
sy Pomocnicza. W klasie Main wolno jedynie tworzyć obiekty publicznej klasy Punkt.
Oczywiście dostęp do pakietu jest możliwy dzięki poleceniu import grafika.*.

Można zadać sobie pytanie, jakiego rodzaju w takim razie były klasy Punkt i Main, których
używaliśmy, począwszy od lekcji 13. W pliku Punkt.java umieszczany był kod klasy
Punkt w postaci:
class Punkt {
/*
…treść klasy Punkt…
*/
}

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 pa-
kietowymi. Wskazówką jest to, że znajdowały się w jednym katalogu. Kompilator tworzył
więc tymczasowy pakiet domyślny, niemający nazwy, do którego zaliczał obie klasy.
Dlatego też możliwa była kompilacja i wykonanie prostych przykładowych programów.

Ć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.

b4ac40784ca6e05cededff5eda5a930f
b
144 Java. Praktyczny kurs

Ćwiczenie 17.2.
Napisz wersję klasy Punkt, w której pola x i y będą przechowywane w tablicy. Klasa
powinna współpracować z klasą Main z listingu 3.37. Zastanów się, jakie składowe klasy
będą niezbędne i jakich modyfikatorów dostępu należy użyć.

Ćwiczenie 17.3.
Utwórz pakiet o nazwie biblioteka, umieść w nim dwie przykładowe klasy: Nowela
i Powiesc. Napisz dodatkową publiczną klasę Main, która będzie się odwoływała do
obiektów tych klas. Pamiętaj o użyciu instrukcji import.

Ćwiczenie 17.4.
Przygotuj taką strukturę klas, aby w jednym programie można było używać trzech
wersji klasy Punkt: z listingu 3.34, z listingu 3.38 i z ćwiczenia 17.2 (wszystkie wersje
mają mieć nazwę Punkt; możesz zmienić typ klasy z pakietowej na publiczną lub od-
wrotnie). Napisz przykładowy program, w którym powstaną obiekty tych trzech róż-
nych klas.

Ćwiczenie 17.5.
Utwórz pakiet o nazwie mojpakiet. Umieść w nim klasę o dostępie publicznym i nazwie
MojaKlasa. W tej klasie zadeklaruj dwa pola typu int o nazwach liczba1 i liczba2.
Pierwsze z nich ma być polem publicznym, drugie polem pakietowym. Zastanów się,
czy będzie można w dowolnej klasie publicznej tworzyć obiekty klasy MojaKlasa, a jeśli
tak, do których pól tej klasy będzie można się swobodnie odwoływać.

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 dziedzi-
czenie 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):

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 145

Listing 3.43.
public class A {
public void f() {
System.out.println("Metoda f() z klasy A");
}
}

i z niej wyprowadzimy klasę pochodną o nazwie B, to w klasie B da się zdefiniować meto-


dę 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ż wypi-
sanie 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 te-
stowy widoczny na listingu 3.45 pozwala przekonać się, że działanie będzie takie sa-
mo 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

b4ac40784ca6e05cededff5eda5a930f
b
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? Al-
bo inaczej: jakiego zachowania metod należałoby się spodziewać w przypadku klas
przedstawionych na listingu 3.46 (klasy należy zapisać w jednym katalogu w dwóch
plikach: A.java i B.java)?

Listing 3.46.
public class A {
public void f() {
System.out.println("Metoda f z klasy A");
}
}

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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 147

nazw (dwukrotne zadeklarowanie metody f), jednak próba kompilacji wykaże, że kompi-
lator nie zgłasza żadnych błędów. Czemu konflikt nazw nie występuje? Otóż zasada
jest następująca: jeśli w klasie bazowej i pochodnej są metody o tej samej nazwie
i argumentach, metoda z klasy bazowej jest przesłaniana. Tę technikę nazywamy
przesłanianiem metod (przykrywaniem, nadpisywaniem16, ang. overriding).

W obiektach klasy bazowej będzie zatem obowiązywała metoda z klasy bazowej, a w obiek-
tach klasy pochodnej — metoda z klasy pochodnej. Sprawdźmy to na przykładzie klas
z listingu 3.46. Co pojawi się na ekranie po uruchomieniu klasy Main z listingu 3.47,
która korzysta z obiektów klas A i B? Oczywiście najpierw tekst Metoda f z klasy A,
a następnie Metoda f z klasy B. Skoro bowiem obiekt a jest typu A, to wywołanie a.f()
powoduje uruchomienie metody f z klasy A, czyli wyświetlenie tekstu Metoda f z klasy A.
Z kolei typem obiektu b jest B, zatem wywołanie b.f() powoduje uruchomienie me-
tody f z klasy B i wyświetlenie na ekranie tekstu Metoda f z klasy B (rysunek 3.24).

Listing 3.47.
public class Main {
public static void main (String args[]) {
A a = new A();
B b = new B();

a.f();
b.f();
}
}

Rysunek 3.24.
Efekt wywoływania
przesłoniętych metod

Może się w tym miejscu pojawić pytanie, czy jest w takim razie możliwe wywołanie
w klasie pochodnej przesłoniętej metody z klasy bazowej. Ten problem może się wy-
dawać zawiły. Spójrzmy na przykład klas z listingu 3.46 — chodzi o to, czy w obiek-
cie klasy B można wywołać metodę f pochodzącą z klasy A. Nie jest to zagadnienie
czysto teoretyczne, gdyż w praktyce programistycznej takie odwołania często uprasz-
czają kod i ułatwiają tworzenie spójnych hierarchii klas. Skoro tak, to takie odwołanie
oczywiście jest możliwe. Jak wiadomo z lekcji 16., jeśli trzeba było wywołać konstruktor
klasy bazowej, wystarczyło skorzystać ze słowa super. W tym przypadku jest dokładnie
tak samo. Odwołanie do przesłoniętej metody klasy bazowej uzyskujemy dzięki wy-
wołaniu w schematycznej postaci:
super.nazwa_metody(argumenty);

16
Akurat ten termin nie oddaje w pełni zasady działania omawianego mechanizmu.

b4ac40784ca6e05cededff5eda5a930f
b
148 Java. Praktyczny kurs

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();
}
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 149

Rysunek 3.25.
Ilustracja działania
techniki przesłaniania
metod w klasach
potomnych

Przeanalizujmy zasadę działania tego zestawu klas i zastanówmy się, czemu rezultat
jest właśnie taki. Klasa A ma postać znaną nam z wcześniejszych przykładów. Została
w niej zdefiniowana metoda f, której jedynym zadaniem jest wyświetlenie nazwy klasy na
ekranie. Klasa B jest klasą pochodną od A, czyli dziedziczy po A. Zdefiniowano w niej
metodę f, która przesłania metodę f z klasy A. Jednakże w tej metodzie jest wywoływana,
za pomocą składni super, metoda przesłonięta. Dopiero po tym wywołaniu na ekranie
zostaje wyświetlona nazwa klasy B.

W klasie B występują jeszcze dodatkowo dwie inne metody, g i h. Pierwsza z nich wywo-
łuje w standardowy sposób metodę f (czyli będzie to metoda f z klasy B), druga ko-
rzysta ze składni super w postaci: super.f(), zatem wywołuje metodę f zdefiniowaną
w klasie A.

Trzecia klasa — Main — jest klasą testową. Tworzony jest w niej jeden obiekt o nazwie a
(typu A) i jeden o nazwie b (typu B). Następnie jest wywoływana metoda f obiektu klasy A,
zatem na ekranie pojawia się napis Klasa A. W kolejnym kroku jest wywoływana me-
toda f obiektu b. Ponieważ najpierw wywołuje ona metodę f klasy A, a dopiero potem wy-
świetla na ekranie nazwę klasy B, wynikiem jej działania są dwa teksty: Klasa A i Klasa B.

Krok trzeci to wywołanie metody g obiektu b, której jedynym zadaniem jest wywołanie
metody f z klasy B. Zatem zachowanie będzie takie samo jak w kroku drugim: na ekranie
pojawią się napisy Klasa A i Klasa B. W kroku ostatnim — czwartym — jest wywo-
ływana metoda h obiektu b. Jest w niej uruchamiana, przy użyciu składni super, me-
toda f z klasy A, zatem na ekranie pojawia się tekst Klasa A. Ostatecznie otrzymywany
jest wynik widoczny na rysunku 3.25.

Przesłanianie pól
Pola klas bazowych są przesłaniane w sposób analogiczny do wykorzystywanego w przy-
padku metod. Jeśli więc w klasie pochodnej zdefiniowane zostanie pole o takiej samej
nazwie jak w klasie bazowej, bezpośrednio dostępne będzie tylko pole klasy pochodnej.
Taka sytuacja została zobrazowana na listingu 3.50.

b4ac40784ca6e05cededff5eda5a930f
b
150 Java. Praktyczny kurs

Listing 3.50.
public class A {
public int liczba;
}

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 dzie-
dziczy po A, ponownie zostało zadeklarowane pole o takiej samej nazwie i identycznym
typie. Trzeba sobie jednak dobrze uświadomić, że każdy obiekt klasy B będzie w takiej
sytuacji zawierał DWA pola o nazwie liczba — jedno pochodzące z klasy A, drugie z B.
Tym polom można z kolei przypisywać różne wartości.

Jeśli mamy obiekt takiej klasy B, to z dowolnej klasy zewnętrznej jesteśmy w stanie
dostać się jedynie do pola liczba zdefiniowanego w klasie B. Jednak już z wnętrza klasy B
za pomocą składni super możemy odwołać się również do drugiego pola liczba (po-
chodzącego z klasy A). Taka sytuacja została przedstawiona na listingu 3.51.

Listing 3.51.
public class A {
public int liczba;
}

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 ustawia-
nie, druga zwraca jego aktualną wartość. Dwie pozostałe metody, czyli ustawLiczbaA
i pobierzLiczbaA, operują na polu liczba odziedziczonym po klasie A. Aby odwołać się
do tego pola, wykorzystują składnię super, czyli odwołanie wygląda następująco:
super.liczba

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 151

Możemy teraz napisać klasę testową Main, która udowodni, że obiekt klasy B napraw-
dę zawiera dwa pola o takiej samej nazwie. Taka klasa Main jest zaprezentowana na
listingu 3.52.

Listing 3.52.
public class Main {
public static void main (String args[]) {
B b = new B();

b.ustawLiczbaA(0);
b.ustawLiczbaB(1);

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 pobierz-
LiczbaA 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 przedsta-
wiona na listingu 3.53. Podobnych konstrukcji należy jednak zdecydowanie unikać —
nie ma zwykle praktycznej potrzeby stosowania tego typu techniki, wprowadza ona
bowiem niepotrzebne zamieszanie w budowie klas.

Listing 3.53.
public class A {
protected int pole1;
}

public class B extends A {


protected char pole1;
}

b4ac40784ca6e05cededff5eda5a930f
b
152 Java. Praktyczny kurs

Składowe statyczne
Składowe statyczne to pola i metody klasy, które mogą istnieć, nawet jeśli nie istnieje
obiekt tej klasy. Takie metody lub pola są wspólne dla wszystkich obiektów danej
klasy. Składowe te są oznaczane słowem static. W dotychczasowych przykładach
wykorzystywana była jedna metoda tego typu — main, od której rozpoczyna się wykony-
wanie programu.

Metody statyczne
Metodę statyczną oznacza się słowem static, które zwyczajowo powinno znaleźć się
zaraz za specyfikatorem dostępu17, zatem schematycznie deklaracja metody statycznej
będzie wyglądała następująco:
specyfikator_dostępu static typ_zwracany nazwa_metody(argumenty) {
//treść metody
}

Przykładowa klasa z zadeklarowaną metodą statyczną może wyglądać tak jak na li-
stingu 3.54.

Listing 3.54.
public class A {
public static void f() {
System.out.println("Metoda f klasy A");
}
}

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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 153

Na listingu 3.55 jest przedstawiona przykładowa klasa Main, która korzysta z takiego
wywołania. Uruchomienie tego kodu pozwoli się przekonać, że w przypadku metody
statycznej rzeczywiście nie trzeba tworzyć obiektu.

Listing 3.55.
public class Main {
public static void main (String args[]) {
A.f();
}
}

Dlatego też metoda main, od której rozpoczyna się wykonywanie kodu klasy, jest
metodą statyczną, może bowiem zostać wykonana, mimo że w trakcie uruchamiania apli-
kacji nie powstały jeszcze żadne obiekty.

Należy jednak zdawać sobie sprawę, że metoda statyczna jest umieszczana w specjalnie
zarezerwowanym do tego celu obszarze pamięci i jeśli powstaną obiekty danej klasy,
będzie ona dla nich wspólna. To znaczy, że dla każdego obiektu klasy nie tworzy się
kopii metody statycznej. Najłatwiej będzie jednak pokazać to na przykładzie pól sta-
tycznych.

Pola statyczne
Do pól oznaczonych jako statyczne można się odwoływać, podobnie jak w przypadku
metod statycznych, nawet jeśli nie istnieje żaden obiekt danej klasy. Pola takie deklaruje-
my, umieszczając przed typem słowo static. Schematycznie deklaracja taka wygląda
następująco:
static typ_pola nazwa_pola;

lub:
specyfikator_dostępu static typ_pola nazwa_pola;

Jeśli zatem w naszej przykładowej klasie A ma się pojawić pole statyczne o nazwie
liczba typu int o dostępie publicznym, taka klasa będzie miała postać widoczną na
listingu 3.5618.

Listing 3.56.
public class A {
public static int liczba;
}

18
Podobnie jak w przypadku metod statycznych, słowo static może się znaleźć przed specyfikatorem
dostępu, czyli przykładowo: static public int liczba. Jednak dla ujednolicenia notacji będziemy
konsekwentnie stosować formę zaprezentowaną w powyższym akapicie, czyli public static int liczba.

b4ac40784ca6e05cededff5eda5a930f
b
154 Java. Praktyczny kurs

Do pól statycznych odwołujemy się w sposób klasyczny, tak jak do innych pól klasy
— poprzedzając je nazwą obiektu (oczywiście jeśli wcześniej stworzymy dany obiekt),
czyli stosując konstrukcję:
nazwa_obiektu.nazwa_pola

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 nieza-
leżnie od liczby obiektów danej klasy pole statyczne o danej nazwie będzie tylko jedno.
Przykład ilustrujący to zagadnienie jest widoczny na listingu 3.57 (korzystamy w nim
z klasy A z listingu 3.56).

Listing 3.57.
public class Main {
public static void main (String args[]) {
//krok 1
A.liczba = 100;

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. Stosu-
jemy odwołanie w postaci nazwa_klasy.nazwa_pola = wartość, czyli A.liczba = 100.
Można tu zauważyć, że nie powstał jeszcze żaden obiekt klasy A, tymczasem polu

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 155

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 tym-
czasem 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 odczyta-
nie wartości zarówno za pomocą konstrukcji obiekt1.liczba, jak i obiekt2.liczba da
w wyniku wartość 300. Nic dziwnego, skoro istnieje tylko jedno pole liczba, po pro-
stu odwołujemy się do niego w różny sposób. Ostatecznie otrzymamy wynik widoczny
na rysunku 3.27.

Rysunek 3.27.
Różne typy odwołań
do pola statycznego
z klasy A

Ć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.

b4ac40784ca6e05cededff5eda5a930f
b
156 Java. Praktyczny kurs

Ćwiczenie 18.2.
Napisz klasę Opakowanie zawierającą pole typu int o nazwie liczba oraz klasę Nowe
Opakowanie dziedziczącą po Opakowanie i również zawierającą pole liczba. W klasie
NoweOpakowanie zdefiniuj metodę pobierzWartosc przyjmującą jeden argument typu
boolean. Jeśli wartością argumentu będzie true, metoda ta ma zwracać wartość pola
liczba zdefiniowanego w klasie NoweOpakowanie, a jeśli tą wartością będzie false, ma
ona zwrócić wartość pola liczba odziedziczonego po klasie Opakowanie.

Ćwiczenie 18.3.
Do klasy NoweOpakowanie z ćwiczenia 18.2 dopisz metodę ustawWartosc przyjmującą
dwa argumenty: pierwszy typu int, drugi typu boolean. Jeśli wartością drugiego argu-
mentu będzie true, wartość pierwszego przypisz polu liczba zdefiniowanemu w klasie
NoweOpakowanie, jeśli natomiast wartością drugiego argumentu będzie false, wartość
pierwszego przypisz polu liczba zdefiniowanemu w klasie Opakowanie.

Ćwiczenie 18.4.
Napisz klasę Dodawanie zawierającą metodę statyczną dodaj przyjmującą dwa argumenty
typu int. Metoda ta powinna zwrócić wartość będącą wynikiem dodawania obu ar-
gumentów.

Ćwiczenie 18.5.
Napisz klasę Przechowalnia zawierającą metodę statyczną o nazwie przechowaj przyjmu-
jącą jeden argument typu int. Klasa ta ma zapamiętywać argument przekazany meto-
dzie przechowaj w taki sposób, że każde wywołanie tej metody spowoduje zwrócenie
poprzednio zapisanej wartości i zapamiętanie aktualnie przekazanej.

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 zmie-
niać. 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 pro-
gramiście tworzyć klasy, które nie będą miały klas pochodnych — ich postać będzie
po prostu z góry ustalona. Jeśli klasa ma stać się finalna, należy przed jej nazwą
umieścić słowo kluczowe final zgodnie ze schematem:
specyfikator_dostępu final class nazwa_klasy {
//pola i metody klasy
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 157

Tak więc przykładowa klasa finalna Ostateczna mogłaby wyglądać jak na listingu 3.58.

Listing 3.58.
public final class Ostateczna {
public int liczba;
public void wyswietl() {
System.out.println(liczba);
}
}

Podobnie jak w przypadku słowa kluczowego static (lekcja 18.), nie ma formalnego
znaczenia to, czy napiszemy public final class, czy final public class, niemniej dla
przejrzystości i ujednolicenia notacji będziemy konsekwentnie stosować pierwszy przed-
stawiony sposób zapisu.

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 dzie-
dziczą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

b4ac40784ca6e05cededff5eda5a930f
b
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 miej-
scu umieszcza się specyfikator dostępu, słowo final występuje zawsze tuż przed ty-
pem 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, wyko-
rzystując wartości przypisane zadeklarowanym polom. Na początku zmiennej liczba2
przypisujemy wynik mnożenia 2 * liczba1. Jest to poprawna instrukcja, gdyż wolno
odczytywać wartość pola finalnego liczba1 oraz przypisywać wartości zwykłemu
polu liczba2. Identyczna sytuacja zachodzi w przypadku drugiego działania. Trzecia
instrukcja została ujęta w komentarz, gdyż jest nieprawidłowa i spowodowałaby błąd
kompilacji widoczny na rysunku 3.29. Występuje tu bowiem próba przypisania wyniku
działania liczba2 / 2 polu liczba1, które jest polem finalnym. Takiej operacji nie wolno
wykonywać, zatem po usunięciu znaków komentarza z tej instrukcji kompilator zdecydo-
wanie zaprotestuje. Do klasy Obliczenia została też dopisana metoda main (lekcja 14.),
w której tworzymy nowy obiekt typu Obliczenia i wywołujemy jego metodę licz.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 3.  Programowanie obiektowe. Część I 159

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ło-
by 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();
}
}

b4ac40784ca6e05cededff5eda5a930f
b
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 ponie-
waż 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 de-
klarować również bez ich inicjalizacji. Wartością takiego pola staje się wartość użyta
w pierwszym przypisaniu i dopiero od chwili tego przypisania nie wolno jej zmieniać.
Pole takie musi jednak zostać zainicjalizowane w konstruktorze danej klasy. Należy tylko
pamiętać, że po pierwszym przypisaniu wartości polu finalnemu nie wolno mu już
przypisywać innych wartości. Kilka przykładowych, poprawnych oraz niepoprawnych
inicjalizacji zostało pokazanych na listingu 3.62.

Listing 3.62.
public class Main {
public final int liczba1 = 100;
public final int liczba2;
public final Punkt punkt1 = new Punkt();
public final Punkt punkt2;

public Main() {
//prawidłowo, można modyfikować pola obiektu punkt1
punkt1.x = 100;
punkt1.y = 200;

//nieprawidłowo, nie ma jeszcze obiektu punkt2


punkt2.x = 200;
punkt2.y = 300;

//nieprawidłowo, nie można zmieniać referencji finalnej


punkt1 = new Punkt();

b4ac40784ca6e05cededff5eda5a930f
b
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 przypad-
ku dziedziczenia. Dokładniej, zgodnie z tym, co zostało napisane powyżej, metod fi-
nalnych nie można przesłaniać w klasach potomnych. Zilustrowano to w przykładzie
widocznym na listingu 3.63.

Listing 3.63.
public class Bazowa {
public final void metoda() {
//kod metody
}
}

public class Pochodna extends Bazowa {


public void metoda() {
//kod metody
}
}

b4ac40784ca6e05cededff5eda5a930f
b
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 ko-
munikatu 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ów-
nież argumenty metod. Argument finalny to taki, którego nie wolno zmieniać wewnątrz
metody. Aby uzyskać argument finalny, należy umieścić słowo final przed jego typem.
Schematycznie konstrukcja taka będzie wyglądała następująco:
specyfikator_dostępu [static][final] typ_zwracany nazwa_metody(final typ_argumentu
nazwa_argumentu)

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 ilu-
strującymi zachowanie argumentów finalnych.

Listing 3.64.
public class ArgumentyFinalne {
public void metoda1(final int liczba) {
//prawidłowo, można odczytywać wartość argumentu
int x = liczba;

//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;

b4ac40784ca6e05cededff5eda5a930f
b
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?

b4ac40784ca6e05cededff5eda5a930f
b
164 Java. Praktyczny kurs

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4.
Wyjątki
Praktycznie w każdym większym programie powstają jakieś błędy. Powodów jest bardzo
wiele — może to być skutek niefrasobliwości programisty, założenia, że wprowadzone
dane są zawsze poprawne, niedokładnej specyfikacji poszczególnych modułów aplikacji,
użycia niesprawdzonych bibliotek czy nawet zwykłego zapomnienia o zainicjowaniu
tylko jednej zmiennej. Na szczęście w Javie, tak jak w większości współczesnych języków
programowania, istnieje mechanizm tzw. wyjątków, który pozwala na przechwytywanie
błędów. Ta właśnie tematyka zostanie przedstawiona w trzech kolejnych lekcjach.

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 na-
zwie 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 na-
stępowało odwołanie do nieistniejącego elementu tablicy (listing 2.39). Było to spowo-
dowane 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 zadekla-


rowana została tablica 10-elementowa, zatem — ponieważ indeksowanie tablicy za-
czyna 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 de-
klaracją tablicy a nieprawidłowym odwołaniem są umieszczone inne instrukcje.

b4ac40784ca6e05cededff5eda5a930f
b
166 Java. Praktyczny kurs

Dużo więcej kłopotów mogłaby sprawić sytuacja, w której np. tablica byłaby deklaro-
wana w jednej klasie, a odwołanie do niej następowałoby w innej. Taka sytuacja została
przedstawiona na listingu 4.1.

Listing 4.1.
public class Tablica {
private int[] tablica = new int[10];
public int pobierzElement(int indeks) {
return tablica[indeks];
}
public void ustawElement(int indeks, int wartosc) {
tablica[indeks] = wartosc;
}
}

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 pry-
watne pole typu tablicowego o nazwie tablica, któremu została przypisana 10-elemen-
towa tablica liczb całkowitych. Ponieważ to pole jest prywatne (lekcja 17.), dostęp do
niego mają jedynie inne składowe klasy Tablica. Dlatego też powstały dwie metody, po-
bierzElement oraz ustawElement, operujące na elementach tablicy. Metoda pobierzElement
zwraca wartość zapisaną w komórce o indeksie przekazanym jako argument, natomiast
ustawElement zapisuje wartość drugiego argumentu w komórce o indeksie wskazywanym
przez argument pierwszy.

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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4.  Wyjątki 167

Jak poradzić sobie z takim problemem? Pierwszym nasuwającym się sposobem jest
sprawdzenie w metodach pobierzElement i ustawElement, czy przekazane argumenty nie
przekraczają dopuszczalnych wartości. Jeśli takie przekroczenie nastąpi, należy zasygnali-
zować błąd. To jednak prowokuje pytanie: w jaki sposób ten błąd sygnalizować? Jednym
z pomysłów jest zwracanie przez funkcję (metodę) wartości –1 w przypadku błędu
oraz wartości nieujemnej (najczęściej 0), jeśli błąd nie wystąpił. To rozwiązanie będzie
dobre w przypadku metody ustawElement, która będzie wtedy wyglądała następująco:
public int ustawElement(int indeks, int wartosc) {
if(indeks >= tablica.length || indeks < 0){
return –1;
}
else{
tablica[indeks] = wartosc;
return 0;
}
}

Wystarczyłoby teraz w klasie Main testować wartość zwróconą przez ustawElement, aby
sprawdzić, czy nie został przekroczony dopuszczalny indeks tablicy. Niestety tej techniki
nie można zastosować w przypadku metody pobierzElement — przecież zwraca ona
wartość zapisaną w jednej z komórek tablicy. Czyli –1 i 0 użyte przed chwilą do zasygna-
lizowania, czy operacja zakończyła się błędem, mogą być wartościami odczytanymi
z tablicy. Trzeba zatem wymyślić inną metodę. Może to być np. wykorzystanie dodatko-
wego pola sygnalizującego w klasie Tablica. Pole to byłoby typu boolean. Ustawione
na true oznaczałoby, że ostatnia operacja zakończyła się błędem, natomiast ustawione
na false — że zakończyła się sukcesem. Klasa Tablica miałaby wtedy postać jak na
listingu 4.2.

Listing 4.2.
public class Tablica {
private int[] tablica = new int[10];
public boolean wystapilBlad = false;
public int pobierzElement(int indeks) {
if(indeks >= tablica.length){
wystapilBlad = true;
return 0;
}
else{
wystapilBlad = false;
return tablica[indeks];
}
}
public void ustawElement(int indeks, int wartosc) {
if(indeks >= tablica.length){
wystapilBlad = true;
}
else{
tablica[indeks] = wartosc;
wystapilBlad = false;
}
}
}

b4ac40784ca6e05cededff5eda5a930f
b
168 Java. Praktyczny kurs

Do klasy zostało dodane pole typu boolean o nazwie wystapilBlad. Jego początkowa
wartość to false. W metodzie pobierzElement sprawdzamy najpierw, czy przekazany in-
deks nie przekracza maksymalnej dopuszczalnej wartości1. Jeśli tak, ustawiamy pole
wystapilBlad na true oraz zwracamy wartość 0. Oczywiście w tym przypadku zwróco-
na wartość nie ma żadnego praktycznego znaczenia (przecież wystąpił błąd), niemniej
coś musimy zwrócić. Użycie instrukcji return i zwrócenie wartości typu int jest ko-
nieczne, inaczej kompilator zgłosi błąd. Jeśli jednak argument przekazany metodzie
nie przekracza dopuszczalnego indeksu tablicy, pole wystapilBlad ustawiamy na false
oraz zwracamy wartość znajdującą się pod wskazanym indeksem.

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 mo-
dyfikacji 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... (ry-
sunek 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ę: progra-
miś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 warun-
kó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.

b4ac40784ca6e05cededff5eda5a930f
b
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 lek-
cji 11. Był to wyjątek spowodowany przekroczeniem dopuszczalnego zakresu tablicy.
Zobaczmy go raz jeszcze: na listingu 4.4 została zaprezentowana odpowiednio spre-
parowana klasa Main.

Listing 4.4.
public class Main {
public static void main (String args[]) {
int tab[] = new int[10];
tab[10] = 100;
}
}

Deklarujemy tablicę liczb typu int o nazwie tab oraz próbujemy przypisać elementowi
o indeksie 10 (czyli wykraczającym poza zakres tablicy) wartość 100. Jeśli skompilu-
jemy i uruchomimy taki program, na ekranie pojawi się obraz podobny do tego z rysun-
ku 4.1 (tym razem w komunikacie wskazany będzie tylko jeden plik źródłowy — Ma-
in.java). Został tu wygenerowany wyjątek o nazwie ArrayIndexOutOfBoundsException,
oznaczający, że indeks tablicy znajduje się poza dopuszczalnymi granicami.

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 ob-
sł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).

b4ac40784ca6e05cededff5eda5a930f
b
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 wyge-
nerowanie wyjątku. W nawiasie okrągłym występującym po instrukcji catch został wy-
mieniony rodzaj wyjątku, który będzie wygenerowany: ArrayIndexOutOfBoundsException,
oraz jego identyfikator: e. Identyfikator to nazwa3, która pozwala na wykonywanie
operacji związanych z wyjątkiem, tym jednak zajmiemy się w kolejnej lekcji. W bloku
po catch znajduje się instrukcja System.out.println wyświetlająca odpowiedni ko-
munikat na ekranie. Tym razem po uruchomieniu kodu pojawi się widok podobny do
zaprezentowanego na rysunku 4.2.

Blok try…catch nie musi jednak obejmować tylko jednej instrukcji ani też tylko instrukcji
mogących wygenerować wyjątek. Powróćmy na chwilę do listingu 2.39. Blok try
mógłby w tym wypadku objąć wszystkie trzy instrukcje, a klasa Main miałaby wtedy po-
stać jak na listingu 4.6.

Listing 4.6.
class Main {
public static void main (String args[]) {
try{
int tab[] = new int[10];
tab[10] = 5;
System.out.println("Dziesiąty element tablicy ma wartość: " + tab[10]);
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Nieprawidłowy indeks tablicy!");

3
Dokładniej, jest to nazwa zmiennej obiektowej (referencyjnej), co zostanie bliżej wyjaśnione w lekcji 21.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4.  Wyjątki 171

}
}
}

Nie trzeba również obejmować blokiem try instrukcji bezpośrednio generujących wyją-
tek, tak jak miało to miejsce w dotychczas przedstawionych przykładach. Wyjątek
wygenerowany przez obiekt klasy Y może być bowiem przechwytywany w klasie X,
która korzysta z obiektów klasy Y. Sprawdźmy to na przykładzie klas z listingu 4.1.
Klasa Tablica pozostanie bez zmian, natomiast klasę Main zmodyfikujemy tak, aby
miała wygląd zaprezentowany na listingu 4.7.

Listing 4.7.
public class Main {
public static void main (String args[]) {
Tablica tablica = new Tablica();
try{
tablica.ustawElement(5, 10);
int liczba = tablica.pobierzElement(10);
System.out.println(liczba);
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Nieprawidłowy indeks tablicy!");
}
}
}

Spójrzmy: w bloku try zostają wykonane trzy instrukcje, z których jedna, int liczba =
tablica.pobierzElement(10), jest odpowiedzialna za wygenerowanie wyjątku. Ale wyją-
tek jest przecież generowany we wnętrzu metody pobierzElement klasy Tablica, a nie
w klasie Main! Zostanie on jednak przekazany klasie Main, jako że wywołuje ona me-
todę z klasy Tablica. Tym samym w klasie Main można zastosować blok try…catch.

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();
}

b4ac40784ca6e05cededff5eda5a930f
b
172 Java. Praktyczny kurs

catch(ArrayIndexOutOfBoundsException e){
System.out.println("Wyjątek: metoda g");
}
}
public void h() {
int[] tab = new int[0];
try{
tab[0] = 1;
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Wyjątek: metoda h");
}
}
public static void main(String args[]) {
Wyjatki ex = new Wyjatki();
try{
ex.f();
}
catch(ArrayIndexOutOfBoundsException e){
System.out.println("Wyjątek: metoda main");
}
}
}

Taką klasę da się skompilować bez żadnych problemów. Trzeba jednak zdawać sobie
sprawę z tego, jak taki kod będzie wykonywany. Pytanie brzmi: które bloki try zosta-
ną wykonane? Zasada jest następująca: zostanie wykonany blok znajdujący się najbli-
żej instrukcji powodującej wystąpienie wyjątku. Tak więc w przypadku przedstawionym
na listingu 4.8 będzie to jedynie blok obejmujący wywołanie metody tab[0] = 1;
w metodzie h. Jeśli jednak będziemy usuwać kolejne bloki try najpierw z metody h,
następnie g, f i ostatecznie z main, zobaczymy, że w rzeczywistości wykonywany jest
zawsze blok najbliższy miejsca wystąpienia błędu. Po usunięciu wszystkich instrukcji
try wyjątek nie zostanie obsłużony w klasie Wyjatki i obsłuży go maszyna wirtualna
Javy, co zaowocuje znanym nam już komunikatem na konsoli. Co jednak warte uwagi,
w tym wypadku zostanie wyświetlona cała hierarchia metod, w których propagowany
był wyjątek (rysunek 4.3).

Rysunek 4.3. Przy hierarchicznym wywołaniu metod po wystąpieniu wyjątku otrzymamy ich nazwy

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4.  Wyjątki 173

Ć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 dopusz-
czalnej wartości.

Ćwiczenie 20.2.
Zmień kod klasy Main z listingu 4.3 w taki sposób, by sprawdzane było również to,
czy wywołanie metody ustawElement zakończyło się sukcesem.

Ćwiczenie 20.3.
Zmień kod z listingu 4.2 tak, aby do wychwytywania błędów był wykorzystywany
mechanizm wyjątków, a nie instrukcja warunkowa if (przy czym zewnętrzna sygnalizacja
wystąpienia błędu nadal ma się odbywać poprzez pole wystapilBlad).

Ćwiczenie 20.4.
Napisz klasę Wyjatki, w której będzie się znajdowała metoda o nazwie a, wywoływana
z kolei przez metodę o nazwie b. W metodzie a wygeneruj wyjątek ArrayIndexOutOf
BoundsException. Napisz następnie klasę Main zawierającą metodę main, w której zosta-
nie utworzony obiekt klasy Wyjatki i zostaną wywołane metody a oraz b tego obiektu.
W metodzie main zastosuj bloki try…catch przechwytujące powstałe wyjątki.

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).

b4ac40784ca6e05cededff5eda5a930f
b
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 zdefinio-
wanych 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 po-
wstają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 (referen-


cyjna) 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ła-
dzie 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;

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4.  Wyjątki 175

}
catch(ArithmeticException e){
System.out.println("Wystąpił wyjątek arytmetyczny...");
System.out.println("Komunikat systemowy:");
System.out.println(e.getMessage());
}
}
}

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.

b4ac40784ca6e05cededff5eda5a930f
b
176 Java. Praktyczny kurs

Rysunek 4.6.
Pełniejszy komunikat
o typie wyjątku

Hierarchia wyjątków
Każdy wyjątek jest obiektem pewnej klasy. Klasy podlegają z kolei regułom dziedzicze-
nia, zgodnie z którymi powstaje ich hierarchia. Kiedy zatem pracuje się z wyjątkami,
trzeba tę kwestię wziąć pod uwagę. Wszystkie standardowe wyjątki, które można
przechwytywać w naszych aplikacjach za pomocą bloku try…catch, dziedziczą po
klasie Exception, dziedziczącej z kolei po Throwable oraz Object. Hierarchia klas dla
wyjątku ArithmeticException, który był wykorzystywany we wcześniejszych przy-
kładach, jest zaprezentowana na rysunku 4.74.

Rysunek 4.7.
Hierarchia klas
dla wyjątku
ArithmeticException

Wynika z tego kilka własności. Przede wszystkim, jeśli spodziewamy się, że dana
instrukcja może wygenerować wyjątek typu X, możemy zawsze przechwycić wyjątek
ogólniejszy, czyli taki, którego typem będzie jedna z klas nadrzędnych do X. Jest to
bardzo wygodna technika. Przykładowo po klasie RuntimeException dziedziczy bar-
dzo wiele klas wyjątków odpowiadających najróżniejszym błędom. Jedną z nich jest
ArithmeticException. Jeśli instrukcje, które są obejmowane blokiem try…catch, mogą
spowodować wiele różnych wyjątków, zamiast stosować wiele oddzielnych instrukcji
przechwytujących konkretne typy błędów, często lepiej jest użyć jednej przechwytu-
jącej wyjątek bardziej ogólny. Taka sytuacja została przedstawiona na listingu 4.10.

4
Kierunek strzałek na rysunku jest niezgodny z metodyką modelowania UML, za to bardziej zgodny
z intuicją.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4.  Wyjątki 177

Listing 4.10.
public class Main {
public static void main (String args[]) {
try{
int liczba = 10 / 0;
}
catch(RuntimeException e){
System.out.println("Wystąpił wyjątek czasu wykonania...");
System.out.println("Komunikat systemowy:");
System.out.println(e);
}
}
}

Jest to znany nam już program, który generuje błąd polegający na próbie wykonania
niedozwolonego dzielenia przez zero. Tym razem jednak zamiast wyjątku klasy
ArithmeticException przechwytywany jest wyjątek klasy nadrzędnej — Runtime
Exception. Co więcej, nic nie stoi na przeszkodzie, aby przechwycić wyjątek jeszcze
bardziej ogólny, czyli klasy nadrzędnej do RuntimeException. Jak widać na rysunku 4.7,
byłaby to klasa Exception.

Przechwytywanie wielu wyjątków


W jednym bloku try…catch można przechwytywać wiele wyjątków. Konstrukcja taka za-
wiera 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 so-
bie błąd bardziej szczegółowy. Jeśli zatem przechwyci się najpierw wyjątek Runtime-
Exception, to tak jakby przechwycić już wyjątki wszystkich klas dziedziczących po
RuntimeException. Dlatego kompilator protestuje.

Kiedy może się przydać rozwiązanie, w którym najpierw przechwytuje się wyjątek
szczegółowy, a dopiero potem ogólny? Otóż wtedy, kiedy chcemy w specyficzny sposób
zareagować na konkretny typ wyjątku, a wszystkie pozostałe wyjątki z danego poziomu
hierarchii obsłużyć w identyczny, standardowy sposób. Taki przykład jest przedstawiony
na listingu 4.12.

Listing 4.12.
public class Main {
public static void main (String args[]) {
Punkt punkt = null;
int liczba;
try{
liczba = 10 / 0;
punkt.x = liczba;
}
catch(ArithmeticException e){
System.out.println("Nieprawidłowa operacja arytmetyczna");

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4.  Wyjątki 179

System.out.println(e);
}
catch(Exception e){
System.out.println("Błąd ogólny");
System.out.println(e);
}
}
}

Zostały zadeklarowane dwie zmienne: pierwsza typu Punkt o nazwie punkt oraz druga
typu int o nazwie liczba. Zmiennej punkt została przypisana wartość pusta null, nie
został zatem utworzony żaden obiekt klasy Punkt. W bloku try są wykonywane dwie
błędne instrukcje. Pierwsza z nich to znane z poprzednich przykładów dzielenie przez
zero. Druga instrukcja z bloku try to z kolei próba odwołania się do pola x nieistniejącego
obiektu klasy Punkt (przecież zmienna punkt zawiera wartość null). Ponieważ chcemy
w sposób niestandardowy zareagować na błąd arytmetyczny, najpierw przechwytujemy
błąd typu ArithmeticException i w przypadku kiedy wystąpi, wyświetlamy na ekranie
napis Nieprawidłowa operacja arytmetyczna. W drugim bloku catch przechwytujemy
wszystkie inne możliwe wyjątki, w tym także NullPointerException występujący podczas
próby wykonania operacji na zmiennej obiektowej, która zawiera wartość null.

Po uruchomieniu kodu z listingu 4.12 na ekranie pojawi się zgłoszenie tylko pierw-
szego błędu. Dzieje się tak dlatego, że po jego wystąpieniu blok try został przerwa-
ny, a sterowanie zostało przekazane blokowi catch. Jeśli więc w bloku try któraś z in-
strukcji spowoduje wygenerowanie wyjątku, dalsze instrukcje z tego bloku nie zostaną
wykonane. Nie będzie więc miała szansy zostać wykonana nieprawidłowa instrukcja
punkt.x = liczba;. Jeśli jednak usuniemy wcześniejsze dzielenie przez zero, przeko-
namy się, że i ten błąd zostanie przechwycony przez drugi blok catch, a na ekranie pojawi
się stosowny komunikat (rysunek 4.9).

Rysunek 4.9.
Odwołanie do pustej
zmiennej obiektowej
zostało wychwycone
przez drugi blok catch

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
}

b4ac40784ca6e05cededff5eda5a930f
b
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. Schema-
tycznie 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śred-
niego zagnieżdżenia więcej niż dwóch poziomów. Aby na praktycznym przykładzie
przyjrzeć się takiej dwupoziomowej konstrukcji, zmodyfikujmy kod z listingu 4.12.
Zamiast obejmować jednym blokiem try dwie instrukcje powodujące błąd, zastosu-
jemy zagnieżdżenie, tak jak jest to przedstawione na listingu 4.14.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4.  Wyjątki 181

Listing 4.14.
public class Main {
public static void main (String args[]) {
Punkt punkt = null;
int liczba;
try{
try{
liczba = 10 / 0;
}
catch(ArithmeticException e){
System.out.println("Nieprawidłowa operacja arytmetyczna");
System.out.println("Przypisuję zmiennej liczba wartość 10");
liczba = 10;
}

punkt.x = liczba;
}
catch(Exception e){
System.out.println("Błąd ogólny");
System.out.println(e);
}
}
}

Podobnie jak w poprzednim przypadku, deklarujemy dwie zmienne: punkt typu Punkt
oraz liczba typu int. Zmiennej punkt przypisujemy wartość pustą null. W wewnętrznym
bloku try próbujemy wykonać nieprawidłowe dzielenie przez zero i przechwytujemy
wyjątek ArithmeticException. Jeśli on wystąpi, zmienna liczba otrzymuje domyślną
wartość równą 10, dzięki czemu można wykonać kolejną operację, czyli próbę przypi-
sania polu x obiektu wskazywanego przez punkt wartości zmiennej liczba. Rzecz jasna
przypisanie takie nie może zostać wykonane, gdyż zmienna punkt jest pusta, jest za-
tem generowany wyjątek NullPointerException, który zostaje przechwycony przez
zewnętrzny blok try. Widać więc, że zagnieżdżanie bloków try może być przydatne,
choć warto zauważyć, że identyczny efekt można osiągnąć, korzystając z niezagnież-
dżonej postaci instrukcji try…catch (ćwiczenie 21.3).

Ćwiczenia do samodzielnego wykonania

Ćwiczenie 21.1.
Popraw kod z listingu 4.11 tak, aby przechwytywanie wyjątków odbywało się w pra-
widłowej kolejności.

Ćwiczenie 21.2.
Zmodyfikuj kod z listingu 4.12 tak, by zgłoszone zostały oba typy błędów: Arithmetic
Exception oraz NullPointerException.

b4ac40784ca6e05cededff5eda5a930f
b
182 Java. Praktyczny kurs

Ćwiczenie 21.3.
Zmodyfikuj kod z listingu 4.14 w taki sposób, aby usunąć zagnieżdżenie bloków
try…catch, ale by nie zmieniać efektów działania programu.

Ćwiczenie 21.4.
Napisz kod klasy TablicaPunktow przechowującej do czterech obiektów typu Punkt oraz
testowy program ilustrujący różne sytuacje powstające przy korzystaniu z obiektu tej
klasy (zadbaj o przechwytywanie wyjątków). Klasa ma zawierać jedynie dwie meto-
dy: pierwszą — przyjmującą trzy argumenty typu int i ustawiającą współrzędne x i y
punktu przechowywanego pod zadanym indeksem tablicy, oraz drugą — zwracającą
referencję do punktu znajdującego się pod wskazanym indeksem. Program ma odpo-
wiednio reagować, gdy następuje próba użycia nieistniejącego indeksu tablicy, a także
wtedy, gdy następuje próba zmiany współrzędnych nieistniejącego punktu (punkt powi-
nien zostać wówczas utworzony).

Ćwiczenie 21.5.
Jeśli w rozwiązaniu ćwiczenia 21.4 użyłeś instrukcji if, rozwiąż je, nie stosując tej in-
strukcji. W przeciwnym przypadku rozwiąż zadanie, korzystając z instrukcji if. Do-
datkowo do klasy TablicaPunktow dopisz metodę usuwającą punkt znajdujący się pod
wskazanym indeksem tablicy.

Lekcja 22. Własne wyjątki


Wyjątki można przechwytywać, aby zapobiec niekontrolowanemu zakończeniu działa-
nia programu w przypadku wystąpienia błędu. Ta technika została omówiona w lek-
cjach 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, wy-
starczy napisać:
throw new Exception();

5
Stosuje się również termin „wyrzucenie wyjątku”, od ang. throw — rzucać, wyrzucać.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4.  Wyjątki 183

Jeśli taki wyjątek zostanie obsłużony przez znajdującą się w danym bloku (danej me-
todzie) instrukcję try…catch, nie trzeba robić nic więcej. Jeśli jednak nie zostanie obsłużo-
ny (i nie jest wyjątkiem klasy RuntimeException lub pochodnej od niej), w specyfikacji
metody należy zaznaczyć, że może ona taki wyjątek zgłaszać (rzucać, wyrzucać). Ro-
bi się to za pomocą instrukcji throws, w ogólnej postaci:
specyfikator_dostępu [static] [final] typ_zwracany nazwa_metody(argumenty)
throws KlasaWyjątku1, KlasaWyjątku2, ... , KlasaWyjątkuN {
//treść metody
}

Zobaczmy, jak to wygląda w praktyce. Załóżmy, że mamy klasę Main, a w niej metodę
main. Jedynym zadaniem tej metody będzie zgłoszenie wyjątku typu Exception. Taka
klasa jest widoczna na listingu 4.15.

Listing 4.15.
public class Main {
public static void main (String args[]) throws Exception {
throw new Exception();
}
}

W deklaracji metody main poprzez użycie słów throws Exception zostało zaznaczone, że
może ona generować wyjątek klasy Exception. W ciele metody main została natomiast
wykorzystana instrukcja throw, która jako argument otrzymała nowy obiekt klasy
Exception. Po uruchomieniu opisanego programu na ekranie pojawi się widok zaprezen-
towany na rysunku 4.10. Jest to najlepszy dowód, że rzeczywiście udało się zgłosić
wyjątek.

Rysunek 4.10.
Zgłoszenie wyjątku
klasy Exception

Utworzenie obiektu wyjątku nie musi mieć miejsca bezpośrednio w instrukcji throw,
można go utworzyć wcześniej, przypisać zmiennej obiektowej (referencyjnej) i dopiero
tej zmiennej użyć jako parametru dla throw. Zamiast więc pisać:
throw new Exception();

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.

b4ac40784ca6e05cededff5eda5a930f
b
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. Przy-


kł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 wy-
jątek klasy ArithmeticException. W konstruktorze klasy przekazujemy komunikat in-
formujący o dzieleniu przez zero. Podajemy w nim wartości argumentów metody po-
dziel, tak by łatwo można było stwierdzić, jakie parametry spowodowały błąd.

W celu przetestowania działania metody podziel w klasie Dzielenie pojawiła się rów-
nież metoda main. Tworzymy w niej nowy obiekt klasy Dzielenie i odniesienie do nie-
go przypisujemy zmiennej o nazwie obj. Następnie dwukrotnie wywołujemy metodę
podziel, raz przekazując jej argumenty równe 20 i 10, drugi raz równe 20 i 0. Można
się spodziewać, że w drugim przypadku program zgłosi wyjątek ArithmeticException
ze zdefiniowanym przez nas komunikatem. Program zachowa się właśnie tak, co jest
widoczne na rysunku 4.11.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4.  Wyjątki 185

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órym-
kolwiek 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 prze-


chwycony 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 zobra-
zowane 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 niepra-
widłowa instrukcja dzielenia przez zero. Zostaje ona w tym bloku przechwycona, a na
ekranie wyświetlany jest komunikat o pierwszym przechwyceniu wyjątku. Następnie
wyjątek jest ponownie zgłaszany. W bloku zewnętrznym następuje drugie przechwy-
cenie, wyświetlenie drugiego komunikatu oraz kolejne zgłoszenie wyjątku. Ponieważ
nie istnieje trzeci blok try…catch, ostatecznie wyjątek jest obsługiwany przez maszynę
wirtualną. Dlatego po uruchomieniu przykładu wynik będzie taki, jak zaprezentowano
na rysunku 4.13.

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ł

b4ac40784ca6e05cededff5eda5a930f
b
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;
}
}
}

b4ac40784ca6e05cededff5eda5a930f
b
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 (wy-


ją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();
}
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4.  Wyjątki 189

W metodzie main za pomocą instrukcji throws zaznaczamy, że metoda ta może zgła-


szać wyjątek klasy GeneralException, sam wyjątek zgłaszamy natomiast przez zasto-
sowanie instrukcji throw dokładnie w taki sam sposób jak we wcześniejszych przykła-
dach. 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 GeneralExcep-
tion.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

b4ac40784ca6e05cededff5eda5a930f
b
190 Java. Praktyczny kurs

wywołujemy metodę podziel, pierwszy raz z prawidłowymi danymi i drugi raz z tymi,
które spowodują wygenerowanie wyjątku. Efekt działania przedstawionego kodu jest
widoczny na rysunku 4.16.

Rysunek 4.16.
Zgłoszenie wyjątku
DivideByZeroException

Zwróćmy jednak uwagę, że pierwotnie (listing 4.16) przy zgłaszaniu wyjątku argu-
mentem konstruktora był komunikat tekstowy (czyli wartość typu String). Tym razem nie
można było zastosować tego typu konstrukcji, gdyż klasa DivideByZeroException nie
ma konstruktora przyjmującego ciąg znaków jako argument, ale jedynie bezargumentowy
konstruktor domyślny. Aby zatem możliwe było przekazywanie własnych komunika-
tów, należy dopisać do klasy DivideByZeroException odpowiedni konstruktor (przyj-
mujący argument typu String). Będzie on miał postać widoczną na listingu 4.23.

Listing 4.23.
public class DivideByZeroException extends ArithmeticException {
public DivideByZeroException(String str) {
super(str);
}
}

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, nie-
zależnie od tego, co będzie działo się w bloku try. Schemat takiej konstrukcji wygląda
następująco:
try{
//instrukcje mogące spowodować wyjątek
}
catch(TypWyjątku identyfikatorWyjątku){
//instrukcje sekcji catch
}
finally{
//instrukcje sekcji finally
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4.  Wyjątki 191

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 zobra-
zowane 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 prze-
kazanych jej argumentów. Tym razem metoda podziel pozostała bez zmian w stosun-
ku do wersji z listingu 4.22, czyli zgłasza błąd DivideByZeroException. Zmodyfiko-
wana została natomiast metoda main. Oba wywołania metody zostały ujęte w bloki
try…catch…finally. Pierwsze wywołanie nie powoduje powstania wyjątku, nie jest
więc wykonywany pierwszy blok catch, ale jest wykonywany pierwszy blok finally.
Tym samym na ekranie pojawia się napis Sekcja finally 1.

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.

b4ac40784ca6e05cededff5eda5a930f
b
192 Java. Praktyczny kurs

Rysunek 4.17.
Blok finally jest
wykonywany niezależnie
od tego, czy pojawi się
wyjątek, czy nie

Sekcji finally można użyć również w przypadku instrukcji, które nie powodują wy-
generowania wyjątku. Stosuje się wtedy instrukcję try…finally w postaci:
try{
//instrukcje
}
finally{
//instrukcje
}

Działanie jest takie samo jak w przypadku bloku try…catch…finally, to znaczy kod
z bloku finally zostanie wykonany zawsze, niezależnie od tego, jakie instrukcje znajdą
się w bloku try. Przykładowo: nawet jeśli w bloku try znajdzie się instrukcja return
lub zostanie wygenerowany wyjątek, blok finally i tak zostanie wykonany. Zostało
to zobrazowane w przykładzie widocznym na listingu 4.25.

Listing 4.25.
public class Wyjatki {
public int f1() {
try{
return 0;
}
finally{
System.out.println("Sekcja finally f1");
}
}
public void f2() {
try{
int liczba = 10 / 0;
}
finally{
System.out.println("Sekcja finally f2");
}
}
public static void main(String args[]) {
Wyjatki wyjatki = new Wyjatki ();
wyjatki.f1();
wyjatki.f2();
}
}

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-

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 4.  Wyjątki 193

rwanie wykonywania funkcji nie jest w stanie zapobiec wykonaniu instrukcji z bloku
finally. Konstrukcja metody f2 jest bardzo podobna. Różnica jest taka, że w bloku
try jest wykonywane dzielenie przez zero, czyli powstaje wyjątek. Wyjątek nie jest
jednak przechwytywany w tej metodzie, a zatem zostanie przekazany do metody main,
a następnie obsłużony przez maszynę wirtualną. Również w tym przypadku (mimo wyge-
nerowania wyjątku) zostanie wykonany blok finally. Zatem po skompilowaniu i uru-
chomieniu programu zobaczymy widok przedstawiony na rysunku 4.18.

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 do-
wolną instrukcję generującą wyjątek NullPointerException. W main wywołaj metodę f
i przechwyć wyjątek za pomocą bloku try…catch.

Ćwiczenie 22.2.
Zmodyfikuj kod z listingu 4.18 tak, aby generowany, przechwytywany i ponownie zgła-
szany był wyjątek ArrayIndexOutOfBoundsException.

Ćwiczenie 22.3.
Napisz klasę o takim układzie metod jak w przypadku klasy Wyjatki z listingu 4.19.
W najbardziej zagnieżdżonej metodzie h wygeneruj wyjątek ArithmeticException.
Przechwyć go w metodzie g i zgłoś wyjątek klasy nadrzędnej do ArithmeticException,
czyli RuntimeException. Wyjątek ten przechwyć w metodzie f i zgłoś wyjątek nadrzędny
do RuntimeException, czyli Exception. Ten ostatni wyjątek przechwyć w klasie main.

Ćwiczenie 22.4.
Napisz klasę wyjątku o nazwie NegativeValueException oraz klasę Main, która będzie
z niego korzystać. W klasie Main napisz metodę o nazwie f przyjmującą dwa argumenty
typu int. Metoda f powinna zwracać wartość będącą wynikiem odejmowania pierw-
szego argumentu od drugiego. Gdyby jednak wynik ten był ujemny, powinien zostać
zgłoszony wyjątek NegativeValueException. Dopisz metodę main, która przetestuje
działanie metody f.

b4ac40784ca6e05cededff5eda5a930f
b
194 Java. Praktyczny kurs

Ćwiczenie 22.5.
Na podstawie przykładu z listingu 4.1 napisz kod klasy Tablica, w której po wykryciu
nieprawidłowego indeksu (zarówno przy zapisie, jak i odczycie danych) będzie genero-
wany wyjątek nowej klasy o nazwie InvalidIndexException (lub podobnej). Przetestuj
działanie nowego mechanizmu klasy Tablica, zmieniając odpowiednio klasę Main.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.
Programowanie
obiektowe. Część II
W rozdziale 3. zostały omówione podstawy programowania obiektowego. Była w nim
mowa o podstawowych technikach i zasadach niezbędnych do sprawnego programowania
w języku Java. Materiał przedstawiony w tym rozdziale pozwoli na znaczne poszerzenie
zdobytej dotąd wiedzy. Treść tego rozdziału jest podzielona na trzy główne tematy:
polimorfizm, interfejsy oraz klasy wewnętrzne. Obejmują one zagadnienia bardziej
zaawansowane niż te, które zostały do tej pory przedstawione, niemniej nie można ich
pominąć. Każdy, kto myśli poważnie o programowaniu w Javie, powinien się z nimi
zapoznać. W szczególności dotyczy to kwestii polimorfizmu będącego jednym z filarów
programowania obiektowego, a także interfejsów, bez których nie da się sprawnie pra-
cować z klasami zawartymi w JDK, o czym będzie mowa później, w rozdziałach 6. i 8.

Polimorfizm
Lekcja 23. Konwersje typów i rzutowanie obiektów
Lekcja 23. jest poświęcona konwersji typów oraz rzutowaniu obiektów. Będzie się można
dzięki niej dowiedzieć, czy da się przypisać zmiennej typu int wartość double oraz
w jaki sposób są wykonywane domyślne zmiany typów i jak je kontrolować. Zostanie opi-
sana także technika pozwalająca na przypisanie do zmiennej referencyjnej o określonym
typie obiektu klasy nadrzędnej lub potomnej w stosunku do tego typu. Znajdzie się tu
również wyjaśnienie, jak to się dzieje, że jako argumentu instrukcji System.out.println
można użyć dowolnego typu obiektowego.

b4ac40784ca6e05cededff5eda5a930f
b
196 Java. Praktyczny kurs

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), zo-
stanie skonwertowany na typ int. W efekcie tej konwersji zostanie utracona część
ułamkowa i dlatego właśnie zmienna liczba otrzyma wartość 4. A zatem najważniej-
sza operacja to konwersja typów danych, a zaokrąglenie jest jedynie efektem ubocznym
tej konwersji.

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 war-
tością typu double, na typ int, należy zastosować instrukcję:
int liczba = (int) 9 / 2;

Można też dokonać jawnej konwersji zmiennej typu double na typ int, np.:
double liczba1 = 10.5;
int liczba2 = (int) liczba1;

Zwróćmy uwagę, że przy tego typu przypisaniu jawna konwersja jest niezbędna — jeśli
nie zostanie dokonana, kompilator nie dopuści do kompilacji programu. Aby to spraw-
dzić, wystarczy użyć programu przedstawionego na listingu 5.1. Widać na nim przy-
kładową klasę Main, w której wartość zmiennej typu double jest przypisywana zmiennej
typu int. Kompilator wygeneruje w takim przypadku błąd widoczny na rysunku 5.1.

Listing 5.1.
public class Main {
public static void main (String args[]) {
double liczba1 = 0;
int liczba2 = liczba1;
}
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 197

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ż wy-
stępuje, choć najczęściej jest po prostu niezauważalna. Jeśli spróbujemy przypisać
zmiennej typu double (reprezentującej szerszy zakres wartości) wartość całkowitą lub
wartość zmiennej typu int (reprezentującej węższy zakres wartości), konwersja również
zostanie wykonana, tyle że będzie w pełni automatyczna. Można więc spokojnie wykonać
instrukcje:
int liczba1 = 10;
double liczba2 = liczba1;

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 automa-
tyczne 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 {

b4ac40784ca6e05cededff5eda5a930f
b
198 Java. Praktyczny kurs

public int z;
}

Jeśli zadeklarujemy teraz zmienną typu Punkt, to będzie można przypisać jej nowy
obiekt klasy Punkt3D (dokładniej: odniesienie do nowego obiektu klasy Punkt3D). Po-
prawne zatem będą instrukcje:
Punkt punkt;
punkt = new Punkt3D();

Oczywiście odwołując się teraz w standardowy sposób do metod i pól obiektu wskazy-
wanego przez zmienną punkt, można uzyskać dostęp jedynie do metod zdefiniowa-
nych w klasie Punkt. W związku z tym niepoprawne będzie np. odwołanie:
punkt.z = 10;

Zostało to zobrazowane w kodzie z listingu 5.3. Próba jego kompilacji zakończy się
błędem widocznym na rysunku 5.2.

Listing 5.3.
public class Main {
public static void main (String args[]) {
Punkt punkt;
punkt = new Punkt3D();
punkt.z = 10;
}
}

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();

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 199

Przypomina to konwersje typów prostych, jednak znaczenie tego zapisu jest nieco inne.
Zostało tu wykonane rzutowanie wskazania do obiektu klasy Punkt3D na klasę Punkt.
Jest to informacja dla kompilatora: traktuj zmienną punkt wskazującą obiekt klasy
Punkt3D tak, jakby wskazywała obiekt klasy Punkt, a w uproszczeniu: traktuj obiekt klasy
Punkt3D tak, jak gdyby był on klasy Punkt. Obiekt Punkt3D nie zmienia się jednak ani
nie traci żadnych informacji, jest po prostu inaczej traktowany.

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 ty-
pu double po konwersji na typ int zmieniała się na 4. W takiej sytuacji część ułamkowa
zostanie bezpowrotnie utracona i nawet powtórna konwersja na typ double nie przywróci
poprzedniej wartości. Zobrazowano to w przykładzie widocznym na listingu 5.4.

Listing 5.4.
public class Main {
public static void main (String args[]) {
double x = 4.5;
int y = (int) x;
double z = (double) y;
System.out.println("x = " + x);
System.out.println("y = " + y);
System.out.println("z = " + z);
}
}

Deklarujemy zmienną x typu double i przypisujemy jej wartość 4,5. Następnie deklaruje-
my zmienną y typu int oraz przypisujemy jej wartość konwersji zmiennej x na typ int
(int y = (int) x;). W kroku trzecim deklarujemy zmienną z typu double, której
przypisujemy wynik konwersji zmiennej y na typ double. Na zakończenie wyświetlamy
wartości wszystkich trzech zmiennych na ekranie (rysunek 5.3). Widać wyraźnie, że
nie da się odzyskać informacji utraconej przez konwersję typu double na typ int.

Rysunek 5.3.
Utrata informacji
w wyniku konwersji
typów prostych

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.

b4ac40784ca6e05cededff5eda5a930f
b
200 Java. Praktyczny kurs

Listing 5.5.
public class Main {
public static void main (String args[]) {
Punkt3D punkt3D1 = new Punkt3D();

punkt3D1.x = 10;
punkt3D1.y = 20;
punkt3D1.z = 30;

System.out.println("punkt3D1");
System.out.println("x = " + punkt3D1.x);
System.out.println("y = " + punkt3D1.y);
System.out.println("z = " + punkt3D1.z);
System.out.println("");

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 utwo-
rzony został tylko JEDEN obiekt klasy Punkt3D. Żadna z operacji nie zmieniła typu
tego obiektu, nie odbyły się żadne konwersje, które mogłyby doprowadzić do utraty
części danych. Powstały natomiast trzy zmienne, przez które spoglądaliśmy na obiekt.
Zmienne punkt3D1 i punkt3D2 traktowały go jako obiekt klasy Punkt3D, a zmienna punkt
jako obiekt klasy Punkt. Schematycznie przedstawiono tę sytuację na rysunku 5.5.
Można zatem powiedzieć, że rzutowanie pozwala spojrzeć na dany obiekt z innej,
szerszej lub węższej, perspektywy.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 201

Rysunek 5.4.
Ilustracja rzutowania
typów obiektowych

Rysunek 5.5.
Schematyczne
przedstawienie
zmiennych
i obiektu
z listingu 5.5

Przykład z listingu 5.5 pokazał również, że rzutowanie obiektów jest możliwe w obie
strony, to znaczy obiekt klasy bazowej można rzutować na obiekt klasy potomnej, a obiekt
klasy potomnej na obiekt klasy bazowej. Przypadek drugi jest oczywisty, wiadomo
już, że obiekt klasy potomnej zawiera w sobie obiekt klasy bazowej. Sytuacja odwrotna
jest jednak bardziej skomplikowana. Jak np. potraktować poniższy fragment kodu?
Punkt3D punkt3D = (Punkt3D) new Punkt();
punkt3D.z = 10;

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:

b4ac40784ca6e05cededff5eda5a930f
b
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 prze-
ciążonych wersji metody println, żadna jednak nie przyjmuje argumentu typu
ArithmeticException. Nie istnieje też domyślna konwersja żadnego typu obiektowego
na typ String.

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.

b4ac40784ca6e05cededff5eda5a930f
b
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 napi-
su 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 ar-
gument obiekt klasy Object, a tym razem ta metoda przyjmuje obiekt klasy String.
Można byłoby więc tę instrukcję rozbić na dwie następujące linie:
String komunikat = mk.toString();
System.out.println(komunikat);

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.

b4ac40784ca6e05cededff5eda5a930f
b
204 Java. Praktyczny kurs

Ćwiczenie 23.2.
Napisz klasy Bazowa i Pochodna (dziedziczącą po Bazowa) oraz testową klasę Main. W klasie
Main utwórz obiekt klasy Pochodna i przypisz go zmiennej typu Bazowa o nazwie bazowa.
Następnie zadeklaruj dodatkową zmienną typu Pochodna i przypisz jej obiekt wskazywany
przez zmienną bazowa.

Ćwiczenie 23.3.
Zmodyfikuj kod z listingu 5.6 tak, aby metoda toString z klasy MojaKlasa wyświetlała
również komunikat zwracany przez klasę nadrzędną.

Ćwiczenie 23.4.
Zmodyfikuj kod klasy Punkt z listingu 3.1 z rozdziału 3. tak, by po użyciu obiektu tej
klasy jako argumentu metody println na ekranie były wyświetlane współrzędne punktu.
Dodatkowo spraw, aby ta klasa pakietowa stała się publiczna, a także by oba jej pola
były publiczne.

Ćwiczenie 23.5.
Napisz klasę Main, która przetestuje działanie metody toString klasy Punkt powstałej
w ćwiczeniu 23.4.

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 rzuto-
wania typów; pojawi się tu też pojęcie polimorfizmu, jedno z głównych pojęć w pro-
gramowaniu 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 frag-
mencie kodu:
Punkt3D punkt3D = (Punkt3D) new Punkt();
punkt3D.z = 10;

Taką konstrukcję można zapisać również przy użyciu dodatkowej zmiennej klasy Punkt:

b4ac40784ca6e05cededff5eda5a930f
b
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 rzutowa-


ny 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 przypi-
sania. Czy uda się w związku z tym skompilować kod widoczny na listingu 5.8?
Odpowiedź brzmi… tak.
Listing 5.8.
public class Main {
public static void main(String args[]) {
Punkt punkt = new Punkt();
Punkt3D punkt3D = (Punkt3D) punkt;
punkt3D.z = 10;
}
}

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 sprawdze-
nie 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.).

b4ac40784ca6e05cededff5eda5a930f
b
206 Java. Praktyczny kurs

Rysunek 5.9. Próba odwołania się do nieistniejącego w klasie Punkt pola o nazwie z

Spróbujmy teraz odwołać się do jednego z istniejących w klasie Punkt pól x lub y,
czyli wykonać instrukcje:
Punkt punkt = new Punkt();
Punkt3D punkt3D = (Punkt3D) punkt;
punkt3D.x = 10;

Czy tym razem program zadziała? Otóż spotka nas tu niespodzianka: reakcja będzie
identyczna z tą, jaką widzieliśmy w poprzednim przypadku, zostanie wygenerowany
wyjątek ClassCastException (rysunek 5.9). Stanie się tak, mimo że obiekt, na który wska-
zuje zmienna punkt3D, zawiera pole x. To jednak nie ma znaczenia, gdyż podstawowym
problemem jest to, że sam obiekt jest innej klasy niż Punkt3D. Maszyna wirtualna w trakcie
wykonania programu dokonuje sprawdzenia zgodności klas. Wykonywanie operacji na
obiekcie okazuje się możliwe tylko wtedy, kiedy zmienna wskazująca na ten obiekt
jest klasy tego obiektu lub klasy nadrzędnej do klasy tego obiektu — nigdy odwrotnie. Je-
żeli opisywana zgodność nie następuje, w trakcie wykonania zostanie wygenerowany
błąd ClassCastException, tak jak miało to miejsce w ostatnich przykładach. Jest on gene-
rowany już przy próbie wykonania instrukcji:
Punkt3D punkt3D = (Punkt3D) punkt;

nic więc dziwnego, że nie sposób wykonać żadnej instrukcji modyfikującej pola obiektu,
niezależnie od tego, czy są one w nim zawarte, czy też nie.

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ąza-
niem dynamicznym (ang. dynamic binding). W przypadku rzutowania w dół, oma-
wianego w poprzednim podpunkcie, polimorfizm uniemożliwia wykonanie niedozwolo-
nych 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 li-
stingu 5.9.

b4ac40784ca6e05cededff5eda5a930f
b
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 re-
ferencję do nowego obiektu klasy MojaDrugaKlasa, drugiej — do obiektu klasy Moja
TrzeciaKlasa. Można tego dokonać, jako że obie klasy dziedziczą po MojaKlasa. Jest tu
wykorzystywane rzutowanie domyślne. Formalnie można by ten kod zapisać również jako:
MojaKlasa mk1 = (MojaKlasa) new MojaDrugaKlasa ();
MojaKlasa mk2 = (MojaKlasa) new MojaTrzeciaKlasa ();

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ą po-
traktowane tak, jakby były obiektami tej klasy, i na ekranie dwukrotnie pojawi się napis
MojaKlasa.

b4ac40784ca6e05cededff5eda5a930f
b
208 Java. Praktyczny kurs

Nic bardziej mylnego! W Javie wywołania metod są domyślnie polimorficzne, to zna-


czy, że skojarzenie obiektu z metodą jest wykonywane w trakcie działania programu. Tym
samym sprawdzeniu podlega rzeczywisty typ obiektu, którego metoda jest wywoły-
wana. 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ąza-
nie, ang. early binding), takie wywołanie nie mogłoby mieć miejsca. W trakcie kompi-
lacji 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 rze-
czywisty 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ę jedy-
nie do wyświetlenia nazwy. Taki zestaw klas jest widoczny na listingu 5.11.
Listing 5.11.
public class Shape {
public void draw() {
System.out.println("Shape");
}
}

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");
}
}

b4ac40784ca6e05cededff5eda5a930f
b
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ą meto-
dę drawShape w klasie Main i będzie ona pasować do każdej kolejnej klasy wyprowadzonej
z Shape. Zostało to zobrazowane w programie widocznym na listingu 5.12.

Listing 5.12.
public class Main {
public void drawShape(Shape shape) {
shape.draw();
}
public static void main(String args[]) {
Main main = new Main();

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 two-
rzymy bowiem obiekty klas Circle, Triangle oraz Rectangle i przekazujemy je jako
argumenty metodzie drawShape klasy Main. Wynik działania, zgodny z naszymi oczeki-
waniami, jest widoczny na rysunku 5.11.

Warto się teraz zastanowić, co by się stało, gdyby w jednej z klas dziedziczących po
Shape nie było zdefiniowanej metody draw. Jaki będzie wynik działania programu z li-
stingu 5.12, jeśli np. klasa Triangle z listingu 5.11 przyjmie pokazaną poniżej postać?
public class Triangle extends Shape {
}

b4ac40784ca6e05cededff5eda5a930f
b
210 Java. Praktyczny kurs

Rysunek 5.11.
Polimorficzne
wywołania metody
drawShape

Efekt jest widoczny na rysunku 5.12. Zamiast napisu Triangle pojawił się napis Shape. To
nie powinno dziwić, bo metoda draw z klasy Triangle przesłaniała metodę draw zdefi-
niowaną w klasie Shape. Kiedy z definicji klasy Triangle usunięta została metoda draw,
doszło do odsłonięcia metody draw odziedziczonej po klasie nadrzędnej, czyli Shape.
Została zatem wykonana metoda draw obiektu klasy Triangle, tak jak w poprzednim
przypadku, tyle że była to metoda odziedziczona po klasie bazowej i stąd też na ekranie
pojawił się napis Shape.
Rysunek 5.12.
Z klasy Triangle została
usunięta metoda draw

Metody prywatne
Przy wykorzystywaniu polimorficznych wywołań metod należy pamiętać, że mają
one miejsce, kiedy metoda X z klasy bazowej jest przesłaniana przez metodę X z klasy
potomnej. Problem może wystąpić w sytuacji, kiedy w klasie bazowej zostanie zdefi-
niowana metoda prywatna. Taka metoda nie może być bowiem przesłonięta. Jest to
oczywiste, bo przecież metody prywatne (lekcja 17.) są dostępne wyłącznie dla klasy,
w której zostały zdefiniowane. Łatwo tu jednak wpaść w pewną pułapkę. Otóż to, że
dana metoda została zadeklarowana w klasie bazowej jako prywatna, nie oznacza wcale,
że nie można zdefiniować metody o takiej samej nazwie i argumentach w klasie po-
chodnej. Choć brzmi to może paradoksalnie, taka możliwość naprawdę istnieje, ale nie
zaleca się stosowania tego typu konstrukcji, gdyż zaciemnia to sposób działania kodu.
Trzeba natomiast wiedzieć, co mogłoby się dziać w tego typu sytuacji, gdyby się jed-
nak przytrafiła. Co będzie się działo w sytuacji przedstawionej na listingu 5.13?
Listing 5.13.
public class A {
private void f() {
System.out.println("Metoda f klasy A");
}
}

public class B extends A {


public void f() {
System.out.println("Metoda f klasy B");
}
}

b4ac40784ca6e05cededff5eda5a930f
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 skom-
pilować. W tym miejscu pojawi się zapewne pytanie: czemu jest to możliwe? Otóż praw-
dą 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 meto-
dzie 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ływa-


niem 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ż — for-
malnie 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 wi-
dać, została wykonana metoda f klasy A.
Rysunek 5.13.
Niespodziewane
zachowanie klasy
z metodą prywatną

b4ac40784ca6e05cededff5eda5a930f
b
212 Java. Praktyczny kurs

Co się stało? Otóż gdyby obie metody f były publiczne, czyli metoda f z klasy B przesła-
niałaby metodę f z klasy A, zachowanie programu okazałoby się zgodne z wcześniej-
szymi oczekiwaniami. Wywołanie polimorficzne spowodowałoby, że choć zmienna a
jest typu A, zostałaby wykonana metoda f klasy B, jako że sam obiekt jest typu B. Tym-
czasem ponieważ metoda f z klasy A jest prywatna, a tym samym finalna, maszyna
wirtualna nie ma potrzeby sprawdzania, czy istnieje jej wersja w klasie B (bo taka
wersja nie może istnieć), czyli zachowuje się tak, jakby metody f w klasie B nie było.

Aby uniknąć tego typu problemów, najlepiej stosować się do zasady, że w klasach po-
tomnych nie używa się metod o takich samych nazwach jak nazwy metod prywatnych
w klasie bazowej. Nie ma praktycznej potrzeby stosowania tego typu konstrukcji, a ich
unikanie pozwoli również ustrzec się niepotrzebnych problemów i niejasności w kon-
strukcji kodu programów.

Ćwiczenia do samodzielnego wykonania

Ćwiczenie 24.1.
Popraw kod z listingu 5.8 tak, aby po wystąpieniu wyjątku nie następowało niekon-
trolowane zakończenie działania programu. Zastosuj właściwy blok try…catch.

Ćwiczenie 24.2.
Zmodyfikuj kod z listingu 5.10 tak, by metoda toString nie była jawnie wywoływana.
Zachowanie programu ma pozostać bez zmian.

Ćwiczenie 24.3.
Zmodyfikuj kod klas z listingu 5.11 tak, aby wywołanie metody draw powodowało wy-
świetlenie odpowiedniej figury geometrycznej zbudowanej z dowolnych znaków semi-
graficznych.

Ćwiczenie 24.4.
Popraw kod klas z listingów 5.13 tak, by wykonanie kodu metody main z listingu 5.14
powodowało wywołanie metody f z klasy B.

Lekcja 25. Konstruktory oraz klasy abstrakcyjne


Lekcja 25. jest poświęcona klasom abstrakcyjnym oraz problematyce zachowań kon-
struktoró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 konstrukto-
rach innych metod danej klasy.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 213

Klasy abstrakcyjne
W poprzedniej lekcji był wykorzystywany zestaw klas opisujących figury geometryczne,
które dziedziczyły po wspólnej klasie bazowej Shape. Klasy te zostały przedstawione
na listingu 5.11. W tego typu przypadkach klasa bazowa często jest tylko atrapą, która
w rzeczywistości nie wykonuje żadnych zadań, służy jedynie do zdefiniowania zestawu
metod, jakimi będą posługiwały się klasy potomne, oraz udostępnienia udogodnień, jakie
niesie możliwość korzystania z wywołań polimorficznych (jak w przykładach z lekcji 24.).
W takich sytuacjach często nie ma potrzeby lub jest wręcz niewskazane, aby były tworzo-
ne obiekty klasy bazowej. W przypadku zwykłych klas nie można jednak nikomu za-
bronić tworzenia ich instancji3, taką możliwość dają natomiast klasy abstrakcyjne.

Klasa abstrakcyjna to taka klasa, która została zadeklarowana z użyciem słowa kluczowe-
go abstract. Przy czym klasa, w której przynajmniej jedna metoda jest abstrakcyjna
(oznaczona słowem kluczowym abstract), MUSI być zadeklarowana jako abstrakcyjna4.
Schemat takiej konstrukcji wygląda następująco:
[public] abstract class nazwa_klasy {
[specyfikator_dostępu] abstract typ_zwracany nazwa_metody(argumenty);
}

Metoda abstrakcyjna ma jedynie definicję, nie może zawierać żadnego kodu (nie wolno
użyć nawet pustego nawiasu klamrowego; deklarację należy zakończyć średnikiem).
Przykładowo metoda draw z klasy Shape z zestawu klas z listingu 5.11 mogłaby być z po-
wodzeniem metodą abstrakcyjną. Zgodnie z powyższym opisem klasa Shape w takim wy-
padku również musiałaby być abstrakcyjna. Cała konstrukcja miałaby następującą postać:
public abstract class Shape {
public abstract void draw();
}

Po takiej deklaracji nie będzie można tworzyć obiektów klasy Shape. Próba wykonania
przykładowej instrukcji:
Shape shape = new Shape();

skończy się komunikatem o błędzie: Shape is abstract; cannot be instantiated.

Co jednak ważniejsze, zadeklarowanie metody jako abstrakcyjnej wymusza jej redekla-


rację 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 de-
finicji metody draw).

3
Można by co prawda zastosować prywatny konstruktor, ale to nie oznacza, że w ogóle nie da się utworzyć
obiektu danej klasy.
4
Nie wyklucza to oczywiście istnienia klas abstrakcyjnych, w których żadna z metod nie jest abstrakcyjna.

b4ac40784ca6e05cededff5eda5a930f
b
214 Java. Praktyczny kurs

Rysunek 5.14. Klasa pochodna musi zawierać metody zadeklarowane jako abstrakcyjne w klasie bazowej

Można mieć zatem pewność, że jeśli klasa bazowa zawiera metodę abstrakcyjną, to
każda klasa potomna również ją zawiera. Da się więc bezpiecznie stosować wywołania
polimorficzne, takie jak omówione w poprzedniej lekcji.

W tym miejscu może paść pytanie, czemu klasa, która zawiera metodę abstrakcyjną, rów-
nież musi być zadeklarowana jako abstrakcyjna. Odpowiedź jest prosta: gdyby w zwykłej
klasie została zadeklarowana metoda abstrakcyjna, a następnie utworzony zostałby
obiekt tej klasy, nie byłoby przecież możliwe wywołanie tej metody, ponieważ nie miała-
by ona kodu wykonywalnego. Musiałoby się to skończyć błędem w trakcie wykonania
aplikacji.

Należy jednak pamiętać, że zgodnie z definicją podaną powyżej klasa musi być zade-
klarowana jako abstrakcyjna, jeżeli co najmniej jedna jej metoda jest abstrakcyjna. Wy-
nika z tego, że nie ma żadnych przeciwwskazań, aby pozostałe metody nie były abs-
trakcyjne. Taka sytuacja została zilustrowana na listingu 5.15.

Listing 5.15.
public abstract class Shape {
public abstract void draw();
public String toString() {
return "metoda toString z klasy Shape";
}
}

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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 215

oraz toString, natomiast Triangle jedynie definicję metody draw. Jak już wiadomo, taka
sytuacja jest możliwa, gdyż metoda toString klasy Shape nie jest abstrakcyjna, klasy
pochodne nie muszą zatem zawierać jej definicji. Aby się przekonać, jak działa taki ze-
staw klas i metod, napiszmy dodatkowo testową klasę Main. Została ona zaprezentowana
na listingu 5.16.

Listing 5.16.
public class Main {
public void drawShape(Shape shape) {
shape.draw();
}
public static void main(String args[]) {
Main main = new Main();

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. Po-
nieważ jednak w klasie Triangle nie była zdefiniowana przeciążona metoda toString,
została wywołana metoda toString odziedziczona po klasie Shape. Jest to zachowanie jak
najbardziej prawidłowe i zgodne z oczekiwaniami. Warto zatem zapamiętać, że w klasie
abstrakcyjnej mogą znaleźć się również metody, które nie są abstrakcyjne.

b4ac40784ca6e05cededff5eda5a930f
b
216 Java. Praktyczny kurs

Rysunek 5.15.
Wynik działania klas
przedstawionych na
listingach 5.15 i 5.16

Wywołania konstruktorów
Wywołania konstruktorów nie są polimorficzne, ważne jednak, aby wiedzieć, jaka jest
kolejność ich wywoływania, szczególnie w odniesieniu do hierarchii klas. Otóż w klasie
potomnej zawsze musi zostać wywołany konstruktor klasy bazowej. Powinno to być
oczywiste, jako że obiekt klasy potomnej zawiera w sobie obiekt klasy bazowej (ale
może nie mieć dostępu do niektórych jego składowych!). Skoro tak, zawsze najpierw
powinien zostać wykonany konstruktor klasy bazowej, a dopiero potem klasy potomnej.
Programista ma wtedy pewność, że obiekt klasy bazowej został prawidłowo zainicjo-
wany. Sytuacja taka jest zilustrowana na listingu 5.17.

Listing 5.17.
public class A {
public A() {
System.out.println("Konstruktor klasy A");
}
}

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 ekra-
nie napisu oznajmiającego, z jakiej klasy pochodzi. Klasa B, dziedzicząca po klasie A,
również zawiera jeden publiczny konstruktor, który wyświetla na ekranie informację,
że pochodzi z klasy B. Konstruktory te są domyślne, jako że nie przyjmują żadnych
argumentów. W klasie B została dodatkowo zdefiniowana metoda main, w której jest
tworzony jeden obiekt klasy B. Zatem zgodnie z tym, co zostało napisane powyżej, w ta-
kiej sytuacji najpierw zostanie wywołany konstruktor klasy bazowej A, a dopiero po nim
konstruktor klasy potomnej B, co jest widoczne na rysunku 5.16.

Obowiązuje zatem zasada, że jeśli w konstruktorze klasy potomnej nie będzie jawnie wy-
wołany żaden konstruktor klasy bazowej, automatycznie zostanie wywołany domyślny
konstruktor klasy bazowej (konstruktorem domyślnym jest konstruktor bezargumentowy).

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 217

Rysunek 5.16.
W klasie potomnej
zawsze jest wywoływany
konstruktor klasy
bazowej

Co by się jednak stało, gdyby w klasie A z listingu 5.17 nie umieścić żadnego kon-
struktora, czyli gdyby miała ona postać zaprezentowaną poniżej?
public class A {
}

Obie klasy udałoby się skompilować, a na ekranie pojawiłby się jedynie napis Kon-
struktor klasy B. I choć wydawać by się mogło, że w takim razie nie został wywołany
żaden konstruktor klasy A, w rzeczywistości tak nie jest. Konstruktor musi zostać wy-
wołany, nawet jeśli nie zostanie umieszczony jawnie w ciele klasy. W takiej sytuacji
Java dodaje własny pusty konstruktor domyślny, który rzecz jasna jest wywoływany.

Z zupełnie inną sytuacją mamy do czynienia, kiedy w klasie bazowej nie zostanie umiesz-
czony konstruktor domyślny (czyli taki, który nie przyjmuje żadnych argumentów),
ale znajdzie się w niej dowolny inny konstruktor. Sytuacja tego typu została zapre-
zentowana na listingu 5.18.

Listing 5.18.
public class A {
public A(int arg) {
System.out.println("Konstruktor klasy A");
}
}

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, zmody-
fikowana została natomiast treść klasy A. Nie ma już konstruktora domyślnego, bezar-
gumentowego, pojawił się natomiast konstruktor przyjmujący jeden argument typu
int. Jak już wiadomo, taka konstrukcja jest niepoprawna, gdyż konstruktor klasy B
będzie próbował wywołać domyślny konstruktor klasy A, którego po prostu nie ma (pusty
konstruktor domyślny zostałby dodany przez kompilator tylko wtedy, gdyby w klasie A
nie było żadnego innego konstruktora). Już przy próbie kompilacji zostanie zatem za-
sygnalizowany błąd, który został zaprezentowany na rysunku 5.17.

b4ac40784ca6e05cededff5eda5a930f
b
218 Java. Praktyczny kurs

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 przy-
padku 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();
}
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 219

Klasy A i B mają tu postać taką jak na listingu 5.17, z tą różnicą, że z B została usunięta
metoda main. W klasie C zostały zadeklarowane dwa pola o nazwach obiektA i obiektB.
Pierwsze z nich jest typu A, drugie typu B. Oba są inicjowane w konstruktorze klasy C,
wcześniej jednak konstruktor ten przedstawia się, czyli wyświetla na ekranie informację,
z jakiej klasy pochodzi. W metodzie main z kolei zostaje utworzony nowy obiekt klasy
C. Jaka zatem będzie kolejność wykonania konstruktorów w tym przykładzie?

Skoro tworzony jest obiekt klasy C, najpierw zostanie wywołany konstruktor tej klasy,
a tym samym w pierwszej kolejności pojawi się na ekranie napis Konstruktor klasy C.
W tym konstruktorze jest tworzony obiekt klasy A, zatem wywołany zostanie konstruk-
tor tej klasy i zostanie wyświetlony napis Konstruktor klasy A. W kolejnym kroku po-
wstaje obiekt klasy B. Klasa B dziedziczy jednak po A, więc w tym kroku zostaną wywoła-
ne dwa konstruktory, najpierw z klasy bazowej A, a następnie z potomnej B. W związku
z tym na ekranie pojawi się ciąg napisów widoczny na rysunku 5.18.

Rysunek 5.18.
Złożone wywołania
konstruktorów

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 przed-
stawionym 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);

b4ac40784ca6e05cededff5eda5a930f
b
220 Java. Praktyczny kurs

}
public static void main(String args[]) {
B b = new B(1);
b.f();
}
}

Na pierwszy rzut oka ten kod wygląda całkiem poprawnie. Klasa A zawiera publiczną
metodę f, która nie robi nic, oraz wywołujący ją konstruktor domyślny. W komentarze
zostały ujęte instrukcje System.out.println, które później pozwolą dokładniej prześledzić
sposób działania programu.

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 meto-
dzie 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 skom-
pilować 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 pro-
gramu 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ł przed-
stawiony 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ść wyko-
nywania konstruktorów.

b4ac40784ca6e05cededff5eda5a930f
b
222 Java. Praktyczny kurs

Ćwiczenie 25.4.
Zmodyfikuj kod z listingu 5.19 tak, by klasa C dziedziczyła po B. Jaka będzie kolejność
wykonania konstruktorów?

Interfejsy
Lekcja 26. Tworzenie interfejsów
W lekcji 25. zostały omówione klasy abstrakcyjne, czyli takie, w których znajdowały
się metody niemające implementacji. Implementacja tych metod musiała się odbywać
w klasach potomnych (o ile klasy potomne nie były również zadeklarowane jako abs-
trakcyjne). W tej lekcji zostaną przedstawione interfejsy, które można traktować (w wer-
sjach Javy do 7 włącznie) jako klasy czysto abstrakcyjne, niemające żadnej implementacji.

Czym są interfejsy?
W Javie do wersji 8 interfejs to klasa czysto abstrakcyjna, czyli taka, w której wszystkie
metody są traktowane jako abstrakcyjne. W wersji 8 wprowadzono jednak (ze wzglę-
du na inne zmiany dotyczące języka) możliwość definiowania tzw. metod domyśl-
nych (o czym pod koniec lekcji)5. Interfejs deklaruje się za pomocą słowa kluczowe-
go interface. Może on być publiczny, o ile jest zdefiniowany w pliku o takiej samej
nazwie jak nazwa interfejsu, lub pakietowy. W tym drugim przypadku jest dostępny
jedynie dla klas wchodzących w skład danego pakietu. Schematyczna konstrukcja in-
terfejsu wygląda więc następująco:
[public] interface nazwa_interfejsu {
typ_zwracany nazwa_metody1(argumenty);
typ_zwracany nazwa_metody2(argumenty);
/*…dalsze metody interfejsu…*/
typ_zwracany nazwa_metodyN(argumenty);
}

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).

b4ac40784ca6e05cededff5eda5a930f
b
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 li-


stingu 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 mu-
si być spełniony przez klasę). Wróćmy teraz do przykładu z figurami z lekcji 24. i 25.
(klasy Triangle, Rectangle, Circle). Wymuszaliśmy w tych klasach deklarację metody
draw. Odbywało się to poprzez umieszczenie w klasie nadrzędnej Shape abstrakcyjnej
metody draw. Można to jednak zrobić w inny sposób — wykorzystując interfejs Drawable.

6
Lub musi być klasą abstrakcyjną.

b4ac40784ca6e05cededff5eda5a930f
b
224 Java. Praktyczny kurs

Wystarczy, jeśli każda z klas będzie ten interfejs implementowała, tak jak jest to
przedstawione na listingu 5.23. W takiej sytuacji również występuje konieczność de-
klaracji metody draw w każdej z przedstawionych klas.

Listing 5.23.
public class Circle implements Drawable {
public void draw() {
System.out.println("metoda draw z klasy Circle");
}
}

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 {

b4ac40784ca6e05cededff5eda5a930f
b
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 implemen-
tacja metody draw, a w klasie bazowej nie. A więc nawet jeśli nie zapomnimy o napisa-
niu pustej metody draw w klasie Shape i kompilacja uda się bez problemu, nie będziemy
mogli skorzystać z udogodnień, jakie dają wywołania polimorficzne. Z kolei jeżeli nie
uwzględnimy metody draw w klasie Shape, kompilacja nie będzie możliwa. Łatwo się
o tym przekonać, redukując klasę Shape do postaci:
public class Shape {
}

oraz próbując skompilować klasę Main przedstawioną na listingu 5.25 (lub tę z listingu
5.12; obie mają prawie identyczną postać). Spowoduje to błąd widoczny na rysunku 5.22.
To nie powinno dziwić, przecież nie istnieje teraz w klasie Shape metoda draw, a pró-
bujemy ją wywołać w metodzie drawShape w klasie Main. Kompilator musi zatem za-
protestować.

Listing 5.25.
public class Main {
public void drawShape(Shape shape) {
shape.draw();
}
public static void main(String args[]) {
Main main = new Main();

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

b4ac40784ca6e05cededff5eda5a930f
b
226 Java. Praktyczny kurs

Jakie jest rozwiązanie tego problemu? Otóż metoda bazowa również powinna imple-
mentować interfejs Drawable, czyli klasa Shape powinna mieć postać:
public class Shape implements Drawable {
public void draw(){};
}

Warto rozważyć jeszcze jeden problem. Jeśli jest wiele klas potomnych i każda z nich
ma implementować dany interfejs, łatwo o pominięcie słowa implements w jednej z nich,
a tym samym spowodowanie powstania błędu. Jak temu zapobiec? Czy nie dałoby się
zmusić klas potomnych do zaimplementowania interfejsu z klasy bazowej? Okazuje
się, że można to zrobić, pod warunkiem jednak, że klasa bazowa będzie klasą abstrak-
cyjną. W takiej sytuacji klasa ta nie będzie musiała implementować metod zawartych
w interfejsie, ale będą je musiały implementować wszystkie nieabstrakcyjne klasy po-
chodne. Taka sytuacja została przedstawiona na listingu 5.26.

Listing 5.26.
public abstract class Shape implements Drawable {
}

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ą zaw-
sze publiczne, statyczne oraz finalne, czyli trzeba im przypisać wartości już w momen-
cie ich deklaracji. Można stosować typy zarówno proste, jak i złożone. Pola interfejsów
są najczęściej wykorzystywane do tworzenia wyliczeń. Deklaracja pola interfejsu nie
różni się od deklaracji pola klasy, schematycznie wygląda tak:
[public] interface nazwa_interfejsu {
typ_pola1 nazwa_pola1 = wartość_pola1;

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 227

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ę za-
pisu: ponieważ pola interfejsów są zawsze statyczne i finalne, przyjmuje się, że ich na-
zwy są pisane wielkimi literami, a poszczególne człony nazwy są oddzielane znakiem
podkreślenia.

Listing 5.27.
public interface NowyInterfejs {
int POLE_TYPU_INT = 100;
double POLE_TYPU_DOUBLE = 1.0;
char POLE_TYPU_CHAR = 'a';
}

Nic nie stoi również na przeszkodzie, aby interfejs zawierał pola typu obiektowego,
ważne jest tylko, żeby były one inicjowane również w momencie deklaracji, dokładnie
tak, jak ma to miejsce w przypadku pól typów prostych. Przykład takiego interfejsu
został zaprezentowany na listingu 5.28.

Listing 5.28.
public interface NowyInterfejs {
Rectangle POLE_TYPU_RECTANGLE = new Rectangle();
Triangle POLE_TYPU_TRIANGLE = new Triangle();
Circle POLE_TYPU_CICRLE = new Circle();
}

Metody domyślne
W Java 8 wprowadzono możliwość definiowania w interfejsach tzw. metod domyślnych
(ang. default methods). Taka metoda może mieć definicję (implementację) i ta defini-
cja zostanie odziedziczona przez wszystkie klasy implementujące dany interfejs. Do
jej utworzenia należy użyć słowa kluczowego default. Nie wyklucza to jednak istnienia
w interfejsie typowych metod abstrakcyjnych (które były omawiane do tej pory). Schemat
takiej konstrukcji jest następujący:
[public] interface nazwa_interfejsu {
typ_zwracany nazwa_metody1(argumenty);
typ_zwracany nazwa_metody2(argumenty);
/*…dalsze metody abstrakcyjne interfejsu…*/
typ_zwracany nazwa_metody_domyślnej1(argumenty){
/*treść metody domyślnej 1*/
}
typ_zwracany nazwa_metody_domyślnej2(argumenty){
/*treść metody domyślnej 2*/
}
/*…dalsze metody domyślne interfejsu…*/
}

b4ac40784ca6e05cededff5eda5a930f
b
228 Java. Praktyczny kurs

Załóżmy, że w trakcie prac nad interfejsem Drawable (z listingu 5.21) wystąpiła koniecz-
ność jego unowocześnienia i dodania metody draw3D. Jej zadaniem ma być np. wyświetla-
nie danego kształtu z symulacją trójwymiarowości. Jeżeli do interfejsu zostanie dodana
zwyczajna deklaracja metody (w sposób dostępny w Javie do wersji 7; będzie to więc
metoda abstrakcyjna), przestaną działać wszystkie dotychczas napisane klasy (nie będą
bowiem implementowały metody draw3D). W związku z tym trzeba będzie albo doko-
nać poprawek we wszystkich klasach implementujących interfejs, albo też utworzyć
zupełnie nowy interfejs (np. o nazwie Drawable3D). Jednak dzięki dostępnym w Java 8
i w nowszych wersjach metodom domyślnym da się wprowadzić poprawki do istniejące-
go interfejsu Drawable w taki sposób, aby „stare” klasy (Shape, Rectangle itp.) mogły
działać bez problemów. Kod tego interfejsu mógłby wyglądać tak jak na listingu 5.29.

Listing 5.29.
public interface Drawable {
public void draw();
default public void draw3D(){
System.out.println("Domyślny kształt 3D");
}
}

Teraz bez najmniejszych problemów da się skompilować i uruchomić program z listin-


gu 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 korzysta-
ją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[]) {

b4ac40784ca6e05cededff5eda5a930f
b
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 abs-
trakcyjna i implementuje interfejs Drawable. W klasie znajdują się dwie metody draw
oraz draw3D — ich zadaniem jest wyświetlenie komunikatu z nazwą danej metody.
Klasa Main z listingu 5.31 zawiera program pozwalający na przetestowanie różnych
właściwości najnowszej wersji interfejsu Drawable (z listingu 5.29). Analogicznie do wer-
sji z listingu 5.25 mamy w niej metodę drawShape wywołującą metodę draw obiektu
przekazanego jako argument oraz dodatkowo metodę drawShape3D, która wywołuje
metodę draw3D.

W metodzie main, od której zacznie się wykonywanie programu, tworzony jest obiekt
klasy Main oraz obiekty klas Triangle, Rectangle i Circle. Następnie dwukrotnie
wywoływana jest metoda drawShape3D obiektu wskazywanego przez zmienną main.
Przy pierwszym wywołaniu jako argument przekazywany jest obiekt klasy Triangle,
a przy drugim — obiekt klasy Rectangle. W ostatnim wierszu wywoływana jest natomiast
metoda drawShape i jest jej przekazywany obiekt klasy Circle. Oczywiście każde z tych
wywołań będzie polimorficzne (lekcja 24.).

Jaki zatem będzie efekt działania aplikacji i jakie napisy pojawią się po jej uruchomieniu?
Widać to na rysunku 5.23. Wszystkie klasy oznaczające figury dziedziczą po klasie ba-
zowej Shape i w związku z tym implementują najnowszą wersję interfejsu Drawable
(listing 5.29). Interfejs ten zawiera domyślną metodę draw3D, ale klasa Triangle w wersji
z listingu 5.30 ma własną wersję tej metody. Dlatego w pierwszym wywołaniu draw
Shape3D zostanie użyta metoda draw3D pochodząca z klasy Triangle.

Rysunek 5.23.
Efekt użycia metody
domyślnej interfejsu

W drugim wywołaniu drawShape3D jest już inaczej. Użyta została stara wersja klasy
Rectangle (z listingu 5.26), która nic „nie wie” o istnieniu metody draw3D. Dlatego też
zostanie zastosowana domyślna metoda pochodząca z interfejsu Drawable, a na ekranie
pojawi się napis „Jestem kształtem 3D”. Ostatnie wywołanie dotyczy obiektu klasy

b4ac40784ca6e05cededff5eda5a930f
b
230 Java. Praktyczny kurs

Circle oraz metody draw. Ono nie powinno budzić wątpliwości. Nowa wersja interfejsu
nie zmienia zachowania starych metod, dlatego efekt będzie taki jak w przykładzie z li-
stingu 5.25.

Ć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. Zagadnie-
nia te pojawiły się w lekcji 26. Kolejnym tematem, którym się zajmiemy, jest techni-
ka 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 in-
terfejsach, a także to, że interfejsy, tak jak i inne klasy, podlegają regułom dziedzicze-
nia. 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ść implemento-
wania 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 kon-
strukcji wygląda następująco:
[public][abstract] class nazwa_klasy implements interfejs1, interfejs2, ... ,
interfejsN {
/*

b4ac40784ca6e05cededff5eda5a930f
b
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
}

b4ac40784ca6e05cededff5eda5a930f
b
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 je-
dynie interfejs WydajeDzwiek, a klasa Radio zawiera wyłącznie metodę graj. Jak widać,
dzięki temu, że jedna klasa może implementować wiele interfejsów, mogą one być bar-
dziej uniwersalne. Gdyby taka możliwość nie istniała, dla telewizora musiałby po-
wstać interfejs np. o nazwie WyswietlaObrazIWydajeDzwiek, w którym zostałyby zade-
klarowane metody graj i wyswietl, co ograniczałoby jego zastosowania. Może to być
jednak ćwiczenie do samodzielnego wykonania.

Konflikty nazw
Kiedy jedna klasa implementuje wiele interfejsów, mogą powstać budzące wątpliwości
sytuacje konfliktu nazw, które niestety czasami prowadzą do powstawania błędów (na
szczęście zwykle wykrywanych w trakcie kompilacji). Załóżmy, że mamy dwa interfejsy
przedstawione na listingu 5.34.

Listing 5.34.
public interface PierwszyInterfejs {
public void f();
}

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 inter-
fejsy? Z kodu pierwszego interfejsu wynika, że klasa MojaKlasa powinna mieć zdefi-
niowaną metodę o nazwie f, z kodu drugiego wynika dokładnie to samo. Czy zatem
klasa ta ma zawierać dwie metody f? Oczywiście nie, nie byłoby to przecież możliwe.
Skoro jednak deklaracje metod f w obu interfejsach są takie same, oznacza to, że klasa
implementująca oba interfejsy musi zawierać JEDNĄ metodę f o deklaracji public
void f(). Zatem przykładowa klasa MojaKlasa będzie miała postać przedstawioną na
listingu 5.35.

Listing 5.35.
public class MojaKlasa implements PierwszyInterfejs, DrugiInterfejs {
public void f() {
System.out.println("f");
}
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 233

Nieco inna, ale również jednoznaczna sytuacja nastąpi, kiedy w dwóch interfejsach
będą metody o takiej samej nazwie, ale innych argumentach. Wtedy również będzie
można zaimplementować je w jednej klasie. Takie dwa przykładowe interfejsy zostały
przedstawione na listingu 5.36.

Listing 5.36.
public interface PierwszyInterfejs {
public void f(int argument);
}

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 nato-
miast argument typu int (tak jak zostało to zdefiniowane w interfejsie). Trzecia me-
toda 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 {

b4ac40784ca6e05cededff5eda5a930f
b
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 utwo-
rzona wcześniej przykładowa klasa MojaKlasa miała implementować oba interfejsy
przedstawione na listingu 5.38? Musiałaby mieć postać z listingu 5.39.

Listing 5.39.
public class MojaKlasa implements PierwszyInterfejs, DrugiInterfejs {
public void f() {
//System.out.println("f");
}
public int f() {
//System.out.println("f");
}
}

Taka klasa jest w sposób oczywisty nieprawidłowa. W jednej klasie nie mogą istnieć
dwie metody o takiej samej nazwie różniące się jedynie typem zwracanego wyniku.
Próba kompilacji takiego kodu spowoduje powstanie serii błędów widocznych na ry-
sunku 5.24.

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ć prze-
ciwwskazań do implementacji metody f przyjmującej argument typu int i niezwraca-
jącej wyniku.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 235

Listing 5.40.
public interface MojInterfejs {
public void f(int argument);
}

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 nie-
prawidłowy, ponieważ ze względu na implementowanie interfejsu MojInterfejs w klasie
musi istnieć metoda o deklaracji void f(int). Z kolei gdy taka metoda zostanie dodana
(a komentarz usunięty), kompilacja się nie uda, bo metoda o deklaracji void f(int) nie
może być użyta ze względu na odziedziczoną po klasie bazowej (MojaKlasa) metodę
o deklaracji int f(int).

Dziedziczenie interfejsów
Interfejsy można budować, stosując mechanizm dziedziczenia. Odbywa się to podobnie
jak w przypadku klas. Należy wykorzystać słowo extends. Interfejs potomny będzie za-
wierał wszystkie swoje metody oraz wszystkie metody z interfejsu bazowego. Schemat
zastosowania słowa extends w przypadku dziedziczenia interfejsów wygląda następująco:
[public] interface interfejs_potomny extends interfejs_bazowy {
//deklaracje pól i metod
}

Zatem z przykładowego interfejsu I1 zawierającego metodę f można wyprowadzić in-


terfejs 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 {

b4ac40784ca6e05cededff5eda5a930f
b
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ć zde-


finiowane 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 imple-
mentująca interfejs I3 została przedstawiona na listingu 5.44.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 237

Listing 5.44.
public class MojaKlasa implements I3 {
public void f() {//wymuszona przez I1
//treść metody f
}
public void g() { //wymuszona przez I2
//treść metody g
}
public void h() {//wymuszona przez I3
//treść metody g
}
}

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 nie-
bezpieczeństwo istnieje zarówno przy dziedziczeniu pojedynczym, jak i wielokrot-
nym. 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 kon-
flikt 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 doty-
czy trzech interfejsów. Interfejs I1 zawiera bezargumentową metodę f o typie zwraca-
nym int, I2 zawiera również bezargumentową metodę f, ale o typie zwracanym double.
Trzeci interfejs o nazwie I3 dziedziczy zarówno po I1, jak i po I2 (dodatkowo zawiera
własną metodę o nazwie h). Oczywiście czegoś takiego nie wolno zrobić, gdyż to
prowadzi do konfliktu nazw. Próba kompilacji interfejsu I3 spowoduje pojawienie się
komunikatu o błędzie widocznego na rysunku 5.25.

Listing 5.46.
public interface I1 {
public int f();
}

public interface I2 {
public double f();
}

public interface I3 extends I1, I2 {


public void h();
}

b4ac40784ca6e05cededff5eda5a930f
b
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 zapo-
znanie się z owym nieprzedstawionym jeszcze rodzajem klas. Lekcja 28. jest poświę-
cona właśnie wprowadzeniu w tę nową tematykę. Zostanie w niej pokazane, w jaki

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 239

sposób tworzy się klasy wewnętrzne oraz jakie mają one właściwości, a także jak dostać
się do ich składowych i jakie relacje zachodzą między klasą wewnętrzną a zewnętrzną.

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 na-
zwie Outside.java i skompilować ten plik tak jak każdy inny plik z kodem źródłowym
w Javie. Po kompilacji powstaną dwa pliki z rozszerzeniem class: Outside.class zawiera-
jący kod klasy Outside oraz Outside$Inside.class zawierający kod klasy Inside.

Listing 5.47.
public class Outside {
class Inside {
}
}

W klasie zewnętrznej można bez problemów oraz bez żadnych dodatkowych zabiegów
programistycznych korzystać z obiektów klasy wewnętrznej. Można je tworzyć, a także
bezpośrednio odwoływać się do zdefiniowanych w nich pól i metod. Przykład odwołań
do obiektu klasy wewnętrznej jest widoczny na listingu 5.48.

Listing 5.48.
public class Outside {
class Inside {
public int liczba = 100;
public void f() {
System.out.println("Inside:f liczba = " + liczba);
}
}

7
Dokładniej rzecz ujmując: taka, która została zdefiniowana we wnętrzu innej klasy i nie jest klasą statyczną.
Ściśle zgodnie z terminologią należałoby mówić o klasach zagnieżdżonych (ang. nested classes), które
dzielą się na statyczne klasy zagnieżdżone (ang. static nested classes) i klasy wewnętrzne (ang. inner
classes).

b4ac40784ca6e05cededff5eda5a930f
b
240 Java. Praktyczny kurs

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ę wyko-
nywanie kodu programu. W tej metodzie jest tworzony nowy obiekt klasy Outside
i wywoływana jest jego metoda g. Odbywa się to w jednej linii programu. Niestosowana
do tej pory przez nas konstrukcja:
new Outside().g();

oznacza: utwórz obiekt klasy Outside, a następnie wywołaj jego metodę o nazwie g
(odwołanie do obiektu nie jest tym samym nigdzie zapamiętywane). Efekt działania tego
fragmentu kodu jest taki sam jak rezultat wykonania dwóch instrukcji:
Outside out = new Outside();
out.g();

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 zapre-
zentowano wyżej. Przykładowa klasa Outside, w której zostały zdefiniowane dwie klasy
wewnętrzne, została przedstawiona na listingu 5.49.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 241

Listing 5.49.
public class Outside {
SecondInside secondIns = new SecondInside();
class FirstInside {
public int liczba = 100;
public void f() {
System.out.println("FirstInside:f liczba = " + liczba);
}
}
class SecondInside {
public double liczba = 1.0;
public void f() {
System.out.println("SecondInside:f liczba = " + liczba);
}
}
public void g() {
FirstInside firstIns = new FirstInside();
firstIns.f();
firstIns.liczba = 200;
firstIns.f();
secondIns.f();
secondIns.liczba = 2.5;
secondIns.f();
}
public static void main(String args[]) {
new Outside().g();
}
}

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 dys-
pozycji są również dwie metody: g oraz main. W g tworzymy zmienną typu FirstInside
o nazwie firstIns i przypisujemy jej odniesienie do nowego obiektu klasy FirstInside,
następnie wywołujemy metodę f tego obiektu, przypisujemy polu liczba wartość 200
oraz ponownie wywołujemy metodę f. W kolejnym kroku wywołujemy metodę f obiektu
wskazywanego przez secondIns, przypisujemy polu liczba tego obiektu wartość 2.5
oraz ponownie wywołujemy jego metodę f. Nie musimy tworzyć obiektu secondIns,
gdyż czynność ta została wykonana w trakcie deklaracji pola secondIns. Ostatecznie
po uruchomieniu takiej aplikacji na ekranie pojawi się widok zaprezentowany na ry-
sunku 5.27.

b4ac40784ca6e05cededff5eda5a930f
b
242 Java. Praktyczny kurs

Rysunek 5.27.
Wykorzystanie dwóch
klas wewnętrznych

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 we-
wnę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;

b4ac40784ca6e05cededff5eda5a930f
b
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 na-
zwie 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 de-
klaracji 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 no-
wy obiekt klasy Outside, następnie odwołujemy się do jego pola ins, czyli do obiektu
klasy Inside. Wywołujemy metodę f, po czym przypisujemy polu liczba wartość 100
i ponownie wywołujemy metodę f. Próba kompilacji spowoduje wystąpienie błędu
widocznego na rysunku 5.28. Skoro bowiem pole liczba z klasy Inside jest polem
prywatnym, można się do niego odwoływać jedynie z wnętrza klasy Inside lub klasy
zewnętrznej dla Inside (czyli Outside). Problem zniknie, jeśli pole liczba będzie polem
publicznym lub też dopiszemy metody publiczne operujące na nim.

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żli-
wość. Klasa wewnętrzna może się również znaleźć w dowolnym miejscu między po-
lami, między metodami, a także… wewnątrz metod (będzie to wtedy lokalna klasa

b4ac40784ca6e05cededff5eda5a930f
b
244 Java. Praktyczny kurs

wewnętrzna, ang. local inner class)8. Ta ostatnia sytuacja jest rzadziej spotykana, nie-
mniej taka możliwość istnieje i warto wiedzieć, jak w praktyce traktować taki frag-
ment kodu. Jak zatem umieścić klasę wewnętrzną w treści metody klasy zewnętrznej?
Taka sytuacja została zaprezentowana na listingu 5.52.

Listing 5.52.
public class Outside {
public void f() {
class Inside {
/*…pola klasy Inside…*/
public void g() {
/*…instrukcje metody g…*/
}
/*…dalsze metody klasy Inside…*/
}
/*…dalsze instrukcje metody f…*/
}
/*…dalsze metody klasy Outside…*/
}

Są tu dwie klasy: Outside i Inside. Outside jest klasą zewnętrzną i zawiera bezargu-
mentową metodę o nazwie f. W tej metodzie została zdefiniowana klasa wewnętrzna
o nazwie Inside zawierająca bezargumentową metodę g. Trzeba zdawać sobie jednak
sprawę, że w takiej sytuacji widoczność klasy Inside została ograniczona wyłącznie
do metody f klasy Outside. Poza tą metodą jest ona nieznana i nie można się do niej
odwoływać. Zostało to zilustrowane w przykładzie z listingu 5.53.

Listing 5.53.
public class Outside {
//źle, w tym miejscu nie jest znana klasa Inside
//Inside ins1 = new Inside();
public void f() {
//źle, w tym miejscu nie jest znana klasa Inside
//Inside ins2 = new Inside();
class Inside {
public void g() {
}
}
//dobrze, klasa Inside została zdefiniowana
Inside ins3 = new Inside();
}
//źle, w tym miejscu nie jest znana klasa Inside
//Inside ins4 = new Inside();
}

W komentarze zostały ujęte instrukcje, których wykonanie skończy się błędem kom-
pilacji. Nie można utworzyć obiektów ins1, ins2 i ins4, gdyż w miejscach ich deklaracji
nie jest widoczna klasa Inside. Jedyne miejsce, w którym można się do niej bezpo-

8
W rzeczywistości klasa wewnętrzna może się znaleźć w dowolnym bloku wyróżnionym za pomocą
znaków nawiasu klamrowego, a więc również w pętli czy w instrukcji warunkowej.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 245

średnio odwoływać, znajduje się w metodzie f, za jej (klasy) definicją. Próby odwołań poza
tym zasięgiem nieuchronnie doprowadzą do powstania błędów kompilacji.

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 ilu-
strują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 = " + liczba);
ins.g(argument);
System.out.println("liczba = " + liczba);
}
public static void main(String args[]) {
new Outside().f(200);
}
}

Klasa zewnętrzna Outside zawiera prywatne pole o nazwie liczba, któremu w trakcie
deklaracji jest przypisywana wartość 100. W tej klasie jest również zdefiniowana klasa
wewnętrzna Inside, która zawiera prywatną metodę g przyjmującą jeden argument typu
int. Wartość tego argumentu jest przypisywana, uwaga… prywatnemu polu liczba
w klasie Outside. Jak widać, nie trzeba stosować żadnych specjalnych konstrukcji
programistycznych, dostęp do pola klasy zewnętrznej jest bezpośredni.

Klasa Outside zawiera też prywatną metodę f przyjmującą argument typu int. W tej
klasie tworzony jest obiekt klasy Inside, który jest przypisywany zmiennej ins. Następ-
nie na ekranie jest wyświetlana bieżąca wartość pola liczba oraz wywoływana jest
metoda g obiektu ins. Metodzie tej zostaje przekazany argument otrzymany przez
metodę f. Na zakończenie ponownie wyświetlana jest wartość pola liczba.

W metodzie main, od której rozpocznie się wykonywanie kodu, jest tworzony nowy obiekt
klasy Outside i wywoływana jest jego metoda f. Metodzie tej jest przekazywana wartość
200. Tym samym wartość ta zostaje ostatecznie przypisana (w metodzie g klasy Inside)
polu liczba. Na ekranie pojawi się więc widok zaprezentowany na rysunku 5.29.

b4ac40784ca6e05cededff5eda5a930f
b
246 Java. Praktyczny kurs

Rysunek 5.29.
Ilustracja odwołania
z klasy wewnętrznej
do prywatnego pola
klasy zewnętrznej

Ć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 do-
konuj zmian w klasie Outside.

Ćwiczenie 28.3.
Zmodyfikuj kod klas z listingu 5.51 tak, aby pole liczba klasy Inside pozostało polem
prywatnym, ale by była możliwa jego modyfikacja. Nie dokonuj zmian w klasie Inside.

Ćwiczenie 28.4.
Umieść definicję klasy wewnętrznej w metodzie klasy zewnętrznej, w bloku instrukcji if.

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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 247

Co jednak zrobić, aby móc operować na obiekcie klasy wewnętrznej poza klasą ze-
wnętrzną? Przykładowo: co zrobić w sytuacji, kiedy klasą zewnętrzną jest Outside,
wewnętrzną Inside, a chcemy mieć dostęp do obiektów klasy Inside z zupełnie nie-
zależnej klasy Main?

Możliwe są dwa rozwiązania tego problemu. Otóż w klasie zewnętrznej można umieścić
metodę tworzącą i zwracającą obiekty klasy wewnętrznej lub też, za pomocą specjalnej
składni, bezpośrednio powołać do życia obiekt klasy wewnętrznej. Zacznijmy od pierw-
szego sposobu. Na listingu 5.55 są widoczne przykładowe klasy Inside i Outside.

Listing 5.55.
public class Outside {
class Inside {
public void g() {
System.out.println("Inside:g()");
}
}
public Inside getInside() {
return new Inside();
}
}

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ęp-
nie wywołujemy metodę getInside tego obiektu, która zwraca odniesienie do obiektu
klasy Inside, oraz wywołujemy metodę g tego obiektu. Po skompilowaniu i urucho-
mieniu klasy Main pojawi się widok zaprezentowany na rysunku 5.30. W klasie Main
udało się więc wykonać bezpośrednią operację na obiekcie typu Inside.

Rysunek 5.30.
Wywołanie metody klasy
wewnętrznej z klasy
Main

b4ac40784ca6e05cededff5eda5a930f
b
248 Java. Praktyczny kurs

Przedstawiony sposób dostępu do obiektu klasy wewnętrznej jest jak najbardziej po-
prawny, ale ma jedną wadę. Otóż obiekt klasy Inside można wykorzystać tylko raz, tylko
do jednokrotnego wywołania metody g, gdyż nie jest przechowywana referencja do
niego. Tego typu wywołania spotyka się w praktyce, jeśli jednak ma być zachowany
dostęp do obiektu zwróconego przez metodę getInside, trzeba postąpić inaczej. Należy
oczywiście zadeklarować zmienną, w której zostanie zapisana referencja.

Pytanie brzmi: jakiego typu będzie ta zmienna? Odpowiedź, która nasuwa się w pierw-
szej chwili, brzmi: zmienna powinna być typu Inside. Jeśli jednak spróbujemy w klasie
Main dokonać przykładowej deklaracji w postaci:
Inside ins;

nie osiągniemy zamierzonego celu. Kompilator nie zna osobnej klasy o nazwie Inside
i zgłosi błąd. Klasa wewnętrzna nie może istnieć samodzielnie, bez zewnętrznej. Jest to
również odzwierciedlone w deklaracji zmiennych klas wewnętrznych. Deklaracja taka
powinna wyglądać następująco:
klasa_zewnętrzna.klasa_wewnętrzna nazwa_zmiennej;

Czyli prawidłowa deklaracja zmiennej klasy Inside w klasie Main powinna mieć postać:
Outside.Inside ins;

Przykładowa klasa Main wykorzystująca taką deklarację jest widoczna na listingu 5.57.

Listing 5.57.
public class Main {
public static void main(String args[]) {
Outside out = new Outside();
Outside.Inside ins;
ins = out.getInside();
ins.g();
ins.g();
}
}

Tworzymy nowy obiekt klasy Outside i przypisujemy go zmiennej out. Następnie dekla-
rujemy zmienną klasy Inside, wykorzystując przedstawioną przed chwilą konstrukcję.
Tę zmienną inicjujemy, wywołując metodę getInside z klasy Outside, która zwraca
obiekt klasy Inside. W ten sposób zachowujemy referencję do obiektu, którą można dalej
dowolnie wykorzystywać. W tym przypadku dwukrotnie wywoływana jest metoda g.

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() {

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 249

System.out.println("Inside:g()");
}
}
}

Przede wszystkim trzeba sobie uświadomić, że obiekt klasy wewnętrznej nie może
istnieć, o ile nie istnieje również obiekt klasy zewnętrznej10. Zawsze więc najpierw
tworzymy obiekt klasy zewnętrznej i — dopiero korzystając z niego — możemy po-
wołać do życia obiekt klasy wewnętrznej. Powinniśmy zastosować w takiej sytuacji
ciąg instrukcji o następującej postaci:
klasa_zewnętrzna obiekt_zew = new klasa_zewnętrzna();
klasa_zewnętrzna.klasa_wewnętrzna obiekt_wew = obiekt_zew.new klasa_wewnętrzna();

Przy założeniu, że klasy Outside i Inside są takie jak na listingu 5.58, instrukcje te
wyglądałyby następująco:
Outside out = new Outside();
Outside.Inside ins = out.new Inside();

Po ich wykonaniu zmienna ins będzie wskazywała na nowy obiekt klasy wewnętrznej
Inside, na którym będzie można wykonywać wszelkie dozwolone operacje, np. wywoły-
wać metody, tak jak zostało to przedstawione na listingu 5.59.

Listing 5.59.
public class Main {
public static void main(String args[]) {
Outside out = new Outside();
Outside.Inside ins = out.new Inside();
ins.g();
}
}

Rodzaje klas wewnętrznych


Dotychczas zostało zaprezentowanych już kilkanaście przykładów z klasami wewnętrz-
nymi. Ł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 zo-
stał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 zo-
brazowane 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 we-
wnętrzna dla Outside, to klasa pakietowa, w związku z tym nie ma możliwości od-
woływania się do niej z klasy Main. Dlatego też kolejna instrukcja metody main (two-
rząca obiekt klasy Inside) spowoduje powstanie błędów kompilacji.

Gdyby zatem istniała konieczność odwołania się w klasie Main do klasy Inside, czyli uzy-
skania dostępu do klasy wewnętrznej z klasy niezależnej, klasa wewnętrzna musiałaby
mieć dostęp publiczny. W takiej sytuacji klasa Inside powinna mieć postać:
public class Inside {
public void g() {
System.out.println("Inside:g()");
}
}

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ć:

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 251

 pakietowe,
 publiczne,
 prywatne,
 chronione
i należy je traktować tak jak składowe wymienionych typów. Zatem aby całkowicie za-
bronić dostępu do klasy wewnętrznej (a jest to stosunkowo często spotykana sytuacja),
należy zrobić z niej klasę prywatną, tak jak zostało to przedstawione w przykładzie na
listingu 5.61.

Listing 5.61.
public class Outside {
private class Inside {
public void g() {
System.out.println("Inside:g()");
}
}
public Inside getInside() {
return new Inside();
}
}

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 zdefi-
niowana metoda getInside zwracająca nowy obiekt klasy Inside. Co w takiej sytuacji
dzieje się w klasie Main? Otóż można oczywiście utworzyć obiekt klasy Outside,
gdyż jest ona klasą publiczną, wszelkie próby odwołania do klasy Inside skończą się
natomiast niepowodzeniem. Nie uda się ani utworzyć obiektu Inside, ani pobrać re-
ferencji do niego przy użyciu metody getInside z jednoczesnym wywołaniem metody g.
Istnieje natomiast możliwość wywołania samej metody getInside. Ponieważ jednak
na tak zwróconej referencji nie można wykonać żadnej operacji, nie ma to praktycznego
znaczenia11.

11
Chyba że konstruktor klasy Inside wykonywałby jakieś specjalne operacje, co jednak wydaje się
sytuacją czysto hipotetyczną.

b4ac40784ca6e05cededff5eda5a930f
b
252 Java. Praktyczny kurs

Warto zwrócić uwagę na jeszcze jedną ciekawą kwestię. Otóż metoda g z klasy Inside
jest publiczna, teoretycznie dostępna dla wszystkich innych klas. Ponieważ jednak sama
klasa Inside jest klasą wewnętrzną prywatną, metody g i tak nie można wywołać poza
klasami Inside i Outside.

Dziedziczenie po klasie zewnętrznej


Kolejna kwestia, której znajomość jest konieczna przy wykorzystywaniu klas wewnętrz-
nych, to sposób dziedziczenia. Otóż z klasy zewnętrznej można oczywiście bez żad-
nych 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 wyprowa-
dzona 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 wykorzysty-
wana 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ę ze-
wnę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 trakto-
wać jako klasę zewnętrzną dla klasy wewnętrznej. Brzmi to wyjątkowo zawile, ale
chodzi o to, że klasa ExtendedOutside z listingu 5.62 jest traktowana jako zewnętrzna
dla klasy Inside. Rzeczywiście tak jest, trzeba jednak wziąć w tym momencie pod
uwagę reguły dziedziczenia. Jeśli bowiem klasa Inside będzie klasą pakietową, pu-
bliczną lub chronioną, bez problemu będzie można odwoływać się do niej w klasie
ExtendedOutside. Gdyby jednak Inside była klasą prywatną, takie odwołania okaza-
łyby się niestety niemożliwe. Zilustrowano to w przykładzie z listingu 5.63.
Listing 5.63.
public class Outside {
private class Inside {
}
}

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 pry-
watną składową klasy Outside. W związku z tym klasa ExtendedOutside nie ma do niej
dostępu. Jeśli usuniemy komentarz z linii:
//Inside ins = new Inside();
przekonamy się o tym naocznie, gdyż kompilator zgłosi serię błędów.

b4ac40784ca6e05cededff5eda5a930f
b
254 Java. Praktyczny kurs

Ć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 chronio-
nym. Z klasy A wyprowadź klasę pochodną ExtendedA zawierającą pole klasy B.

Ćwiczenie 29.4.
Napisz klasę Outside zawierającą klasę wewnętrzną Inside o dostępie publicznym.
Z Outside wyprowadź klasę ExtendedOutside zawierającą publiczną klasę wewnętrzną
o nazwie ExtendedInside.

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 za-
skakują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 te-
go 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…*/
}

b4ac40784ca6e05cededff5eda5a930f
b
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 widocz-
nym 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 prywat-
nego 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 usta-
wianie wartości pola liczba oraz zwrócenie poprzedniej wartości tego pola.

b4ac40784ca6e05cededff5eda5a930f
b
256 Java. Praktyczny kurs

Wykonywanie kodu rozpoczyna się od metody main. Jej kod zaczyna się od zadeklarowa-
nia zmiennej referencyjnej obj typu MojaKlasa i przypisania jej nowo utworzonego
obiektu klasy MojaKlasa. Następnie wywołujemy metodę setLiczba tego obiektu,
ustawiając wewnętrzne pole liczba na wartość 1. W kolejnym kroku za pomocą metody
getLiczba pobieramy wartość tego pola i przypisujemy ją zmiennej temp1, po czym
wywołujemy metodę setAndGet, przekazując w argumencie wartość 2. Wartość zwróconą
przez tę metodę przypisujemy zmiennej temp2.

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 komenta-


rza 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ów-
nież 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 zesta-
wem klas takim jak przedstawiony na listingu 5.64, możemy wykonać instrukcje dla
rzutowania jawnego:
MojaKlasa obj = new MojaKlasa();
PierwszyInterfejs pi = (PierwszyInterfejs) obj;

oraz dla rzutowania domyślnego:

b4ac40784ca6e05cededff5eda5a930f
b
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.

b4ac40784ca6e05cededff5eda5a930f
b
258 Java. Praktyczny kurs

innej możliwości manipulacji wartością pola liczba jak tylko przez wykorzystanie
metod setLiczba oraz getLiczba. Nic nie da nawet zmiana modyfikatora dostępu pola
liczba z private na public.

Należy zwrócić uwagę na średnik znajdujący się za definicją klasy anonimowej (formal-
nie kończący instrukcję przypisania). Jest on niezbędny do prawidłowej kompilacji
kodu.

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ć ope-
racje na polu liczba. Wywołujemy więc metodę setLiczba, przekazując jej w argu-
mencie 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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 259

Co zostanie wyświetlone na ekranie? Zmienna obj1 jest typu MojaKlasa, ale obiekt przez
nią wskazywany jest obiektem klasy anonimowej dziedziczącej po MojaKlasa. Po przy-
pomnieniu sobie materiału z lekcji 23. i 24. stanie się jasne, że wywołanie metody f
będzie wywołaniem polimorficznym, czyli zostanie wykonana metoda f z klasy anoni-
mowej. W drugim przypadku zmienna obj2 jest typu MojaKlasa i obiekt przez nią wska-
zywany jest również tego typu, zostanie więc oczywiście wykonana metoda f klasy
MojaKlasa. Ostatecznie na ekranie pojawi się widok zaprezentowany na rysunku 5.32.

Rysunek 5.32.
Polimorficzne
wywołanie metody
klasy anonimowej

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 ar-
gument 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 nawia-
sie 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.

b4ac40784ca6e05cededff5eda5a930f
b
260 Java. Praktyczny kurs

Klasy statyczne
Dzięki informacjom z lekcji 29. wiadomo, że obiekt klasy wewnętrznej nie może istnieć
bez obiektu klasy zewnętrznej. Jednym z powodów jest to, że klasa wewnętrzna prze-
chowuje odwołanie do zawierającego ją obiektu klasy zewnętrznej i dzięki temu ma
dostęp do jego składowych (por. lekcja 28.). Okazuje się jednak, że istnieje pewien
specjalny typ klas wewnętrznych, których obiekty mogą istnieć nawet wtedy, kiedy nie
istnieje obiekt klasy zewnętrznej. Są to wewnętrzne klasy statyczne nazywane klasami
zagnieżdżonymi (lub statycznymi klasami zagnieżdżonymi). Przykład takiej klasy
jest widoczny na listingu 5.68.

Listing 5.68.
public class Zewnetrzna {
public static class Wewnetrzna {
public void f() {
System.out.println("Wewnetrzna:f()");
}
}
public static void main(String args[]) {
Wewnetrzna wewnetrzna = new Wewnetrzna();
wewnetrzna.f();
}
}

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 utwo-
rzyć 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 typo-
wych 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 we-
wnętrznej z klasy zewnętrznej, nie jest też możliwa sytuacja odwrotna. Odwołanie może
nastąpić wyłącznie poprzez obiekt, tak jak zostało to pokazane w przykładzie z listingu
5.68 (wywołanie metody f). Inne próby odwołań skończą się błędami kompilacji. Prze-
konamy się o tym, jeśli spróbujemy skompilować kod z listingu 5.69. Zamiast spodzie-
wanych plików wynikowych pojawi się seria komunikatów o błędach (rysunek 5.33).

Listing 5.69.
public class Zewnetrzna {
public int zewLiczba;
public static class Wewnetrzna {
public int wewLiczba;
public void f() {
//nieprawidłowo, nie ma dostępu do pola
//zewLiczba klasy Zewnetrzna
zewLiczba = 100;

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 261

wewLiczba = 100;
System.out.println("Wewnetrzna:f()");
}
}
public void g() {
zewLiczba = 100;

//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 przeka-
zać 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 progra-
mistę 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ątne-


go, 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.

b4ac40784ca6e05cededff5eda5a930f
b
262 Java. Praktyczny kurs

Listing 5.70.
public class Przyprostokatne{
private double p1;
private double p2;
Przyprostokatne(double p1, double p2){
setPrzyprostokatne(p1, p2);
}
public void setPrzyprostokatne(double p1, double p2){
this.p1 = p1;
this.p2 = p2;
}
public double przeciwprostokatna(){
return Math.sqrt(p1 * p1 + p2 * p2);
}
}

Pola p1 i p2 są prywatne, a ich wartości mogą być zmieniane dzięki metodzie set
Przyprostokatne. W klasie jest tylko jeden dwuargumentowy konstruktor, w którym
ta metoda jest wywoływana. Dzięki temu tworzenie obiektów klasy Przyprostokatne
będzie możliwe wyłącznie po podaniu długości obu przyprostokątnych — nie da się
zatem zapomnieć o podaniu jednej z wartości. W metodzie przeciwprostokatna za
pomocą powszechnie znanego wzoru (pierwiastek z sumy kwadratów przyprostokąt-
nych daje przeciwprostokątną) wyliczana jest długość przeciwprostokątnej. Pierwiastek
kwadratowy uzyskiwany jest dzięki wywołaniu statycznej metody sqrt z klasy Math
(tak jak miało to miejsce w przykładach z rozdziału 2.), a zamiast podnoszenia do
drugiej potęgi zostało użyte mnożenie.

Działanie nowej klasy można wypróbować za pomocą programu przedstawionego na li-


stingu 5.71. W metodzie main tworzony jest nowy obiekt klasy Przyprostokatne, a w kon-
struktorze 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 techni-


ki omawiane do tej pory. Metodę przeciwprostokatna można byłoby jednak przygoto-
wać 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ć ustala-
ne 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 żad-
nych ograniczeń.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 263

A zatem metoda przeciwprostokatna ma być napisana tak, aby przy jej wywoływaniu
dało się przekazać jej kod wykonywalny operujący na wartościach pól p1 i p2. Same
wartości pól mają jednak pozostać niezmienione — nie chcemy zmieniać parametrów
trójkąta, chcemy jedynie dowiedzieć się, co będzie się teoretycznie działo przy różnych
zmianach długości boków. Jak jednak przekazać kod wykonywalny, czyli w rzeczy-
wistości treść innej metody? Nie ma możliwości, by zrobić to bezpośrednio. Moglibyśmy
jednak przekazać obiekt zawierający metodę wykonującą żądane przez nas obliczenia.
Utwórzmy więc przeciążoną wersję metody przeciwprostokatna przyjmującą argument
będący obiektem typu Przeksztalcenia, który zawiera metodę o nazwie przeksztalc.
Ta metoda będzie przyjmowała jeden argument typu double i zwracała jego wartość
po pewnym przekształceniu. Metoda przeciwprostokatna przyjmie wtedy postać
przedstawioną na listingu 5.72.

Listing 5.72.
public double przeciwprostokatna(Przeksztalcenia prz){
double p1 = prz.przeksztalc(this.p1);
double p2 = prz.przeksztalc(this.p2);
return Math.sqrt(p1 * p1 + p2 * p2);
}

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 wy-
glą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ć korzy-
stanie z konkretnej klasy Przeksztalcenia, która będzie pełniła wyłącznie rolę po-
mocniczą (rolę opakowania dla metody przeksztalc)? Nie chodzi przecież o to, żeby
dostarczany był obiekt konkretnej klasy (dokładniej: pochodny od pewnej klasy abs-
trakcyjnej), 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 pro-
stu 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);
}

b4ac40784ca6e05cededff5eda5a930f
b
264 Java. Praktyczny kurs

Taki interfejs, który zawiera deklarację tylko jednej metody abstrakcyjnej, nazywamy
interfejsem funkcjonalnym (ang. functional interface). Powinno już być teraz widać,
że praktyczne wykorzystanie klasy Przyprostokatne i metody przeciwprostokatna będzie
wymagało użycia obiektu DOWOLNEJ klasy implementującej interfejs Przeksztalcenia
(taka klasa mogłaby też implementować dowolnie wiele innych interfejsów). Może ona
być publiczna, pakietowa, wewnętrzna. Zatem przykładowy program mógłby wyglądać
tak jak na listingu 5.74.

Listing 5.74.
class Zwiekszaj implements Przeksztalcenia{
public double przeksztalc(double arg){
return arg + 5;
}
}

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 przy-
kł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 obli-
czeniem 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 typo-
wych konstrukcji wymaga dla każdego rodzaju obliczeń pisania osobnych klas (np. klasa
zmniejszająca, mnożąca, dzieląca, dodająca itd.). To na pewno nie byłoby wygodne.
Od czego jednak są klasy anonimowe. Przecież klasa jest tak naprawdę jedynie opa-
kowaniem dla metody przeksztalc. W większość przypadków nie będzie więc potrzeby
jej nazywania i tworzenia osobnych konstrukcji. Nic nie stoi na przeszkodzie, aby me-
todzie przeciwprostokatna przekazywać obiekty klasy anonimowej implementującej
interfejs Przeksztalcenia. Wyglądałoby to wtedy tak jak na listingu 5.75.

Listing 5.75.
public class Main {
public static void main(String args[]) {
Przyprostokatne p = new Przyprostokatne(3, 4);
System.out.println(p.przeciwprostokatna(
new Przeksztalcenia(){
public double przeksztalc(double arg){

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 265

return arg - 1;
}
})
);
}
}

Przedstawiony zapis może wydawać się niezbyt czytelny, ale to wyłącznie kwestia przy-
zwyczajenia. To rodzaj wywołania kaskadowego (charakterystycznego dla stylu pro-
gramowania funkcyjnego), w którym unika się tworzenia zbędnych zmiennych pomoc-
niczych (dodatkowo można byłoby pozbyć się zmiennej p, co może być dobrym
ćwiczeniem do samodzielnego wykonania). Metoda println otrzymała w postaci argu-
mentu wartość zwróconą przez wywołanie metody przeciwprostokatna obiektu wska-
zywanego przez zmienną p.

Argumentem metody przeciwprostokatna jest z kolei nowy obiekt klasy anonimowej


implementującej interfejs Przeksztalcenia. W związku z tym pojawi się definicja me-
tody przeksztalc, która tym razem zwraca wartość przekazanego jej argumentu pomniej-
szoną 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ąt-
kują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.

b4ac40784ca6e05cededff5eda5a930f
b
266 Java. Praktyczny kurs

wyrażeń lambda (ang. lambda expressions). Takie wyrażenie można przedstawić w sche-
matycznej postaci:
(typ1 arg1, typ2 arg2, … , typN argN) -> {treść metody}

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 przyj-
mujący argument typu int oraz drugi przyjmujący argument typu double. Utwórz dwa
obiekty klasy anonimowej, korzystając z obu konstruktorów.

Ćwiczenie 30.4.
Napisz statyczną klasę zagnieżdżoną zawierającą metodę statyczną o nazwie f wyświe-
tlającą dowolny napis na ekranie. Wywołaj tę metodę, nie tworząc żadnego obiektu.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 5.  Programowanie obiektowe. Część II 267

Ćwiczenie 30.5.
Napisz taką wersję programu z listingu 5.75, aby nie została w niej użyta żadna zmienna.
Dodatkowo wykonaj to ćwiczenie, stosując wyrażenie lambda.

Ćwiczenie 30.6.
Przygotuj taką wersję klasy Przyprostokatne, aby można było osobno decydować o prze-
kształceniu wykonywanym na każdej z przyprostokątnych (np. pierwszą zwiększyć, a drugą
zmniejszyć). Czy trzeba będzie wprowadzać zmiany do interfejsu Przeksztalcenia?
Napisz program testujący nowe możliwości.

b4ac40784ca6e05cededff5eda5a930f
b
268 Java. Praktyczny kurs

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.
System wejścia-wyjścia
Do pisania aplikacji w Javie niezbędna jest znajomość przynajmniej podstaw systemu
wejścia-wyjścia. Tej tematyce jest poświęcony ten rozdział. W Javie operacje wej-
ścia-wyjścia są wykonywane za pomocą strumieni. Strumień to abstrakcyjny ciąg da-
nych, który działa — mówiąc w uproszczeniu — w taki sposób, że dane wprowadzone
na jednym jego końcu pojawiają się na drugim końcu. Strumienie dzielą się na wejściowe
i wyjściowe. Strumienie wyjściowe mają początek w aplikacji i koniec w innym urzą-
dzeniu, np. na ekranie czy w pliku, umożliwiają zatem wyprowadzanie danych z pro-
gramu. Przykładowo standardowy strumień wyjściowy jest reprezentowany przez obiekt
System.out, a wykonanie metody println tego obiektu powoduje, że dane domyślnie zo-
stają wysłane na ekran. Strumienie wejściowe działają odwrotnie: ich początek znajduje
się poza aplikacją (może to być np. klawiatura albo plik dyskowy), a koniec w aplikacji,
umożliwiają zatem wprowadzanie danych. Co więcej, strumienie umożliwiają też ko-
munikację między obiektami w obrębie jednej aplikacji. W tym rozdziale zostanie
jednak omówiona tylko komunikacja aplikacji ze światem zewnętrznym.

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 wpro-
wadzania 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 reprezentu-
ją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.

b4ac40784ca6e05cededff5eda5a930f
b
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 Zaznacza bieżącą pozycję w strumieniu.
readlimit)
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, Odczytuje ze strumienia liczbę bajtów nie większą niż określona
int off, int len) 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 wprowa-
dzony 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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.  System wejścia-wyjścia 271

Pierwszym krokiem jest zaimportowanie pakietu java.io (lekcja 17.), znajduje się w nim
bowiem definicja wyjątku IOException, który musimy przechwycić. W metodzie main
za pomocą metody System.out.println wyświetlamy tekst z prośbą o wprowadzenie
dowolnego znaku z klawiatury, a następnie wykonujemy instrukcję:
char c = (char) System.in.read();

Wywołujemy zatem metodę read obiektu in zawartego w klasie System2. Obiekt ten jest
klasy InputStream (typem obiektu jest InputStream) i reprezentuje standardowy stru-
mień wejściowy. W typowym przypadku jest on powiązany z klawiaturą, więc z tego
strumienia będą odczytywane dane wprowadzane przez użytkownika z klawiatury.
Metoda read zwraca wartość typu int, dokonujemy zatem konwersji na typ char, tak
aby wyświetlić na ekranie wprowadzony znak, i przypisujemy go zmiennej c. Następnie
wyświetlamy zawartość tej zmiennej za pomocą instrukcji:
System.out.println(c);

Aby wyświetlić kod znaku, należy ponownie dokonać konwersji. Zmienną c typu char
konwertujemy na typ int, tak aby metoda println potraktowała ją jako liczbę, a nie jako
znak. Ponieważ metoda read może spowodować powstanie wyjątku IOException,
wszystkie instrukcje zostały ujęte w blok try, który zapobiegnie niekontrolowanemu
zakończeniu programu (por. lekcje z rozdziału 4.). Przykładowy efekt działania programu
z listingu 6.1 jest widoczny na rysunku 6.1.

Rysunek 6.1.
Przykładowy efekt
działania programu
z listingu 6.1

Trzeba zwrócić uwagę, że w ten sposób tak naprawdę odczytywany jest ze strumienia
nie jeden znak, ale jeden bajt. A te pojęcia nie są tożsame. Jeden znak może zawierać
w zależności od standardu kodowania od jednego do kilku bajtów.

Wczytywanie tekstu
Wiadomo już, jak odczytać jeden bajt, co jednak zrobić, aby wprowadzić całą linię tekstu?
Przecież taka sytuacja jest o wiele częstsza. Można oczywiście odczytywać pojedyn-
cze znaki w pętli tak długo, aż zostanie osiągnięty znak końca linii, oraz połączyć je
w obiekt typu String. Najlepiej byłoby wręcz wyprowadzić z klasy InputStream klasę
pochodną, która zawierałaby metodę np. o nazwie readLine, wykonującą taką czynność.
Trzeba byłoby przy tym pamiętać o odpowiedniej obsłudze standardów kodowania
znaków. Na szczęście w JDK został zdefiniowany cały zestaw klas operujących na
strumieniach wejściowych. Zamiast więc powtarzać coś, co zostało już zrobione, najlepiej
po prostu z nich skorzystać.

2
Obiekt in jest polem finalnym i statycznym klasy System.

b4ac40784ca6e05cededff5eda5a930f
b
272 Java. Praktyczny kurs

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 naj-
odpowiedniejszy będzie InputStreamReader. Ponieważ nie ma takiego obiektu w aplikacji
(do dyspozycji mamy jedynie obiekt System.in typu InputStream), należy go utwo-
rzyć, 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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.  System wejścia-wyjścia 273

znaków) linię tekstu wprowadzoną przez użytkownika. Linia tekstu jest rozumiana
jako ciąg znaków wprowadzony aż do naciśnięcia klawisza Enter (dokładniej: aż do
osiągnięcia znaku końca wiersza w strumieniu). Wynika z tego, że instrukcja:
String line = brIn.readLine();

utworzy zmienną line typu String i przypisze jej obiekt (referencję do obiektu) zwrócony
przez metodę readLine obiektu brIn. Działanie programu będzie zatem następujące:
po uruchomieniu zostanie wyświetlona prośba o wprowadzenie linii tekstu, następnie
linia ta zostanie odczytana i przypisana zmiennej line, a potem zawartość tej zmiennej
zostanie wyświetlona na ekranie. Przykład wykonania tego programu pokazano na ry-
sunku 6.2.

Rysunek 6.2.
Wczytanie linii tekstu
za pomocą obiektu
klasy BufferedReader

Skoro wiadomo już, jak wczytać wiersz tekstu z klawiatury, spróbujmy napisać pro-
gram, którego zadaniem będzie wczytywanie kolejnych linii ze standardowego wej-
ścia tak długo, aż zostanie odczytany ciąg znaków quit. Zadanie wydaje się banalne,
wystarczy przecież do kodu z listingu 6.2 dodać pętlę while, która będzie sprawdzała, czy
zmienna line zawiera napis quit. W pierwszym odruchu napiszemy zapewne pętlę,
która będzie wyglądać następująco (przy założeniu, że wcześniej został prawidłowo
zainicjowany obiekt brIn):
String line = "";
while(line != "quit"){
line = brIn.readLine();
System.out.println("Wprowadzona linia to: " + line);
}
System.out.println("Koniec wczytywania danych.");

Warto przeanalizować ten przykład. Na początku deklarujemy zmienną line i przypisu-


jemy 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 po-
winien zakończyć działanie, tymczasem działa nadal, co jest widoczne na rysunku 6.3.

b4ac40784ca6e05cededff5eda5a930f
b
274 Java. Praktyczny kurs

Rysunek 6.3.
Błąd w pętli while
uniemożliwia
zakończenie programu

Listing 6.3.
import java.io.*;

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ł nie-
nazwany obiekt klasy String faktycznie zawierający ciąg znaków quit, a w miejsce
napisu "quit" została wstawiona referencja do tego obiektu. Jak wiadomo z wcześniej-
szych lekcji (lekcja 13.), zmienna line jest także referencją do obiektu klasy String.
Instrukcja porównania line != "quit" porównuje zatem ze sobą dwie REFERENCJE,
które w tym przypadku muszą być różne. W związku z tym nie dochodzi tu do porów-
nania zawartości obiektów. Dlatego program nie może działać poprawnie i wpada w nie-
skończoną pętlę (aby zakończyć jego działanie, trzeba wcisnąć na klawiaturze kombi-
nację klawiszy Ctrl+C).

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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.  System wejścia-wyjścia 275

Warto pamiętać o tym podczas tworzenia własnych klas, by w razie potrzeby dopisać
własną wersję tej metody.

Jak zastosować tę metodę w praktyce? Otóż zwraca ona wartość true, kiedy obiekty
są takie same (czyli ich zawartość jest taka sama), lub false, kiedy obiekty są różne.
W przypadku obiektów typu String wartość true zostanie zwrócona, jeśli ciąg zna-
ków zapisany w jednym z nich jest taki sam jak ciąg znaków zapisany w drugim. Jeżeli
np. istnieje zmienna zadeklarowana jako:
String napis1 = "test";

to wynikiem operacji:
napis1.equals("test");

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 zasa-
dzie 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,

b4ac40784ca6e05cededff5eda5a930f
b
276 Java. Praktyczny kurs

a potem w warunku pętli while następuje próba wywołania metody equals obiektu
wskazywanego przez line. Tymczasem line ma wartość null i nie wskazuje na żaden
obiekt. W takiej sytuacji musi zostać wygenerowany wyjątek NullPointerException.

Jak temu zapobiec? Możliwości są dwie: albo dodamy w pętli warunek sprawdzający,
czy line jest równe null, a jeśli tak, to przerwiemy pętlę np. instrukcją break, albo też
zmodyfikujemy sam warunek pętli. Ta druga możliwość jest lepsza, gdyż nie powoduje
wykonywania dodatkowego kodu, a jednocześnie otrzymujemy ciekawą konstrukcję.
Poprawiony warunek powinien wyglądać następująco:
while(!"quit".equals(line)){
/*instrukcje pętli while*/
}

Początkowo tego typu zapis budzi zdziwienie: jak można wywoływać jakąkolwiek
metodę na rzecz ciągu znaków? Wystarczy jednak sobie przypomnieć stwierdzenie,
które pojawiło się kilka akapitów wyżej, otóż literał3 "quit" w rzeczywistości powoduje
powstanie obiektu klasy String i podstawianie w jego (literału) miejsce referencji do
tego obiektu. Skoro tak, można wywołać metodę equals. To właśnie dzięki takiej kon-
strukcji unikamy wyjątku NullPointerException, gdyż nawet jeśli line będzie miało
wartość null, to metoda equals po prostu zwróci wartość false, bo null na pewno jest
różne od ciągu znaków quit. Ostatecznie pełny prawidłowy kod będzie miał postać
przedstawioną na listingu 6.4. Przykład działania tego kodu został z kolei zaprezento-
wany na rysunku 6.5.

Listing 6.4.
import java.io.*;

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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.  System wejścia-wyjścia 277

Rysunek 6.5.
Wprowadzanie w pętli
kolejnych linii tekstu

Wprowadzanie liczb
Potrafimy już odczytywać w aplikacji wiersze tekstu wprowadzane z klawiatury, równie
ważną umiejętnością jest wprowadzanie liczb. Jak to zrobić? Trzeba sobie uzmysłowić, że
z klawiatury zawsze wprowadzany jest tekst. Jeśli próbujemy wprowadzić do aplika-
cji wartość 123, to w rzeczywistości wprowadzimy trzy znaki, "1", "2" i "3" o kodach
ASCII 61, 62, 63. Mogą one zostać przedstawione w postaci ciągu "123", ale to dopie-
ro aplikacja musi przetworzyć ten ciąg na wartość 123. Takiej konwersji w przypadku
wartości całkowitej można dokonać np. za pomocą metody parseInt z klasy Integer.
Jest to metoda statyczna, można ją więc wywołać, nie tworząc obiektu klasy Integer4.
Przykładowe wywołanie może wyglądać następująco:
int liczba = Integer.parseInt(ciąg_znaków);

Zmiennej liczba zostanie przypisana wartość typu int zawarta w ciągu znaków ciąg_
znaków. Gdyby ciąg przekazany jako argument metody parseInt nie zawierał popraw-
nej wartości całkowitej, zostałby wygenerowany wyjątek NumberFormatException.
Aby zatem wprowadzić do aplikacji wartość całkowitą, można odczytać wiersz tekstu,
korzystając z metody readLine, a następnie wywołać metodę parseInt. Ten właśnie spo-
sób został wykorzystany w programie z listingu 6.5. Zadaniem tego programu jest wczy-
tanie liczby całkowitej oraz wyświetlenie wyniku mnożenia tej liczby przez wartość 2.

Listing 6.5.
import java.io.*;

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.

b4ac40784ca6e05cededff5eda5a930f
b
278 Java. Praktyczny kurs

catch(IOException e){
System.out.println("Błąd podczas odczytu strumienia.");
return;
}

int liczba;

try{
liczba = Integer.parseInt(line);
}
catch(NumberFormatException e){
System.out.print("Podana wartość nie jest liczbą całkowitą.");
return;
}
System.out.print(liczba + " * 2 = " + liczba * 2);
}
}

Kod zaczyna się od utworzenia obiektu brIn klasy BufferedReader powiązanego poprzez
obiekt pośredniczący klasy InputStreamReader ze standardowym strumieniem wejścio-
wym. Stosujemy technikę opisaną przy omawianiu przykładu z listingu 6.2. Następnie
wyświetlamy prośbę o wprowadzenie liczby całkowitej oraz odczytujemy wiersz tek-
stu za pomocą metody readLine obiektu brIn, nie zapominając o przechwyceniu ewentu-
alnego wyjątku IOException. W przypadku wystąpienia takiego wyjątku wyświetlamy sto-
sowną informację na ekranie oraz kończymy wykonywanie programu. Warto zwrócić
uwagę, że używana jest w tym celu instrukcja return bez żadnych parametrów, co ozna-
cza wyjście z funkcji main (bez zwracania wartości), a tym samym zakończenie dzia-
łania aplikacji.

Jeśli odczyt wiersza tekstu się powiedzie, zostanie on zapisany w zmiennej line. Dekla-
rujemy więc dalej zmienną liczba typu int oraz przypisujemy jej wynik działania metody
parseInt z klasy Integer. Metodzie przekazujemy ciąg znaków zapisany w zmiennej
line. Jeśli wprowadzony przez użytkownika ciąg znaków nie reprezentuje poprawnej
wartości liczbowej, wygenerowany zostaje wyjątek NumberFormatException. W takim wy-
padku wyświetlamy komunikat o tym fakcie oraz kończymy działanie funkcji main, a tym
samym programu, wywołując instrukcję return. Jeśli jednak konwersja tekstu na liczbę
powiedzie się, odpowiednia wartość zostanie zapisana w zmiennej liczba, można za-
tem wykonać mnożenie liczba * 2 i wyświetlić wartość wynikającą z tego mnożenia
na ekranie. Przykładowy wynik działania programu jest widoczny na rysunku 6.6.

Rysunek 6.6.
Przykładowy wynik
działania programu
z listingu 6.5

Gdyby miała być wczytana liczba zmiennoprzecinkowa, należałoby do konwersji zasto-


sować metodę parseDouble z klasy Double lub parseFloat z klasy Float, co jest do-
skonałym ćwiczeniem do samodzielnego wykonania.

b4ac40784ca6e05cededff5eda5a930f
b
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żli-
wiał wprowadzenie liczby zmiennoprzecinkowej.

Ćwiczenie 31.4.
Napisz klasę pochodną od BufferedReader, która będzie zawierała metody getInt oraz
getDouble odczytujące ze strumienia liczby całkowite oraz zmiennopozycyjne (zmien-
noprzecinkowe).

Ćwiczenie 31.5.
Napisz klasę Main testującą działanie metod getInt oraz getDouble z ćwiczenia 31.4.

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 da-
nych do aplikacji. Zostanie też pokazane, jak zbudować program wykonujący działa-
nie arytmetyczne oraz aplikację obliczającą pierwiastki równania kwadratowego o pa-
rametrach wprowadzanych z klawiatury. Przedstawione zostaną również bardziej
nowoczesne klasy Scanner i Console. Na końcu lekcji znajdzie się rozwiązanie proble-
mu 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 wprowa-
dzane ze standardowego strumienia wejściowego. Tym sposobem było wczytanie linii
tekstu za pomocą metody readLine z klasy BufferedReader oraz dokonanie konwersji

b4ac40784ca6e05cededff5eda5a930f
b
280 Java. Praktyczny kurs

z wykorzystaniem metod z klas Integer, Double lub Float5. Istnieje również inna metoda:
użycie klasy StreamTokenizer. Klasa ta dzieli strumień wejściowy na jednostki leksy-
kalne, czyli tokeny. Przykładowo jeśli strumień zawiera dane:
123 test 18

to można w nim wyróżnić trzy tokeny: 123, test i 18. Po przetworzeniu otrzymamy dwie
liczby oraz ciąg znaków. To, jakie znaki będą separatorami oddzielającymi poszcze-
gólne jednostki leksykalne (w powyższym przykładzie są to spacje), można zdefinio-
wać samodzielnie, jednak dla dalszych ćwiczeń i w wielu typowych zastosowaniach
domyślne ustawienia są w zupełności wystarczające (to wszystkie tzw. białe znaki,
czyli spacje, tabulatory, znaki końca wiersza itp.).

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 rozpo-
znawanie sytuacji, kiedy token nie reprezentuje wartości liczbowej. W tym celu wystar-
czy 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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.  System wejścia-wyjścia 281

}
catch(IOException e){
System.out.print("Błąd podczas odczytu danych ze strumienia.");
return;
}

if(strTok.ttype != StreamTokenizer.TT_NUMBER){
System.out.print("To nie jest prawidłowa liczba.");
return;
}

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 prze-
ciwnym 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 ry-
sunku 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ż po-
dawane 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 poprzed-
nim przykładzie. Po wyświetleniu w bloku try prośby o wprowadzenie liczby rozpo-
czyna się jednak działanie pętli typu while. Działa ona tak długo, aż wartość zwrócona
przez wywołanie metody nextToken będzie równa TT_NUMBER. Dopóki więc użytkownik
nie poda prawidłowej liczby, będzie proszony o jej podanie i wywoływana będzie metoda
nextToken. Kiedy poprawna wartość liczbowa zostanie wprowadzona, pętla zakończy

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.  System wejścia-wyjścia 283

swoje działanie i wartość ta zostanie przypisana zmiennej liczba. Na ekranie jest na-
tomiast wyświetlany wynik mnożenia wartości zapisanej w zmiennej liczba przez 2.

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. pro-
gram obliczający pierwiastki równania kwadratowego o zadanych parametrach wprowa-
dzanych z klawiatury. Na pewno nie są to bardzo skomplikowane zadania programi-
styczne. 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;

b4ac40784ca6e05cededff5eda5a930f
b
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ła-
dowy 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 przedstawio-
ny 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");

b4ac40784ca6e05cededff5eda5a930f
b
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 A: ");
paramA = pobierzLiczbe(strTok);
System.out.print("Podaj parametr B: ");
paramB = pobierzLiczbe(strTok);
System.out.print("Podaj parametr C: ");
paramC = pobierzLiczbe(strTok);
System.out.println("");
}
catch(IOException e){
System.out.println("Błąd podczas odczytu strumienia.");
return;
}
oblicz(paramA, paramB, paramC);
}
public static void main (String args[]) {
Main main = new Main();
main.start();
}
}

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 meto-
da oblicz nie ma zapisanych na stałe parametrów A, B i C, tylko otrzymuje je w postaci
argumentów.

b4ac40784ca6e05cededff5eda5a930f
b
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ć praktycz-
nie 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 zdecydo-
wanie 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 wy-
mienione zmienne odzwierciedlają kolejne parametry równania kwadratowego. Wykony-
wanie kodu rozpocznie się tradycyjnie od metody main, w której tworzymy nowy obiekt
klasy Main i wywołujemy metodę start. O tym, że program rzeczywiście rozwiązuje
równania kwadratowe, powinien przekonać rysunek 6.9, który prezentuje rozwiązanie
równania o parametrach A = 2, B = -2 i C = -4, czyli równania o postaci: 2x2 + 2x – 4 = 0.

Rysunek 6.9.
Efekt działania
programu
rozwiązującego
równania kwadratowe

Klasa Scanner
W Java 5 pojawiła się nowa klasa, o nazwie Scanner, upraszczająca wiele operacji wczy-
tywania i przetwarzania danych. Zawiera ona konstruktory, które mogą przyjmować
obiekty klas: File, InputReader i String, a także obiekty implementujące interfejsy
Readable lub ReadableByteChannel. Jest to więc zestaw pozwalający na obsługę bardzo
wielu formatów wejściowych. Metod klasy Scanner jest bardzo wiele, nie ma potrzeby,
aby je wszystkie omawiać. Warto jednak wiedzieć, że najbardziej dla nas interesujące
rodziny next i hasNext są konstruowane na bardzo prostej zasadzie, którą schematycznie
można przedstawić jako:
hasNextNazwaTypuProstego

i
nextNazwaTypuProstego

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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.  System wejścia-wyjścia 287

leksykalna. Dodatkowo dostępna jest metoda hasNextLine, która określa, czy w stru-
mieniu znajduje się linia tekstu.

Metody z rodziny next, a więc nextInt, nextDouble, nextByte itd., zwracają natomiast
kolejną jednostkę leksykalną w postaci wartości danego typu prostego. Metoda next
zwraca zaś token w postaci ciągu znaków. Podobnie jak w przypadku opisanym w po-
przednim akapicie, istnieje również metoda nextLine, która zwraca całą linię tekstu.
Zobaczmy, jak wykorzystać to w praktyce: napiszmy program analogiczny do prezento-
wanego w poprzedniej części lekcji, ale korzystający z klasy Scanner. Będzie wczyty-
wał ze standardowego strumienia wejściowego wartość całkowitą i wyświetli rezultat
jej mnożenia przez 2. Kod realizujący takie zadanie został przedstawiony na listingu 6.10.

Listing 6.10.
import java.util.*;

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 war-
tość true, a więc wtedy, kiedy w strumieniu wejściowym pojawi się wartość, która będzie
mogła być zinterpretowana jako całkowita. Oczywiście w sytuacji, gdy hasNextInt
zwróci wartość false, wykonywane będzie wnętrze pętli, w którym wyświetlana jest in-
formacja o tym, że wprowadzono niepoprawną wartość. Wartość ta będzie pobierana
ze strumienia za pomocą metody next i w celach informacyjnych wyświetlana na ekranie:
System.out.println(scanner.next());

b4ac40784ca6e05cededff5eda5a930f
b
288 Java. Praktyczny kurs

Z kolei kiedy w strumieniu pojawi się wartość typu int, pętla zostanie zakończona,
a wczytana wartość zostanie pobrana za pomocą wywołania metody nextInt i zapisana
w zmiennej value:
int value = scanner.nextInt();

Następnie jest wykonywane mnożenie przez 2, a wynik tej operacji zostaje wyświetlony
na ekranie. Przykładowy efekt działania programu widać na rysunku 6.10.

Rysunek 6.10.
Wczytywanie wartości
za pomocą klasy Scanner

Klasa Console
Począwszy od Java 6 (1.6), w JDK dostępna jest klasa Console wyspecjalizowana w ob-
słudze konsoli (zawarta jest w pakiecie java.io). Dzięki niej możliwe jest pobranie od-
wołania do konsoli, na której operuje Java (o ile taka jest udostępniona; nie musi to być
też konsola systemowa), i pobieranie oraz wyświetlanie danych. Jest to klasa finalna
i dziedziczy bezpośrednio po Object. Aby uzyskać obiekt typu Console będący odwoła-
niem do bieżącej konsoli systemowej, nie wywołuje się bezpośrednio konstruktora,
ale metodę statyczną console z klasy System. To wywołanie ma postać:
System.console()

W efekcie zostanie uzyskany obiekt konsoli, za którego pośrednictwem będzie można wy-
konywać różne operacje, lub też wartość null, o ile konsola nie jest dostępna. Wspomnia-
ne operacje mogą być realizowane za pomocą metod przedstawionych w tabeli 6.3.
Prosty program pozwalający na wczytanie wiersza tekstu, a następnie wyświetlający
uzyskane dane na ekranie został przedstawiony na listingu 6.11.

Listing 6.11.
import java.io.Console;

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);
}
}
}

b4ac40784ca6e05cededff5eda5a930f
b
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 Wyświetla dane sformatowane za pomocą ciągu formatującego fmt.
fmt, Object… args)
Console printf(String Wyświetla dane sformatowane za pomocą ciągu formatującego format.
format, Object… args)
Reader reader() Zwraca powiązany z konsolą obiekt typu Reader (używany do odczytu
danych).
String readLine() Odczytuje wiersz tekstu.
String readLine(String Wyświetla tekst zachęty (prompt) sformatowany za pomocą ciągu fmt,
fmt, Object… args) a następnie wczytuje wiersz tekstu.
char[] readPassword() Odczytuje wiersz tekstu (hasło), nie wyświetlając wprowadzanych
z klawiatury znaków.
char[] readPassword Wyświetla tekst zachęty (prompt) sformatowany za pomocą ciągu fmt,
(String fmt, Object… a następnie wczytuje wiersz tekstu (hasło), nie wyświetlając
args) 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 zapi-
sywane w zmiennej con. Następnie badane jest, czy wartość con jest równa null (byłoby
tak, gdyby aplikacja nie miała dostępu do konsoli). Jeśli jest równa null, za pomocą
instrukcji System.out.println następuje próba podania komunikatu o błędzie i jest to
koniec działania aplikacji. W przeciwnym razie (blok else) wyświetlany jest tekst
z prośbą o wprowadzenie wiersza tekstu i wiersz ten jest wczytywany (wywołanie me-
tody readLine). Po odczytaniu danych są one ponownie wyświetlane za pomocą me-
tody printf.

Program z listingu 6.11 można w łatwy sposób przerobić, tak aby odczytywał dane w pętli
while aż do osiągnięcia pewnego ciągu znaków, np. quit. To jednak pozostanie ćwicze-
niem do samodzielnego wykonania. W tej części lekcji przyjrzymy się jeszcze użytej
na listingu metodzie printf. Pozwala ona na wyświetlanie danych w postaci sforma-
towanej według pewnego wzorca. Wystarczy w ciągu znaków będącym pierwszym
argumentem umieścić znaczniki, które zostaną zamienione na wartości pobrane z ko-
lejnych argumentów. Schematycznie wywołanie metody printf będzie miało zatem
postać (przy założeniu, że obiekt con reprezentuje konsolę):
con.printf("format", wartość1, wartość2, ..., wartośćN);

O tym, że mamy do czynienia ze znacznikiem, informuje znak %. Możliwe do zastoso-


wania 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.

b4ac40784ca6e05cededff5eda5a930f
b
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);

b4ac40784ca6e05cededff5eda5a930f
b
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ążo-
nych wersjach i umożliwiają wyświetlanie różnorodnych typów danych. Wszystkie
dostępne metody klasy PrintStream zostały zebrane w tabeli 6.5.

Tabela 6.5. Metody klasy PrintStream


Deklaracja Opis
PrintStream append(char c) Dodaje znak do strumienia.
PrintStream Dodaje do strumienia sekwencję znaków.
append(CharSequence csq)
PrintStream Dodaje do strumienia część sekwencji znaków wyznaczaną
append(CharSequence csq, przez indeksy start i end.
int start, int 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, Zapisuje w strumieniu dane określone przez argumenty
String format, Object… args) args w formacie zdefiniowanym przez format, zgodnie
z ustawieniami narodowymi wskazanymi przez locale.
PrintStream format(String Zapisuje w strumieniu dane określone przez argumenty args
format, Object… 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.

b4ac40784ca6e05cededff5eda5a930f
b
292 Java. Praktyczny kurs

Tabela 6.5. Metody klasy PrintStream (ciąg dalszy)


Deklaracja Opis
PrintStream printf(Locale l, Zapisuje w strumieniu dane określone przez argumenty args
String format, Object… args) w formacie zdefiniowanym przez format, zgodnie z ustawieniami
narodowymi wskazanymi przez locale.
PrintStream printf(String Zapisuje w strumieniu dane określone przez argumenty args
format, Object… 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, Zapisuje do strumienia liczbę bajtów wskazywaną przez len,
int off, int 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 środowi-
sku 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 (Win-
dows 1250)11, Java natomiast przechowuje je w standardzie Unicode i tak też są one

10
W niektórych wersjach JDK 1.5 znaki na konsoli były standardowo wyświetlane zgodnie
z kodowaniem CP852, nie było więc potrzeby wykonywania opisywanych czynności.
11
Chyba że został użyty edytor pozwalający na zapis w innym kodowaniu, np. UTF-8, i to ono zostało
wybrane jako standard zapisu.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.  System wejścia-wyjścia 293

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 (me-
tody 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 kon-
struktora klasy PrintWriter jest nowo utworzony obiekt klasy OutputStreamWriter po-
wiązany ze standardowym strumieniem wyjściowym reprezentowanym przez System.out
i wykorzystujący stronę kodową 852. Należy zwrócić uwagę na drugi parametr równy
true, który będzie wymuszać wypchnięcie danych do strumienia po wykonaniu każdej
instrukcji print lub println. Niezastosowanie go będzie skutkowało przetrzymywaniem
danych w buforze i nie będą się one prawidłowo pojawiały na ekranie14.

Warto zwrócić również uwagę na to, co się dzieje, kiedy wystąpi wyjątek Unsupported
EncodingException. Otóż w takiej sytuacji nie można pozwolić na to, aby zmienna outp
nie została zainicjowana, gdyż doprowadziłoby to do błędów w dalszej części programu
(niemożność wykorzystania obiektu outp). W związku z tym ponownie konstruujemy
obiekt klasy PrintWriter, tym razem nie wymuszając zastosowania strony kodowej 852.

Kiedy utworzymy już obiekt outp, można używać go zamiast standardowego System.out15,
tak jak jest to widoczne w kolejnych instrukcjach, w których dokonujemy porównania
obu sposobów wyświetlania polskich znaków. Efekt działania programu jest widoczny
na rysunku 6.11.

Rysunek 6.11.
Efekt użycia klas
PrintWriter
i OutputStreamWriter
do wyświetlenia
polskich znaków

14
W praktyce może się jednak okazać konieczne wywoływanie metody flush po każdej instrukcji
print lub println (patrz również rozwiązanie ćwiczenia 32.4).
15
Oczywiście outp korzysta ze strumienia System.out, wcześniej jednak dokonuje przekodowania
znaków zgodnego z wybraną stroną kodową.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.  System wejścia-wyjścia 295

Ćwiczenia do samodzielnego wykonania

Ćwiczenie 32.1.
Zmodyfikuj program z listingu 6.7 tak, aby poprawnie reagował (tzn. nie wyświetlał ko-
lejnej prośby o wprowadzenie liczby) na zakończenie pracy aplikacji przez naciśnięcie
klawiszy Ctrl+C (jest to sytuacja równoznaczna z osiągnięciem końca strumienia).

Ćwiczenie 32.2.
Napisz program, który będzie wczytywał ze standardowego wejścia tekst. Po wprowa-
dzeniu każdej linii program powinien wyświetlić oddzielone od siebie słowa, z których
się ona składa. Koniec pracy ma nastąpić, kiedy w tekście pojawi się słowo quit lub
zostanie wciśnięta kombinacja klawiszy Ctrl+C.

Ćwiczenie 32.3.
Napisz program umożliwiający użytkownikowi wykonywanie na dwóch liczbach czterech
podstawowych działań arytmetycznych: dodawania, odejmowania, mnożenia i dzielenia.

Ćwiczenie 32.4.
Napisz program, który za pomocą klasy Console będzie wczytywał wiersze tekstu i od
razu wyświetlał wczytywane dane na ekranie wraz z numerem wprowadzonego wiersza.
Odczyt danych ma się zakończyć po wprowadzeniu ciągu quit.

Ćwiczenie 32.5.
Zmodyfikuj program z listingu 6.9 w taki sposób, aby wyświetlał znaki na ekranie
przy użyciu strony kodowej 852.

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 te-
mató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.

b4ac40784ca6e05cededff5eda5a930f
b
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 Porównuje ścieżki dostępu do plików. 1.2
pathname)
boolean createNewFile() Jeśli nie istnieje plik określony przez bieżący obiekt File, 1.2
tworzy go.
static File create Tworzy pusty plik tymczasowy w systemowym katalogu 1.2
TempFile(String przeznaczonym dla plików tymczasowych. Nazwa powstaje
prefix, String suffix) przy wykorzystaniu ciągów przekazanych w parametrach
prefix i suffix.
static File Podobnie jak dwie poprzednie metody, tworzy plik 1.2
createTempFile(String tymczasowy, zostaje on jednak umieszczony w katalogu
prefix, String suffix, wskazywanym przez argument directory.
File directory)
boolean delete() Usuwa plik lub katalog (jeśli polecenie dotyczy katalogu, 1.0
musi on być pusty).
void deleteOnExit() Zaznacza, że plik ma zostać usunięty, kiedy maszyna 1.2
wirtualna będzie kończyć pracę.
boolean equals(Object Zwraca true, jeśli obiekt obj reprezentuje taką samą 1.0
obj) ścieżkę dostępu.
boolean exists() Zwraca true, jeśli plik lub katalog istnieje. 1.0
File getAbsoluteFile() Zwraca obiekt zawierający bezwzględną nazwę pliku lub 1.2
katalogu (wraz z pełną ścieżką dostępu).
String getAbsolutePath() Zwraca bezwzględną ścieżkę dostępu do pliku lub 1.0
katalogu (bez nazwy pliku).
File getCanonicalFile() Zwraca obiekt zawierający kanoniczną postać nazwy 1.2
pliku lub katalogu (wraz z pełną ścieżką dostępu).
String Zwraca kanoniczną postać ścieżki dostępu do pliku 1.1
getCanonicalPath() lub katalogu.
long getFreeSpace() Zwraca ilość wolnego miejsca na partycji, na której 1.6
znajduje się plik lub katalog.
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 1.0
obiektu typu String.
long getTotalSpace() Zwraca całkowitą ilość miejsca na partycji, na której 1.6
znajduje się plik lub katalog.
long getUsableSpace() Zwraca dostępną dla danej maszyny wirtualnej ilość 1.6
miejsca na partycji, na której znajduje się plik lub katalog.
int hashCode() Oblicza wartość funkcji skrótu dla danej ścieżki dostępu. 1.0

b4ac40784ca6e05cededff5eda5a930f
b
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ą 1.0
bezwzględną.
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 1.2
katalog lub plik.
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 1.0
typu String.
String[] list Zwraca listę plików i podkatalogów spełniających 1.0
(FilenameFilter filter) kryteria wskazane przez argument filter.
File[] listFiles() Zwraca zawartość katalogu w postaci tablicy obiektów 1.2
typu File.
File[] listFiles Zwraca w postaci tablicy obiektów typu File zawartość 1.2
(FileFilter filter) katalogu spełniającą kryteria wskazane przez filter.
File[] listFiles Zwraca w postaci tablicy obiektów typu File zawartość 1.2
(FilenameFilter filter) katalogu spełniającą kryteria wskazane przez filter.
static File[] Wyświetla wszystkie „korzenie” (węzły główne) systemu 1.2
listRoots() plików.
boolean mkdir() Tworzy nowy katalog. 1.0
boolean mkdirs() Tworzy nowy katalog z uwzględnieniem nieistniejących 1.0
katalogów nadrzędnych.
boolean renameTo Zmienia nazwę pliku lub katalogu na wskazywaną przez 1.0
(File dest) argument dest.
boolean setExecutable Ustawia prawo (atrybut) wykonywalności (executable) 1.6
(boolean executable) katalogu lub pliku (dla właściciela obiektu).
boolean setExecutable Ustawia prawo (atrybut) wykonywalności (executable) 1.6
(boolean executable, pliku lub katalogu dla właściciela lub wszystkich
boolean ownerOnly) użytkowników.
boolean setLastModified Ustawia datę ostatniej modyfikacji pliku lub katalogu. 1.2
(long time)
boolean setReadable Ustawia prawo (atrybut) do odczytu (readable) katalogu 1.6
(boolean readable) lub pliku (dla właściciela obiektu).
boolean setReadable Ustawia prawo (atrybut) do odczytu (readable) pliku lub 1.6
(boolean readable, katalogu dla właściciela lub wszystkich użytkowników.
boolean ownerOnly)
boolean setReadOnly() Ustawia atrybut ReadOnly pliku lub katalogu. 1.2
boolean setWriteable Ustawia prawo (atrybut) do odczytu (writeable) katalogu 1.6
(boolean readable) lub pliku (dla właściciela obiektu).

b4ac40784ca6e05cededff5eda5a930f
b
298 Java. Praktyczny kurs

Tabela 6.6. Metody udostępniane przez klasę File (ciąg dalszy)


Deklaracja metody Opis Wersja JDK
boolean setWriteable Ustawia prawo (atrybut) do zapisu (writeable) do pliku 1.6
(boolean readable, lub katalogu dla właściciela lub wszystkich użytkowników.
boolean ownerOnly)
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 1.4
na zunifikowany identyfikator zasobów — obiekt typu URI.
URL toURL() Konwertuje ścieżkę dostępu reprezentowaną przez obiekt 1.2
File na obiekt klasy URL. Metoda przestarzała, o ile to
możliwe, nie należy z niej korzystać.

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 obrazu-
ją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 katalo-
gu, którego zawartość chcemy wylistować — jest to katalog bieżący oznaczony jako .,
można też użyć zapisu ./. Format, w jakim podaje się nazwę ścieżki dostępu, jest za-
leżny od konwencji stosowanej w danym systemie operacyjnym, klasa File sama radzi
sobie z rozkodowaniem takiego zapisu. Następnie deklarujemy zmienną dirList i przypi-
sujemy jej obiekt zwrócony przez wywołanie metody list. Ponieważ ten obiekt to w rze-
czywistości tablica obiektów klasy String, pozostaje zastosowanie pętli for do odczytania
jego zawartości i wyświetlenia jej na ekranie. Przykładowy efekt wykonania kodu z listin-
gu 6.14 jest widoczny na rysunku 6.12.

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 tyl-
ko 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ć.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.  System wejścia-wyjścia 299

Rysunek 6.12.
Wyświetlenie zawartości
katalogu przy użyciu
metody list klasy File

W rzeczywistości FilenameFilter to interfejs, w którym zadeklarowano tylko jedną me-


todę: 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 przed-
stawiony 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 zastoso-


wanie dodatkowej klasy MyFilenameFilter17, przeanalizujmy jednak wszystko po kolei,
począwszy od kodu klasy Main. Zaczynamy od sprawdzenia, czy podczas wywołania zo-
stał podany przynajmniej jeden argument (por. lekcja 14.). Jeśli nie, należy poinfor-
mować 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 za-
wiera 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 ta-


blica 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 wyko-
nania 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 rzutowa-
nie 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), przekaza-
nie 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.

b4ac40784ca6e05cededff5eda5a930f
b
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. Implementa-
cja 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 na-
szym przypadku będzie to metoda accept z klasy MyFilenameFilter. Metoda ta powinna
zwrócić wartość true, o ile dany plik lub katalog ma być uwzględniony w listingu,
lub false w przeciwnym przypadku. Trzeba zatem tak skonstruować tę metodę, aby
sprawdzała, czy nazwa pliku pasuje do wyrażenia regularnego. Pomogą nam w tym
klasy z pakietu java.util.regex, stworzone specjalnie do operowania na wyrażeniach
regularnych.

Klasa Pattern reprezentuje wyrażenie regularne, w klasie MyFilenameFilter umieści-


liśmy zatem pole tego typu. Obiekt klasy Pattern jest tworzony w konstruktorze po-
przez wykorzystanie statycznej metody compile. Zwraca ona obiekt klasy Pattern ob-
sł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.

b4ac40784ca6e05cededff5eda5a930f
b
302 Java. Praktyczny kurs

Przykładowy efekt działania tego programu został przedstawiony na rysunku 6.13. Pierw-
szy znak . określa katalog bieżący, a użyte wyrażenie regularne ^M.*\.java$ oznacza
ciągi znaków (nazwy plików) zaczynające się (^) od dużej litery M, po której następuje
dowolna liczba dowolnych znaków (.*), a nazwa jest zakończona ($) ciągiem .java20.
Program działa zatem zgodnie z założeniami, choć zawiera pewien błąd. Jego odnalezie-
nie i poprawienie pozostanie jednak ćwiczeniem do samodzielnego wykonania (podpo-
wiedź: co się stanie, jeśli w wywołaniu zostanie użyta nazwa nieistniejącego katalogu?).

Rysunek 6.13.
Efekt działania
programu listującego
zawartość katalogu

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 strumie-
niowych, czym zajmiemy się w lekcji 34., niemniej warto znać i ten sposób. Aby skorzy-
stać z createNewFile, należy najpierw utworzyć obiekt klasy File, podając w konstrukto-
rze 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ą.

b4ac40784ca6e05cededff5eda5a930f
b
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 poda-
na. Jeśli nie, pozostaje wyświetlić stosowny komunikat i zakończyć pracę, wykorzy-
stując instrukcję return. Jeśli tak, należy utworzyć obiekt file klasy File poprzez
przekazanie konstruktorowi pierwszego argumentu z wiersza poleceń oraz wywołać
metodę createNewFile. Jeśli plik nie istniał i został poprawnie utworzony, metoda ta
zwraca wartość true, natomiast jeżeli plik o wskazanej nazwie istniał wcześniej, zwracana
jest wartość false, a próba utworzenia nie jest podejmowana. Jeśli plik nie istniał, ale
próba jego utworzenia się nie powiodła, generowany jest wyjątek IOException. Może
tak się stać np. w sytuacji, kiedy podamy nieistniejącą ścieżkę dostępu.

Równie ważne jak tworzenie plików jest tworzenie katalogów, w których te pierwsze
mogą być umieszczane. Umożliwiają to dwie metody klasy File: mkdir oraz mkdirs.
Różnica między nimi jest taka, że w przypadku metody mkdir musi istnieć pełna
ścieżka dostępu, natomiast mkdirs potrafi utworzyć niezbędne katalogi nadrzędne. To
znaczy, że jeśli istnieje przykładowy katalog /java/test/, to próba utworzenia katalogu
/java/test/files/source przy użyciu mkdir zakończy się niepowodzeniem (nie istnieje
bowiem ścieżka /java/test/files/), natomiast przy wykorzystaniu mkdirs zakończy się
sukcesem (zostanie bowiem utworzony również brakujący podkatalog files). Obie metody
zwracają wartość true, gdy próba utworzenia katalogu zakończyła się powodzeniem,
oraz false w przeciwnym wypadku. Oznacza to, że w sytuacji, kiedy katalog istnieje
i podjęta zostanie próba jego utworzenia, w wyniku otrzymamy wartość false (mimo
że katalog jest na dysku). Warto więc przed wywołaniem mkdir lub mkdirs sprawdzać, czy
katalog istnieje, za pomocą metody exists. Ten sposób został zastosowany w programie
przedstawionym na listingu 6.17.

Listing 6.17.
import java.io.*;

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()){

b4ac40784ca6e05cededff5eda5a930f
b
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ć skonstru-
owany inaczej. Można zastosować w tym celu technikę rekurencji, czyli wywoływania

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.  System wejścia-wyjścia 305

funkcji przez samą siebie22. Kod realizujący takie zadanie został przedstawiony na li-
stingu 6.19.

Listing 6.19.
import java.io.*;

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ł prze-
kazany przynajmniej jeden argument, utworzeniem obiektu klasy Delete oraz wywoła-
niem jego metody delete. Właściwa funkcjonalność, czyli procedury usuwające pliki
i katalogi, zawiera się właśnie w metodzie delete. Jako argument otrzymuje ona na-
zwę katalogu, który ma zostać usunięty. Wewnątrz tworzymy nowy obiekt klasy File
i pobieramy listę obiektów znajdujących się w tym katalogu. Tym razem, inaczej niż
we wcześniejszych przykładach, wykorzystujemy metodę listFiles, która zwraca tablicę
obiektów klasy File.

W pętli for sprawdzamy po kolei obiekty zawarte w tablicy dirList. Jeżeli dany obiekt
jest katalogiem, wywołujemy rekurencyjnie funkcję delete, przekazując jej jako argument
jego nazwę (uzyskaną dzięki wywołaniu metody getPath, patrz też tabela 6.6), czyli
usuwamy ten podkatalog wraz z jego zawartością (delete(dirList[i].getPath())).
Niezależnie jednak od tego, czy mamy do czynienia z plikiem, czy z katalogiem, wy-

22
Osoby nieznające technik rekurencyjnych powinny zapoznać się z dowolną książką opisującą podstawy
algorytmów.

b4ac40784ca6e05cededff5eda5a930f
b
306 Java. Praktyczny kurs

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 prze-
kazanej w postaci argumentu w taki sposób, że najpierw pojawi się ponumerowana lista
plików, a następnie ponumerowana lista katalogów. Pliki i katalogi powinny być nume-
rowane osobno.

Ćwiczenie 33.2.
Zmodyfikuj kod klas z listingu 6.15 w taki sposób, aby w przypadku, kiedy podane
w wywołaniu wyrażenie regularne nie jest prawidłowe, program nie zgłaszał błędu,
ale też nie wyświetlał żadnych plików. W klasie Main nie może występować instrukcja
try przechwytująca wyjątek PatternSyntaxException.

Ćwiczenie 33.3.
Zmodyfikuj program z listingu 6.19 tak, aby po wykonaniu operacji usuwania wyświetlił
liczbę faktycznie usuniętych plików oraz katalogów.

Ćwiczenie 33.4.
Zmodyfikuj program z listingu 6.19 tak, by usuwał jedynie pliki, pozostawiając niena-
ruszoną strukturę katalogów.

Ćwiczenie 33.5.
Zmodyfikuj program z listingu 6.19 tak, aby nie było konieczności tworzenia obiektu
klasy Delete.

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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.  System wejścia-wyjścia 307

Klasa RandomAccessFile
Klasa RandomAccessFile daje możliwość wykonywania wszelkich operacji na plikach
o dostępie swobodnym. Pozwala na odczytywanie i zapisywanie danych w pliku oraz
przemieszczanie się po pliku. Jest dostępna we wszystkich JDK, począwszy od JDK 1.0.
Metody udostępniane przez RandomAccessFile są zebrane w tabeli 6.7.

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 1.0
tablicy b i umieszcza je w tej tablicy. Zwraca rzeczywiście
odczytaną liczbę bajtów.
int read(byte[] b, Odczytuje z pliku liczbę bajtów nie większą niż wskazywana 1.0
int off, int len) przez len i zapisuje je w tablicy b, począwszy od komórki
wskazywanej przez off. Zwraca faktycznie przeczytaną
liczbę bajtów.
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, Odczytuje z pliku liczbę bajtów wskazywaną przez len i zapisuje 1.0
int off, int len) 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 Ustawia rozmiar pliku na newLength. 1.2
(long newLength)
int skipBytes(int n) Pomija n bajtów. 1.0

b4ac40784ca6e05cededff5eda5a930f
b
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, Zapisuje w pliku len bajtów z tablicy b, począwszy od komórki 1.0
int off, int len) wskazywanej przez off.
void write(int b) Zapisuje bajt b w pliku. 1.0
void writeBoolean Zapisuje w pliku wartość boolean w postaci jednego bajtu. 1.0
(boolean v)
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 1.0
znaków.
void writeDouble(double v) Konwertuje wartość v na typ long, korzystając z metody 1.0
doubleToLongBits z klasy Double, i tak powstałą wartość
zapisuje do pliku (8 bajtów).
void writeFloat(float v) Konwertuje wartość v na typ int, korzystając z metody 1.0
floatToLongBits z klasy Float, i tak powstałą wartość
zapisuje w pliku (4 bajty).
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 Zapisuje w pliku ciąg znaków s w kodowaniu UTF-8. 1.0
str)

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;

b4ac40784ca6e05cededff5eda5a930f
b
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 wy-
kracza 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ł zapre-
zentowany 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.");
}
}
}

b4ac40784ca6e05cededff5eda5a930f
b
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 przy-
pisany zmiennej line i ostatecznie zostaje wykonane sprawdzenie, czy line jest różne od
null. Porównanie wartości zwróconej przez readLine z null jest konieczne do stwierdze-
nia, że został osiągnięty koniec pliku. W środku pętli znajduje się tylko jedna instrukcja,
wyświetlająca wartość przypisaną line na ekranie. Po zakończeniu pętli zamykamy stru-
mień, wywołując metodę close. Całość jest ujęta w blok try, gdyż podczas odczytu
może wystąpić wyjątek IOException.

Zapis danych do pliku


Klasa RandomAccessFile udostępnia również wiele funkcji pozwalających na zapis różne-
go 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 po-
znać sposób, w jaki dokonywany jest zapis. Napiszemy program, który będzie odczy-
tywał ze standardowego wejścia dane wprowadzane przez użytkownika i zapisywał je
w pliku tekstowym. Nazwę pliku będzie można podać w wywołaniu programu lub też
wprowadzić w trakcie jego wykonania. Kod realizujący przedstawione zadanie jest
widoczny na listingu 6.21.

Listing 6.21.
import java.io.*;

public class Main {

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.  System wejścia-wyjścia 311

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;
}
}
}

b4ac40784ca6e05cededff5eda5a930f
b
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 wykonywa-
nie 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ę utwo-
rzyć 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 przy-
pisana 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, zapi-
sujemy 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 po-
winny to być dwa znaki: \r\n. Po zakończeniu pętli zamykamy strumień (plik), wyko-
rzystują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ło-
wego, drugi dla pliku docelowego, a następnie w pętli odczytywać zawartość pierwszego
i zapisywać w drugim. Wykonajmy zatem taki przykład. Nazwy plików źródłowego i do-
celowego będą podawane przy wywołaniu programu. Klasę realizującą to zadanie na-
zwijmy Kopiuj; jej kod jest widoczny na listingu 6.22.

Listing 6.22.
import java.io.*;

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;
}

b4ac40784ca6e05cededff5eda5a930f
b
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 źródłowy nie istnieje.");
return;
}
if(destFile.exists()){
System.out.println("Plik docelowy istnieje.");
return;
}
RandomAccessFile rafSource = null;
RandomAccessFile rafDest = null;

try{
rafSource = new RandomAccessFile(sourceFile, "r");
rafDest = new RandomAccessFile(destFile, "rw");
}
catch(FileNotFoundException e){
System.out.println("Błąd podczas otwierania plików: " + e);
return;
}

int b;

try{
while((b = rafSource.read()) != -1){
rafDest.write(b);
}
rafSource.close();
rafDest.close();
}
catch(IOException e){
System.out.print("\nBłąd wejścia-wyjścia podczas kopiowania pliku.");
return;
}
}
}

Tworzymy obiekty sourceFile oraz destfile, wykorzystując parametry przekazane


aplikacji. Obiekt reprezentowany przez sourceFile będzie powiązany z plikiem źródło-
wym, natomiast ten wskazywany przez destfile — z plikiem docelowym. Sprawdza-
my 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 od-
czytu, 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

b4ac40784ca6e05cededff5eda5a930f
b
314 Java. Praktyczny kurs

zasygnalizować koniec pliku, metoda read zwraca wtedy wartość –1. Gdyby wartość
zwracana była typu byte, nie byłoby to możliwe i koniec pliku musiałby być sygnali-
zowany wystąpieniem wyjątku bądź w inny, mniej wygodny sposób.

Po tych wyjaśnieniach znaczenie wyrażenia warunkowego pętli while powinno być jasne.
Jest to wyrażenie złożone, podobnie jak w przypadku przykładu z listingu 6.20. Instruk-
cja ta odczytuje kolejne bajty z pliku źródłowego i przypisuje je zmiennej b tak długo,
aż metoda read zwróci wartość –1, czyli zostanie osiągnięty koniec pliku. Wewnątrz
pętli while za pomocą metody write zapisuje się każdy odczytany bajt w docelowym pli-
ku wskazywanym przez rafDest. Po wykonaniu kopiowania należy zamknąć oba pliki
(czy też dokładniej: strumienie powiązane z plikami), wywołując metodę close.

Przedstawiona metoda kopiowania jest najprostsza w realizacji, jednak nadaje się jedy-
nie do kopiowania plików o niewielkich rozmiarach. Ponieważ klasa RandomAccessFile
tworzy niebuforowany strumień operujący na pliku, czytanie i zapis bajt po bajcie jest
metodą bardzo nieefektywną (łatwo się o tym przekonać, próbując kopiować za pomocą
przedstawionego programu pliki o dużych rozmiarach). Rozwiązaniem jest wykorzy-
stanie buforowanych klas strumieniowych (zostanie to pokazane w dalszej części lekcji)
lub samodzielna obsługa buforowania. Wystarczy przecież umieścić w utworzonej
klasie niewielki bufor i skorzystać z metod czytających naraz większą ilość danych. Jak
wynika z danych zawartych w tabeli 6.7, takie metody są dostępne w klasie RandomAccess
File. Program wykonujący kopiowanie plików, korzystający z własnego bufora,
został przedstawiony na listingu 6.23.

Listing 6.23.
import java.io.*;

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;
}
}
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 6.  System wejścia-wyjścia 315

Początek kodu jest taki sam jak w przykładzie z listingu 6.20, nie ma zatem potrzeby,
aby go powtórnie omawiać. Zmienia się natomiast metoda kopiowania danych. Przede
wszystkim zostają zadeklarowane dodatkowe zmienne:
 count — przechowująca liczbę odczytanych bajtów;
 buffSize — przechowująca wielkość bufora;
 buff — tablica typu byte będąca buforem danych.

W pętli while za pomocą metody read jest odczytywana porcja danych. Zostaną one zapi-
sane w tablicy buff. Liczba odczytanych bajtów zostanie zwrócona przez tę metodę
i zapisana w zmiennej count. Jeżeli będzie to wartość –1, będzie to oznaczało osiągnięcie
końca pliku. Po każdym odczytaniu dane są zapisywane w pliku docelowym za pomocą
metody write. Metodzie tej przekazuje się trzy parametry, pierwszy to bufor (czyli
tablica bajtów), drugi to indeks komórki tablicy, od której rozpocznie się zapis, trzeci to
liczba bajtów do zapisania.

W celach testowych dobrze byłoby teraz uruchomić obie wersje programu (z listingu
6.22 i 6.23) i wykonać kopiowanie jakiegoś dużego pliku (rzędu kilku – kilkunastu
megabajtów). Różnica w czasie wykonania będzie aż nadto widoczna. Ciekawym ekspe-
rymentem jest również zmienianie wielkości bufora i obserwowanie czasu wykonania
programu.

W tym miejscu warto jeszcze zwrócić uwagę na sposób działania wykorzystanej w pro-
gramie metody read. Liczba odczytanych bajtów może wahać się od jednego do wiel-
kości tablicy będącej buforem, nigdy nie wolno zatem zakładać, że bufor jest za każ-
dym razem zapełniany do końca. Nie jest więc możliwe zastosowanie konstrukcji:
while(rafSource.read(buff) != -1){
rafDest.write(buff, 0, buff.length);
}

W tym przypadku błąd jest raczej oczywisty. Przecież plik nie musi mieć rozmiaru będą-
cego wielokrotnością rozmiaru bufora, a takie założenie zostało w tym fragmencie
uczynione. Błąd zostanie też szybko wykryty, gdyż plik źródłowy i docelowy praktycznie
w każdym wypadku będą się różnić. Dużo groźniejsze jest natomiast poprawienie tego
błędu w sposób następujący:
while((count = rafSource.read(buff)) != -1){
rafDest.write(buff, 0, count);
if(count != buffSize) break;
}

lub:
while(true){
count = rafSource.read(buff);
if(count != -1) rafDest.write(buff, 0, count);
if(count != buffSize) break;
}

Oba powyższe przykłady są niepoprawne, gdyż opierają się na założeniu, że liczba od-
czytanych bajtów jest zawsze równa wielkości bufora, a jedynie przy ostatnim odczycie
(przy samym końcu) może być mniejsza (ze względu na to, że rozmiar pliku nie jest

b4ac40784ca6e05cededff5eda5a930f
b
316 Java. Praktyczny kurs

wielokrotnością rozmiaru bufora). W większości wypadków (szczególnie kiedy ope-


ruje 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 prze-
dział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, pod-
czas gdy tradycyjne operacje strumieniowe są rozdzielone. Inne klasy obsługują strumie-
nie 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 Wersja
JDK
int read() Odczytuje jeden bajt danych. 1.0
int read(byte[] b) Odczytuje liczbę bajtów nie większą od długości tablicy b 1.0
i zapisuje je w tablicy b. Zwraca rzeczywiście odczytaną
liczbę bajtów.
int read(byte[] b, Odczytuje liczbę bajtów nie większą niż wskazywana przez len 1.0
int off, int len) i zapisuje je w tablicy b, począwszy od komórki wskazywanej
przez off. Zwraca rzeczywiście przeczytaną liczbę bajtów.

b4ac40784ca6e05cededff5eda5a930f
b
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[] Odczytuje liczbę znaków nie większą niż wskazywana przez length 1.0
cbuf, int offset, i zapisuje je w tablicy cbuf, począwszy od komórki wskazywanej
int length) przez offset. Zwraca rzeczywiście przeczytaną liczbę znaków.

Metod tych można używać bezpośrednio lub też wykorzystać obiekty klas FileReader
i FileInputStream jako argumenty dla konstruktorów klas dających większą funkcjonal-
ność, np. BufferedInputStream lub BufferedReader. Ta technika jest już znana z przykła-
dów obrazujących obsługę standardowego strumienia wejściowego. Napiszmy więc przy-
kładowy program, który za pomocą wymienionych klas będzie odczytywał linie tekstu
z pliku i wypisywał je na ekranie. Tak działający program przedstawiono na listingu 6.24.

Listing 6.24.
import java.io.*;

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

b4ac40784ca6e05cededff5eda5a930f
b
318 Java. Praktyczny kurs

RandomAccessFile. Obiekt brIn jest tworzony przez kaskadowe (podobnie jak w przy-
kładach z lekcji 31.) wywołanie konstruktorów klasy BufferedReader oraz FileReader.
Konstruktorowi klasy FileReader jest przekazywany argument odczytany z wiersza pole-
ceń. Jeśli plik o przekazanej nazwie nie istnieje, zostanie wygenerowany wyjątek FileNot
FoundException, dlatego też cała konstrukcja jest ujęta w blok try. Odczyt danych
odbywa się w pętli while, instrukcja readLine obiektu brIn jest wywoływana tak długo, aż
zwróci wartości null, co oznaczać będzie osiągnięcie końca pliku. Każda odczytana
linia jest zapisywana w zmiennej line, a następnie wyświetlana na ekranie za pomocą
instrukcji System.out.println.

Zapis do pliku
Dane zapisuje się w plikach, wykorzystując klasy obsługujące strumienie wyjściowe.
Podobnie jak w przypadku odczytu, do dyspozycji są dwie podstawowe klasy: FileWriter
oraz FileOutputStream. Pierwsza z nich powinna być wykorzystywana do zapisu pli-
ków tekstowych, druga do zapisu strumieni binarnych. Klasa FileOutputStream oferuje
trzy metody służące do zapisu danych (te metody zebrano w tabeli 6.10). Klasa File
Writer dysponuje również trzema metodami zapisującymi. Wszystkie one zostały
odziedziczone po klasie nadrzędnej, OutputStreamWriter (zestawiono je w tabeli 6.11).

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, Zapisuje w strumieniu len bajtów z tablicy b, począwszy 1.0
int off, int len) od komórki wskazywanej przez off.
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, Zapisuje w strumieniu len znaków z tablicy cbuf, 1.0
int off, int len) począwszy od komórki wskazywanej przez off.
void write(int c) Zapisuje w strumieniu znak c. 1.0
void write(String str, Zapisuje w strumieniu len znaków z obiektu str, 1.0
int off, int len) począwszy od znaku o indeksie wskazywanym przez off.

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.

b4ac40784ca6e05cededff5eda5a930f
b
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 ar-
gument ustawiony na true oznacza, że dane mają być dopisywane na końcu pliku, a usta-
wiony 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 na-
stępujących sytuacjach:
 podana nazwa wskazuje na katalog, a nie na plik;
 podany plik nie istnieje i nie można go również utworzyć;
 nie można z jakiegoś powodu otworzyć istniejącego pliku.

Powyższe wiadomości pozwalają już bez problemu napisać program, który będzie wczy-
tywał dane ze standardowego wejścia i zapisywał je do pliku. Taki program jest przedsta-
wiony na listingu 6.25.

Listing 6.25.
import java.io.*;

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();

b4ac40784ca6e05cededff5eda5a930f
b
320 Java. Praktyczny kurs

}
catch(IOException e){
System.out.print("\nBłąd wejścia-wyjścia.");
return;
}
}
}

Za odczyt danych odpowiada obiekt wskazywany przez zmienną brIn klasy Buffered
Reader, za zapis — obiekt fileWriter klasy FileWriter. Obiekt zapisujący dane jest
tworzony w taki sposób, że jeśli plik istnieje, dane będą dopisywane na jego końcu
(drugi argument konstruktora równy true). W pętli while za pomocą metody readLine
obiektu brIn odczytujemy ze standardowego wejścia kolejne linie tekstu. Sprawdzamy,
czy zwrócona wartość jest równa quit, co by oznaczało, że użytkownik zakończył
wprowadzanie danych, oraz czy jest ona równa null, co wskazuje na osiągnięcie końca
strumienia (if("quit".equals(line) || line == null)). Osiągnięcie końca strumienia
w przypadku standardowego strumienia wejściowego oznacza zwykle sytuację niepra-
widłową, np. niespodziewane przerwanie działania programu przez użytkownika.

Odczytany tekst znajdzie się w zmiennej line. Dodajemy do niej znak końca linii (znak
ten jest usuwany przez metodę readLine podczas odczytu, nie ma go zatem w zmiennej
line) oraz wywołujemy metodę write obiektu fileWriter. Tym samym zapisujemy
odczytaną linię tekstu w pliku. Metoda write wymaga podania parametrów określających,
jaką część ciągu znaków chcemy zapisać. Ponieważ zapisana ma być cała zawartość
zmiennej line, pierwszym parametrem jest 0 (indeks pierwszego znaku, który ma zna-
leźć się w strumieniu), natomiast drugim — długość ciągu pobierana przez wywołanie
metody length (liczba znaków, które mają znaleźć się w strumieniu).

Kopiowanie plików
Skoro wiadomo już, jak zapisywać i odczytywać pliki, można napisać strumieniową
wersję programu wykonującego operację kopiowania. Tym razem zarówno strumień
wejściowy, jak i wyjściowy będą strumieniami buforowanymi. Wykorzystamy klasy
operujące na strumieniach binarnych BufferedOutputStream oraz BufferedInputStream.
Kod realizujący to zadanie jest widoczny na listingu 6.26.

Listing 6.26.
import java.io.*;

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()){

b4ac40784ca6e05cededff5eda5a930f
b
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 wy-
korzystuje 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 poszcze-
gólne wiersze będą ponumerowane. Numeracja powinna zaczynać się od 1 lub od wartości
przekazanej jako parametr wywołania programu.

Ćwiczenie 34.5.
Napisz program, który będzie sumował liczby zapisane w pliku tekstowym i wyświetlał
wynik na ekranie. Nazwa pliku z danymi do sumowania powinna być podana w wierszu
poleceń jako argument wywołania aplikacji.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7.
Kontenery i typy
uogólnione
W każdym programie trzeba przechowywać najrozmaitsze dane, wykorzystując w tym
celu różne struktury, takie jak tablice, listy czy stosy. Java wspomaga programistę
w realizacji tego typu zadań, oferując cały zestaw klas użytkowych mogących służyć za
kontenery na dane. Rozdział 7. jest poświęcony właśnie podstawom kontenerów oraz
typów uogólnionych. O ile kontenery, omówione w lekcji 35., dostępne są w Javie
praktycznie od jej początków, o tyle typy uogólnione, którym poświęcona jest lekcja 36.,
są stosunkowo nową funkcjonalnością wprowadzoną w Java 5 (1.5). To dosyć rozle-
gły i bardziej zaawansowany temat niż dotychczas omawiane, jednak każdy, kto poważnie
myśli o programowaniu w Javie, musi znać przynajmniej jego podstawy.

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. zo-
staną 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 dyna-
micznej 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ślo-
no 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

b4ac40784ca6e05cededff5eda5a930f
b
324 Java. Praktyczny kurs

napisać własną klasę symulującą zachowanie dynamicznej tablicy. Spróbujmy wyko-


nać to zadanie. Klasę nazwijmy np. TablicaInt — będzie ona umożliwiała przecho-
wywanie 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ł zaprezen-
towany 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 argu-
ment konstruktora. Do pobierania danych służy metoda get, która przyjmuje jeden ar-
gument — index — określający indeks wartości, jaka ma zostać zwrócona. Indeks po-
bieranego elementu nie może być w tym przypadku większy niż całkowity rozmiar
wewnętrznej tablicy pomniejszony o 1 (jak pamiętamy, elementy tablicy są indeksowa-
ne od 0) ani też mniejszy od 0. Jeśli zatem indeks znajduje się poza zakresem, gene-
rowany jest wyjątek ArrayIndexOutOfBoundsException:
throw new ArrayIndexOutOfBoundsException("index = " + index);

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7.  Kontenery i typy uogólnione 325

Jeśli zaś argument przekazany metodzie jest poprawny, zwrócona zostaje wartość od-
czytana spod wskazanego indeksu tablicy:
return tab[index];

Metoda set przyjmuje dwa argumenty. Pierwszy — index — określa indeks elementu,
który ma zostać zapisany, drugi — value — wartość, która ma się znaleźć pod tym in-
deksem. W tym przypadku na początku sprawdzamy, czy argument index jest mniej-
szy od 0, a jeśli tak, zgłaszamy wyjątek ArrayIndexOutOfBoundsException. Jest to jasne
działanie, nie można bowiem zapisywać ujemnych indeksów (chociaż ciekawym
rozwiązaniem byłoby wprowadzenie takiej możliwości, co będzie dobrym ćwiczeniem
do samodzielnego wykonania). Inaczej będzie w przypadku, gdy okaże się, że indeks
przekracza aktualny rozmiar tablicy. Skoro tablica ma dynamicznie zwiększać swoją
wielkość w zależności od potrzeb, taka sytuacja jest w pełni poprawna. Zwiększamy
wtedy tablicę, wywołując metodę resize1. Na końcu metody set przypisujemy war-
tość wskazaną przez value komórce określonej przez index:
tab[index] = value;

Pozostało więc przyjrzeć się metodzie resize. Nie wykonuje ona żadnych skompli-
kowanych czynności. Skoro nie można zwiększyć rozmiaru już raz utworzonej tablicy,
metoda ta tworzy nową tablicę o rozmiarze wskazanym przez argument size:
int newTab[] = new int[size];

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”).

b4ac40784ca6e05cededff5eda5a930f
b
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 pro-
gramu to próba pobrania przy użyciu metody get elementu o indeksie 3. Ponieważ taki
element nie istnieje, instrukcja ta powoduje wygenerowanie wyjątku.

Klasa TablicaInt może przechowywać tylko wartości typu int, czyli liczby całkowite. Co
zrobić, gdy trzeba zapisywać wartości innych typów? Można np. napisać kolejną klasę.
Co jednak w sytuacji, gdy przechowywane mają być wartości różnych typów? Jest to
jak najbardziej możliwe. Jak pamiętamy, każdy typ obiektowy dziedziczy bezpośrednio
lub pośrednio po klasie Object. Wiemy też już wiele o dziedziczeniu, rzutowaniu typów
i polimorfizmie. A zatem wystarczy, aby nasza klasa Tablica przechowywała referencje
do typu Object. Wtedy będą mogły być w niej zapisywane obiekty dowolnych typów.
Spójrzmy na listing 7.3.

Listing 7.3.
public class Tablica {
private Object tab[];
Tablica(int size){
tab = new Object[size];
}
public Object get(int index){
if(index >= tab.length || index < 0){
throw new ArrayIndexOutOfBoundsException("index = " + index);
}
else{
return tab[index];
}
}
public void set(int index, Object value){
if(index < 0){
throw new ArrayIndexOutOfBoundsException("index = " + index);
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7.  Kontenery i typy uogólnione 327

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 przyj-
muje dwa argumenty — pierwszy typu int określający indeks komórki do zmiany i drugi
typu Object określający nową wartość komórki.

Taka klasa będzie pracowała ze wszystkimi typami danych, nawet typami prostymi.
Widać to wyraźnie w metodzie main. Dwa pierwsze wywołania tab.set powodują za-
pisanie w komórkach o indeksach 0 i 1 wartości 1 i 2. Jest to możliwe, mimo że metoda
set oczekuje wartości typu Object. W takiej sytuacji następuje po prostu „zapakowanie”
typu prostego w obiekt klasy opakowującej. W tym przypadku będzie to klasa Integer
(każdy typ prosty ma właściwą sobie klasę opakowującą, dzięki czemu jego wartość może
być używana wszędzie tam, gdzie jest oczekiwany typ Object). Trzecia metoda set
powoduje umieszczenie w tablicy obiektu typu Object. Dalsze instrukcje działają tak
samo jak w poprzednim przykładzie. Po kompilacji i uruchomieniu zobaczymy więc
widok podobny do przedstawionego na rysunku 7.2. To dowód na to, że obiekt klasy
Tablica potrafi nie tylko dynamicznie zwiększać swoją pojemność, ale też przecho-
wywać dane różnych typów.

b4ac40784ca6e05cededff5eda5a930f
b
328 Java. Praktyczny kurs

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 no-
wych klas tego typu — wraz z JDK w pakiecie java.util został dostarczony cały ich
zestaw. Określa się je jako klasy kontenerowe lub kolekcje. Odzwierciedlają one naj-
rozmaitsze struktury danych, takie jak tablice, tablice asocjacyjne, zbiory, stosy, kolejki,
drzewa itp. Oczywiście wszystkie pozwalają na dynamiczne dodawanie i usuwanie da-
nych bez ograniczeń wielkości. Nie ma tu niestety miejsca na dokładne omówienie
wszystkich klas kontenerowych występujących w Javie, przyjrzyjmy się jednak sposobom
wykorzystania dynamicznej tablicy oraz stosu.

Klasa ArrayList
Klasę ArrayList można potraktować jako dynamiczny wektor elementów (dynamiczną
tablicę)2. Wybrane metody tej klasy zostały przedstawione w tabeli 7.1. Pewne zdzi-
wienie może budzić użycie w przypadku niektórych metod nazwy E jako typu danych.
Należy go jednak na razie potraktować jako dowolny typ obiektowy; kwestia ta zostanie
dokładniej wyjaśniona w kolejnej lekcji.

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 prze-
chowywała dużo większą liczbę elementów, należy skorzystać z drugiej wersji konstrukto-
ra, aby uniknąć zbędnych operacji powiększania pojemności w trakcie dodawania danych.

2
W rzeczywistości jest to implementacja interfejsu List, która wewnętrznie do przechowywania danych
wykorzystuje tablicę, w odróżnieniu np. od LinkedList, gdzie do przechowywania danych jest
używana lista dwukierunkowa.
3
Istnieje także trzeci konstruktor pozwalający na skopiowanie elementów z innego obiektu
implementującego interfejs Collection.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7.  Kontenery i typy uogólnione 329

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 Zwiększa pojemność listy.
(int minCapacity)
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 Usuwa z listy elementy z zakresu od fromIndex (włącznie)
(int fromIndex, int toIndex) 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();

b4ac40784ca6e05cededff5eda5a930f
b
330 Java. Praktyczny kurs

list.add(line);
}
System.out.println("Koniec wczytywania danych.");
}
catch(IOException e){
System.out.println("Błąd podczas odczytu strumienia.");
}
System.out.println("Wprowadzone zostały następujące dane: ");
int size = list.size();
for(int i = 0; i < size; i++){
line = list.get(i).toString();
System.out.println("[" + i + "] = " + line);
}
}
}

Najpierw za pomocą pierwszego typu konstruktora tworzony jest obiekt list klasy
ArrayList, a następnie obiekt brIn klasy BufferedReader służący do odczytywania
danych ze standardowego strumienia wejściowego. Pobieranie danych odbywa się tak
samo jak w przykładach z lekcji 31. — przy użyciu pętli while. Każda odczytana linia
jest dodawana do listy za pomocą metody add:
list.add(line);

Proces ten kończy się wraz z wprowadzeniem z klawiatury ciągu znaków quit (ten
ciąg również jest dodawany do listy).

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 konwer-
towane 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 wer-


sji na ekranie pojawi się informacja o potencjalnym braku kontroli typów, tak jak zo-
stał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ę prze-
konać, że wszystko działa zgodnie z założeniami (rysunek 7.4), a zastosowana dyna-
miczna lista pozwala na wprowadzenie praktycznie dowolnej ilości danych.

Rysunek 7.3.
Kompilator informuje
o braku kontroli typów

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7.  Kontenery i typy uogólnione 331

Rysunek 7.4.
Przykładowy efekt
działania programu
z listingu 7.4

Klasa Stack
Klasa Stack to implementacja struktury danych nazywanej stosem. Funkcjonuje ona
w taki sposób, że dane umieszcza się na stosie za pomocą metody push, a zdejmuje z nie-
go przy użyciu metody pop (metody klasy Stack zostały zebrane w tabeli 7.2). Dostęp
mamy jednak tylko do ostatnio dodanego elementu. Tak więc dane wprowadzone na
stos w pewnej kolejności będą z niego pobierane zawsze w kolejności odwrotnej. Klasa
Stack udostępnia tylko jeden bezargumentowy konstruktor, a jak w praktyce wygląda
jej wykorzystanie, pokazano w kodzie z listingu 7.5.

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.");

b4ac40784ca6e05cededff5eda5a930f
b
332 Java. Praktyczny kurs

}
catch(IOException e){
System.out.println("Błąd podczas odczytu strumienia.");
}
System.out.println("Wprowadzono następujące dane: ");
while(!stack.empty()){
line = stack.pop().toString();
System.out.println(line);
}
}
}

Kod jest pod względem struktury podobny do przedstawionego w poprzednim przy-


kł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 kla-


wiatury 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 po-
mocą metody empty (metoda zwraca wartość true, gdy stos jest pusty). Wewnątrz pętli
jest wykonywana metoda pop, która zdejmuje obiekt ze stosu i zwraca go jako wynik
swojego działania. Na rzecz tego obiektu jest następnie wywoływana metoda toString,
a wynik całej operacji zostaje przypisany zmiennej line:
line = stack.pop().toString();

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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7.  Kontenery i typy uogólnione 333

Przeglądanie kontenerów
W przykładach zamieszczonych w ramach tej lekcji do przeglądania i odczytywania
danych z obiektów klas kontenerowych (ArrayList, Stack) wykorzystywane były dotąd
pętle for i while. Jak jednak wiadomo z lekcji 7., istnieje także rozszerzona wersja pętli
for, zwana również pętlą foreach. Ten właśnie rodzaj pętli doskonale nadaje się do
przeglądania kolekcji obiektów. Spójrzmy na listing 7.6.

Listing 7.6.
import java.util.*;

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 do-
dane trzy elementy. Elementy te zostały następnie wyświetlone na ekranie przy użyciu
rozszerzonej pętli for oraz instrukcji System.out.println. Jak wiadomo z lekcji 7.,
w każdym przebiegu tej pętli pod zmienną o będzie podstawiany kolejny element listy
(ogólnie — kolejny element kolekcji obiektów). Zauważmy, że jest to bardzo wygodne
w sytuacji, kiedy niezbędne okazuje się przejrzenie i odczytanie wszystkich obiektów,
nie trzeba bowiem ani znać rozmiaru kolekcji (por. listing 7.4), ani też testować, czy od-
czytano już wszystkie elementy (por. listing 7.5).

Dociekliwy programista zada jednak w tym miejscu pytanie, skąd właściwie rozszerzo-
na pętla for „wie”, jak pobierać kolejne elementy, skoro można ją stosować do wielu
różnych klas kontenerowych, a każda z tych klas może inaczej przechowywać dane.
Odpowiedź jest następująca: rozszerzona pętla for potrafi przeglądać elementy każdego
obiektu implementującego interfejs Iterable, a większość klas kontenerowych ten
interfejs implementuje.

Interfejs Iterable wymusza definiowanie tylko jednej metody — iterator. Jej zadaniem
jest zwrócenie tzw. obiektu iteratora, tj. obiektu implementującego interfejs Iterator
i pozwalającego na przeglądanie kolekcji. W interfejsie Iterator zdefiniowane są z kolei
trzy metody przedstawione w tabeli 7.3. Powróćmy więc do klasy Tablica, której kod zo-
stał napisany na początku tej lekcji (listing 7.3), i sprawmy, aby mogła korzystać z dobro-
dziejstw rozszerzonej pętli for. Odpowiedni kod został zaprezentowany na listingu 7.7.

b4ac40784ca6e05cededff5eda5a930f
b
334 Java. Praktyczny kurs

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();
}
};
}

b4ac40784ca6e05cededff5eda5a930f
b
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ła-
niu pierwszy obiekt, przy drugim — drugi itd. W tym przypadku ma to być obiekt o in-
deksie wskazywanym przez pole position, jednocześnie niezbędne jest zwiększenie
wartości position o 1 (tak by wskazywała kolejny element kolekcji). Wszystko to od-
bywa się w jednej instrukcji:
return tab[position++]

Metoda remove nie jest nam do niczego potrzebna i nie będzie używana, dlatego też jedyną
czynnością w jej wnętrzu jest zgłoszenie wyjątku UnsupportedOperationException.

b4ac40784ca6e05cededff5eda5a930f
b
336 Java. Praktyczny kurs

Pozostałe elementy klasy Main działają w taki sam sposób jak w przykładzie z listingu
7.3. Z kolei metoda main testuje zachowanie obiektów wraz z pętlą foreach, dzięki czemu
można się przekonać, że implementacja interfejsu Iterable zakończyła się sukcesem.

Ć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 bar-
dzo częstej realokacji danych.

Ćwiczenie 35.2.
Napisz program wypełniający obiekt klasy TablicaInt (z listingu 7.1) liczbami od 1
do 1000, wykorzystujący do tego celu pętlę for. Początkowym rozmiarem tablicy ma
być 1. Pętla ma mieć taką postać, aby nastąpiła co najwyżej jedna realokacja danych.

Ćwiczenie 35.3.
Zmodyfikuj program z listingu 7.5 w taki sposób, by dane były przechowywane w obiek-
cie klasy Stack, ale zostały wyświetlone w takiej samej kolejności, w jakiej zostały
wprowadzone.

Ćwiczenie 35.4.
Zmodyfikuj implementację interfejsu Iterable z listingu 7.7 w taki sposób, aby zastoso-
wana rozszerzona pętla for wyświetlała dane w odwrotnej kolejności (tzn. od ostatniego
elementu do pierwszego).

Ćwiczenie 35.5.
Napisz program wczytujący z konsoli wiersze tekstu wprowadzane przez użytkow-
nika. Odczytane dane powinny zostać następnie wyświetlone w następującej kolejności:
1. teksty reprezentujące liczby całkowite, 2. teksty reprezentujące liczby rzeczywiste,
3. pozostałe teksty.

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 ty-
pach 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 wszyst-
kie 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 interfej-
sach), ale na typach nieokreślonych, ogólnych. To rozległy temat, którego omówienie

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7.  Kontenery i typy uogólnione 337

wykracza poza ramy tematyczne niniejszej publikacji, ponieważ jednak każdy progra-
mista musi znać przynajmniej podstawy tego zagadnienia, lekcja 36. została poświęcona
niezbędnym podstawom.

Problem kontroli typów


Powróćmy do przykładu z klasą Tablica zaprezentowanego w poprzedniej lekcji (li-
sting 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ć, do-
piszmy 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. Ponie-
waż metoda get zwraca obiekt typu Object, przed wywołaniem metody set niezbędne
było dokonanie rzutowania na typ Rectangle. Wszystko działałoby oczywiście prawi-
dłowo, gdyby nie to, że trzecia instrukcja set spowodowała umieszczenie w obiekcie
rectangles obiektu typu Triangle, który nie ma metody diagonal (trójkąt, ang. triangle,
nie ma przekątnej, ang. diagonal). Kompilator nie ma jednak żadnej możliwości wy-
chwycenia takiego błędu — skoro klasa Tablica może przechowywać dowolne typy
obiektowe, to typem drugiego argumentu metody set jest Object. Zawsze więc będzie
wykonywane rzutowanie na typ Object (czyli formalnie trzecia instrukcja set jest trakto-
wana jako rectangles.set(2, (Object) new Triangle())). Błąd ujawni się zatem dopiero
w trakcie wykonywania programu: przy próbie rzutowania trzeciego elementu pobranego
z kontenera rectangles na typ Rectangle zostanie zgłoszony wyjątek ClassCastException,
tak jak jest to widoczne na rysunku 7.6.

b4ac40784ca6e05cededff5eda5a930f
b
338 Java. Praktyczny kurs

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 spo-
sób wykorzystamy kontener ArrayList, efekt będzie taki sam. Spójrzmy na listing 7.9.
W metodzie main została utworzona lista typu ArrayList, do której zostały dodane
dwa obiekty, pierwszy klasy Rectangle i drugi klasy Triangle. Następnie w pętli typu
foreach została wywołana metoda diagonal, oczywiście po wcześniejszym rzutowaniu
każdego pobranego z kontenera obiektu. Po uruchomieniu efekt będzie taki sam jak w po-
przednim przykładzie — wystąpi wyjątek ClassCastException.

Listing 7.9.
import java.util.*;

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 progra-
mu, co sprawdzi się w praktyce, choć będzie wymagało zarówno większej pracy pro-
gramisty, jak i większego narzutu związanego z wykonywaniem dodatkowych instrukcji
weryfikujących. Najwygodniejsze byłoby rozwiązanie polegające na możliwości prze-
chowywania każdego typu danych, a przy tym przerzucenie części zadań związanych
z kontrolą typów na kompilator. Jest to możliwe dzięki typom uogólnionym.

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 korzy-
stają z dobrodziejstw typów uogólnionych. Wyświetlona informacja mówi właśnie o tym,

b4ac40784ca6e05cededff5eda5a930f
b
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 zadeklaro-
wać, 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 sto-
suje 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();
}
}

b4ac40784ca6e05cededff5eda5a930f
b
340 Java. Praktyczny kurs

class Triangle{};
class Rectangle{
public void diagonal(){}
}

Tym razem przy tworzeniu zmiennej list oraz obiektu klasy ArrayList zostało zaznaczone,
że będzie on przechowywał obiekty klasy Rectangle. Typ Rectangle został umieszczony
w nawiasie kątowym, zarówno w deklaracji zmiennej list:
ArrayList<Rectangle> list

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ń, po-
nieważ 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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7.  Kontenery i typy uogólnione 341

Można bezpośrednio wywołać metodę diagonal bez potrzeby rzutowania na typ Rectangle,
ponieważ po odpowiedniej deklaracji kontenera metoda get będzie zwracała obiekty
typu Rectangle.

W Java 8 wprowadzono możliwość zastosowania składni uproszczonej związanej z typa-


mi 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 do-
określaniu typu lub inferowaniu typu (ang. type inference).

Stosowanie uogólnień
W poprzednim podpunkcie zostało pokazane, w jaki sposób używać klas korzystających
z uogólnień, ale trzeba również wiedzieć, jak taką klasę napisać. Zacznijmy jednak od
napisania kodu klasy, która mogłaby przechowywać obiekty dowolnego typu. Można
ją traktować jako opakowanie, dlatego też nazwiemy ją po prostu Opakowanie. Kod jest
widoczny na listingu 7.11.

Listing 7.11.
public class Opakowanie {
private Object data;
Opakowanie(Object value) {
set(value);
}
public Object get() {
return data;
}
public void set(Object value) {
this.data = value;
}
public static void main(String args[]){
Opakowanie opk1 = new Opakowanie(new Rectangle());
Opakowanie opk2 = new Opakowanie(new Triangle());
((Rectangle) opk1.get()).diagonal();
((Rectangle) opk2.get()).diagonal();
}
}

class Triangle{};
class Rectangle{
public void diagonal(){}
}

Dane przechowywane są w prywatnym polu data, którego typem jest Object. Pobie-
raniem danych zajmuje się metoda get, a ustawianiem — set. Metoda set przyjmuje
jeden argument typu Object i przypisuje jego wartość polu data, natomiast metoda

b4ac40784ca6e05cededff5eda5a930f
b
342 Java. Praktyczny kurs

get jest bezargumentowa i zwraca wartość typu Object. Oprócz tego do dyspozycji
mamy również typowy publiczny konstruktor, a także znane nam już dobrze dwie pa-
kietowe klasy pomocnicze, Triangle i Rectangle.

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) przyj-
muje argument typu Object, w obu przypadkach zostało wykonane niejawne rzuto-
wanie. 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 pobra-
ne przechowywane przez nie dane. Ze względu na to, że metoda get zwraca wartości
typu Object, w obu przypadkach niezbędne było wykonanie rzutowania w dół — do
typu Rectangle. Tylko dzięki temu można było wywołać metodę diagonal. Oczywiście
złożoną instrukcję:
((Rectangle) opk1.get()).diagonal();

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, nato-
miast w trakcie wykonania pojawi się wyjątek ClassCastException, jako że w opk2
został zapisany obiekt typu Triangle, który nie może być rzutowany na typ Rectangle,
a tym bardziej nie może zostać wykonana metoda diagonal.

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 da-
nych (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);
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7.  Kontenery i typy uogólnione 343

public T get() {
return data;
}
public void set(T value) {
this.data = value;
}
}

Za nazwą klasy w nawiasie kątowym pojawił się symbol T. To informacja, że klasa będzie
korzystała z typów uogólnionych oraz że symbolem typu uogólnionego jest T (można
ten symbol zmienić na dowolny inny, niekoniecznie jednoliterowy). Ten symbol został
umieszczony w każdym miejscu kodu klasy, gdzie wcześniej występował konkretny
typ przechowywanych danych (był to typ Object). Tak więc instrukcja
private T data;

oznacza, że w klasie znajduje się prywatne pole, którego typ zostanie określony do-
piero w kodzie aplikacji przy tworzeniu obiektu klasy Opakowanie. Deklaracja:
public T get() {

oznacza metodę get zwracającą typ danych, który zostanie dokładniej określony przy
tworzeniu obiektu klasy Opakowanie itd. Każde wystąpienie T to deklaracja, że właściwy
typ danych będzie określony później. Dzięki temu powstała uniwersalna klasa, któ-
ra może przechowywać dowolne dane, ale która pozwala na kontrolę typów już w mo-
mencie kompilacji. Sprawdźmy to, dopisując do kodu metodę main oraz klasy Triangle
i Rectangle, tak jak jest to widoczne na listingu 7.13.

Listing 7.13.
public class Opakowanie<T> {
/*tutaj początek kodu klasy Opakowanie z listingu 7.12*/
public static void main(String args[]){
Opakowanie<Rectangle> opk1 = new Opakowanie<Rectangle>(
new Rectangle()
);
/*Opakowanie<Rectangle> opk2 = new Opakowanie<Rectangle>(
new Triangle()
);*/
Opakowanie<Triangle> opk3 = new Opakowanie<Triangle>(
new Triangle()
);
opk1.get().diagonal();
//opk2.get().diagonal();
opk3.get().type();
}
}

class Triangle{
public void type(){}
}
class Rectangle{
public void diagonal(){}
}

b4ac40784ca6e05cededff5eda5a930f
b
344 Java. Praktyczny kurs

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ę na-
zwę 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, wystar-
czy typ przechowywanego obiektu zaznaczyć tylko przy deklaracji zmiennej. Dzięki
temu kompilator „wie”, że obiekt opk1 ma przechowywać obiekty klasy Rectangle (lub
klas pochodnych od Rectangle). Bez problemu może więc zostać wykonana instrukcja:
opk1.get().diagonal();
jako że metoda get zwraca w tym przypadku dane typu Rectangle, a obiekt takiego
typu zawiera metodę diagonal.

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 otrzy-
mał informację, że obiekt opk2 będzie współpracował z danymi typu Rectangle, tym-
czasem 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();

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7.  Kontenery i typy uogólnione 345

jako że w tym przypadku metoda get zwróci wartość typu Triangle, a w klasie Triangle
istnieje metoda type.

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 przy-
padku 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 ar-
gumencie typu uogólnionego, to specyfikację tego typu należy umieścić przed typem
zwracanym przez metodę. Wygląda to tak:
modyfikator_dostępu <specyfikacja_typu_ogólnego> typ_zwracany
nazwa_metody(argumenty)

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

b4ac40784ca6e05cededff5eda5a930f
b
346 Java. Praktyczny kurs

mógł być typ dowolny. Pokazuje to metoda main, w której metoda show została wy-
wołana z różnymi argumentami. W pierwszym przypadku jest to argument typu int,
w drugim — double, w trzecim — String, a w czwartym — nowy obiekt klasy Main.
Ponieważ zadaniem metody show jest konwersja przekazanej jej wartości (przez wy-
wołanie metody toString) na typ String i wyświetlenie jej na ekranie, po uruchomie-
niu aplikacji zobaczymy widok przedstawiony na rysunku 7.10.

Rysunek 7.10.
Efekt działania
programu z listingu 7.14

Jak zwrócić wiele wartości?


Zwykła metoda może zwracać tylko jedną wartość. Jeśli ma być ich więcej, trzeba za-
stosować 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(){}
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 7.  Kontenery i typy uogólnione 347

Klasa Opakowanie zawiera dwa pola typu Object, dzięki temu może przechowywać
obiekty dowolnych typów. Zawiera także konstruktor pozwalający na inicjalizację obu
pól. Z tych właściwości korzysta metoda getValues z klasy Main. Zadaniem tej meto-
dy jest zwrócenie dwóch wartości typów Triangle i Rectangle. Odbywa się to przez utwo-
rzenie nowego obiektu klasy Opakowanie i zwrócenie go za pomocą instrukcji return:
new Opakowanie(new Triangle(), new Rectangle());

Działanie kodu jest testowane w metodzie main, w której najpierw jest tworzony
obiekt klasy Main:
Main main = new Main();

Następnie jest wywoływana metoda getValues, a wynik jej działania jest przypisywany
zmiennej opk:
Opakowanie opk = main.getValues();

Ponieważ pole pole1 klasy Opakowanie miało być klasy Triangle, po wykonaniu rzuto-
wania można wywołać metodę type tej klasy:
((Triangle)opk.pole1).type();

i analogicznie, ze względu na to, że pole pole2 miało być klasy Rectangle, po dokona-
niu rzutowania na tę klasę można wywołać metodę diagonal:
((Rectangle)opk.pole2).diagonal();

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. Wystar-
czy 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 prze-
chowywałaby jedynie wartości typów Rectangle i Triangle, jednak straciłoby się wtedy
jej uniwersalność, a w założeniu miała ona wciąż przechowywać dowolne dane. Warto
zatem wykorzystać rozwiązanie z typami uogólnionymi. Zostało ono zaprezentowane
na listingu 7.16.

Listing 7.16.
public class Main {
public Opakowanie<Triangle, Rectangle> getValues(){
return new Opakowanie<Triangle, Rectangle>(
new Triangle(), new Rectangle()
);
}
public static void main(String args[]){
Main main = new Main();
Opakowanie<Triangle, Rectangle> opk = main.getValues();
opk.pole1.type();
opk.pole2.diagonal();
}
}

b4ac40784ca6e05cededff5eda5a930f
b
348 Java. Praktyczny kurs

class 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 zwraca-
nego 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 uni-
wersalności klasy Opakowanie.

b4ac40784ca6e05cededff5eda5a930f
b
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ł po-
prawnie.

Ćwiczenie 36.3.
Napisz klasę uogólnioną, która będzie mogła przechowywać wartości trzech różnych
typów.

Ćwiczenie 36.4.
Do klasy Opakowanie z listingu 7.16 dopisz metody pozwalające na niezależną mani-
pulację każdym polem (osobny zapis i odczyt wartości dla każdego pola).

Ćwiczenie 36.5.
Zmodyfikuj kod klasy Tablica z listingu 7.3 (lekcja 35.) tak, aby korzystała ona ze składni
typów uogólnionych. Uwaga: nie możesz utworzyć generycznej tablicy, zapis typu
new T[] jest nieprawidłowy. Zastanów się, czy tworzenie tego typu klasy ma sens.

Ćwiczenie 36.6.
Popraw kod z ćwiczenia 35.5 (z lekcji 35.) tak, by korzystał z typów uogólnionych i żeby
w związku z tym kompilator nie generował ostrzeżeń.

b4ac40784ca6e05cededff5eda5a930f
b
350 Java. Praktyczny kurs

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.
Aplikacje i aplety
Programy w Javie mogą być apletami (ang. applet) lub aplikacjami (ang. application).
Różnica jest taka, że aplikacja jest programem samodzielnym uruchamianym z poziomu
systemu operacyjnego (a dokładniej: maszyny wirtualnej Javy operującej na poziomie
systemu operacyjnego) i tym samym ma pełny dostęp do zasobów udostępnianych
przez system, natomiast aplet jest uruchamiany pod kontrolą innego programu, naj-
częściej przeglądarki internetowej, i ma dostęp jedynie do środowiska, które ten pro-
gram mu udostępni. Aplet to inaczej program zagnieżdżony w innej aplikacji. Zazwy-
czaj nie ma on dostępu np. do dysku twardego (i innych urządzeń), tak aby ściągnięty
nieświadomie z internetu nie mógł zaszkodzić użytkownikowi, np. kasując dane, chyba że
zostanie wyposażony w specjalny podpis cyfrowy i użytkownik zgodzi się na udostęp-
nienie mu chronionych zasobów. Osobną kwestią są wykrywane co jakiś czas błędy
w mechanizmach bezpieczeństwa przeglądarek i maszyn wirtualnych, które niekiedy
powodują, że złośliwie napisane aplety mogą obejść restrykcje i dostać się do chronio-
nych zasobów systemu. Nie są to jednak sytuacje bardzo częste.

Obecnie aplety straciły na znaczeniu i nie są już tak często używane, jednak są wciąż
powszechne np. na stronach prezentujących notowania giełdowe, oferujących narzędzia
do analizy technicznej związanej z grą na giełdach papierów wartościowych, a także w apli-
kacjach typu czat czy też w niektórych grach działających w przeglądarkach WWW.

W rzeczywistości różnice w konstrukcji apletu i aplikacji nie są bardzo duże, jednak roz-
dział ten dla porządku został podzielony na dwie części. W pierwszej zostaną omówione
aplety działające pod kontrolą przeglądarek, a w drugiej — samodzielne aplikacje.

Aplety
Lekcja 37. Podstawy apletów
Wszystkie przykłady programów prezentowane w poprzednich lekcjach pracowały w try-
bie tekstowym, najwyższy czas zatem przejść w świat aplikacji pracujących w trybie
graficznym. W lekcji 37. rozpoczynamy omawianie apletów, czyli niewielkich aplikacji

b4ac40784ca6e05cededff5eda5a930f
b
352 Java. Praktyczny kurs

uruchamianych pod kontrolą przeglądarek internetowych. Zostanie w niej przedsta-


wione 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źć kom-
ponenty pakietu (biblioteki) Swing, JApplet. Co prawda w pierwszych przykładach nie
będzie wykorzystywany ten pakiet, jednak aplety zostaną wyprowadzone z nowocześniej-
szej klasy JApplet. Komponenty Swing zostaną natomiast przedstawione w lekcji 42.
Przed dokładnym omówieniem budowy tego typu programów warto na rozgrzewkę napi-
sać prosty aplet, który, jakżeby inaczej, będzie wyświetlał na ekranie dowolny napis.
Kod takiego apletu jest widoczny na listingu 8.1.

Listing 8.1.
import javax.swing.JApplet;
import java.awt.*;

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 meto-
dy 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 po-
dobnej 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).

b4ac40784ca6e05cededff5eda5a930f
b
354 Java. Praktyczny kurs

Listing 8.3.
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="utf-8">
<title>Moja strona WWW</title>
</head>
<body>
<div>
<object
type="application/x-java-applet"
code="PierwszyAplet.class"
width="320"
height="200">
</object>
</div>
</body>
</html>

Tak skonstruowany plik z kodem HTML można już wczytać do przeglądarki. Pakiet JDK
zawiera swoją własną przeglądarkę służącą do testowania apletów — appletviewer.
Uruchamia się ją, pisząc w wierszu poleceń:
appletviewer nazwa_pliku.html

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 Firefok-
sa (oczywiście należy wczytywać plik index.html, a nie PierwszyAplet.class)7. Należy
pamiętać, aby plik ze skompilowanym apletem (PierwszyAplet.class) był umieszczony
w tym samym katalogu co plik z kodem HTML (index.html), a dokładniej rzecz ujmując:
w lokalizacji wskazanej przez atrybut code znacznika <object>.

Rysunek 8.1.
Pierwszy aplet
w przeglądarce
appletviewer

6
Zastosowano kod zgodny ze standardem HTML5, z tym wyjątkiem, że w celu zachowania maksymalnej
kompatybilności użyty został parametr code zamiast formalnie poprawnego — data.
7
W celu uruchomienia apletu z pliku lokalnego może być konieczna zmiana systemowych ustawień
zabezpieczeń, w tym dopuszczenie w konfiguracji środowiska JRE otwierania apletów dostępnych
po protokole FILE.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 355

Rysunek 8.2.
Pierwszy aplet
w przeglądarce Firefox

Konstrukcja apletu
Kiedy przeglądarka obsługująca Javę odnajdzie w kodzie HTML osadzony aplet, wyko-
nuje określoną sekwencję czynności. Zaczyna oczywiście od sprawdzenia, czy w podanej
lokalizacji znajduje się plik zawierający kod danej klasy. Jeśli tak, kod ten jest łado-
wany do pamięci i tworzona jest instancja (czyli obiekt) znalezionej klasy, a tym sa-
mym wywołany zostaje konstruktor. Następnie jest wykonywana metoda init utwo-
rzonego obiektu informująca go o tym, że został załadowany do systemu. W metodzie
init można zatem umieścić, o ile zachodzi taka potrzeba, procedury inicjacyjne.

Kiedy aplet jest gotowy do uruchomienia, przeglądarka wywołuje jego metodę start,
informując, że powinien rozpocząć swoje działanie. Przy pierwszym ładowaniu apletu
metoda start jest zawsze wykonywana po metodzie init. W momencie gdy aplet powi-
nien zakończyć swoje działanie, jest z kolei wywoływana jego metoda stop. O tym, że
rzeczywiście występuje taka sekwencja instrukcji, można się przekonać, uruchamiając
(czy też wczytując do przeglądarki) kod z listingu 8.4. Wynik działania dla appletviewera
uruchamianego z konsoli systemowej jest widoczny na rysunku 8.3. Na rysunku 8.4 został
z kolei przedstawiony obraz konsoli Javy w przeglądarce korzystającej z wtyczki JRE 1.8.

Listing 8.4.
import javax.swing.JApplet;
import java.awt.*;

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);
}
}

b4ac40784ca6e05cededff5eda5a930f
b
356 Java. Praktyczny kurs

Rysunek 8.3.
Kolejność wykonywania
metod w aplecie na
konsoli systemowej

Rysunek 8.4.
Kolejność wykonywania
metod w aplecie na
konsoli Java Plugin

Parametry apletu
W przypadku niektórych apletów pojawia się potrzeba przekazania im parametrów
z kodu HTML — na przykład wartości sterujących ich pracą. Taka możliwość oczy-
wiście istnieje, znacznik <object> (a także <applet>) pozwala na zastosowanie znacz-
ników wewnętrznych <param>. Ogólnie konstrukcja taka będzie miała postać:
<object
<!--atrybuty znacznika object-->
>
<param name = "nazwa1" value = "wartość1">
<!--tutaj dalsze znaczniki param-->
<param name = "nazwaN" value = "wartośćN">
</object>

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.*;

b4ac40784ca6e05cededff5eda5a930f
b
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 argu-
ment należy przekazać nazwę parametru (w tym przypadku tekst). Metoda ta zwraca
wartość wskazanego argumentu lub wartość null, jeżeli parametr nie został uwzględ-
niony w kodzie HTML.

W klasie Aplet deklarujemy zatem pole typu String o nazwie tekst. W metodzie init,
która zostanie wykonana po załadowaniu apletu przez przeglądarkę, odczytujemy
wartość parametru tekst i przypisujemy ją polu tekst. Sprawdzamy jednocześnie,
czy pole to otrzymało wartość null (co by oznaczało, że parametr nie został umieszczo-
ny w kodzie HTML i nie można było go pobrać) — jeśli tak, przypisujemy mu wła-
sny tekst. Działanie metody paint jest takie samo jak we wcześniejszych przykładach,
wyświetla ona po prostu zawartość pola tekst na ekranie, w miejscu o wskazanych
współrzędnych (x = 60, y = 50). Pozostaje więc napisanie kodu HTML zawierającego
taki aplet, co zostało przedstawione na listingu 8.6. Po uruchomieniu kodu (nie można
zapomnieć o wcześniejszej kompilacji klasy Aplet) zobaczymy widok zaprezentowany
na rysunku 8.5.

Listing 8.6.
<object
type="application/x-java-applet"
code="Aplet.class"
width="300"
height="100">
<param name="tekst"
value="Testowanie parametrów apletu">
</object>

Rysunek 8.5.
Napis przekazany
apletowi w postaci
parametru

b4ac40784ca6e05cededff5eda5a930f
b
358 Java. Praktyczny kurs

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 wy-
glądały w każdym systemie. Zamiast tego jest wykorzystywana technika mapowania,
tzn. dobierana jest czcionka systemowa (z systemu, na którym działa środowisko uru-
chomieniowe Javy) najlepiej pasująca do jednej z wymienionych rodzin.

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);

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 359

gDC.setFont(monospaced);
gDC.drawString ("Czcionka Monospaced", 60, 120);

gDC.setFont(dialog);
gDC.drawString ("Czcionka Dialog", 60, 160);

gDC.setFont(dialogInput);
gDC.drawString ("Czcionka DialogInput", 60, 200);
}
}

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. Kon-
struktor 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 pogrubio-
ny, 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 dane-
go fontu. Ostatecznie po skompilowaniu kodu i uruchomieniu apletu na ekranie poja-
wi się widok zaprezentowany na rysunku 8.6.

Rysunek 8.6.
Lista czcionek
dostępnych standardowo

b4ac40784ca6e05cededff5eda5a930f
b
360 Java. Praktyczny kurs

Nic nie stoi również na przeszkodzie, aby w konstruktorze klasy Font podać nazwę innej
rodziny czcionek. Co się jednak stanie, jeśli danej czcionki nie ma w systemie? W takiej
sytuacji zostanie wykorzystana czcionka domyślna, czyli kod będzie działał tak, jakby
w konstruktorze został przekazany parametr default. Czcionką domyślną jest Dialog.
Aby uniknąć takich niespodzianek, najlepiej po prostu pobrać listę wszystkich do-
stępnych fontów i tylko te wykorzystywać. Umożliwia to klasa GraphicsEnvironment.
Zawiera ona dwie interesujące nas metody: getAllFonts oraz getAvailableFontFamily
8
Names . Pierwsza z nich zwraca tablicę obiektów typu Font zawierającą wszystkie
dostępne czcionki, natomiast druga — tablicę obiektów typu String z nazwami dostęp-
nych rodzin czcionek. Na listingu 8.8 jest widoczny kod apletu, który wykorzystuje
wymienione metody do wyświetlania na ekranie czcionek dostępnych w systemie.

Listing 8.8.
import javax.swing.JApplet;
import java.awt.*;

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.

b4ac40784ca6e05cededff5eda5a930f
b
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 (pod-
punkt „Ć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 wyko-
nanie tego zadania wymaga jednak znajomości długości oraz wysokości napisu. Wia-
domo 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);

b4ac40784ca6e05cededff5eda5a930f
b
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świetla-
niu napisów. Następnie pobieramy obiekt klasy FontMetrics, wywołując metodę get
FontMetrics, i przypisujemy go zmiennej fm. Ten obiekt udostępnia metody niezbęd-
ne do określania wysokości i szerokości napisu. Szerokość uzyskuje się przez wywołanie
metody stringWidth, a wysokość — przez wywołanie metody getHeight. Kiedy po-
bierzemy te wartości, pozostaje już tylko proste wyliczenie współrzędnych, od których
powinno rozpocząć się wyświetlanie napisu. Wystarczy użyć wzorów:
x = (szerokość apletu – szerokość napisu) / 2
y = (wysokość apletu + szerokość napisu) / 2

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 fi-
nalne określające łącznie dwanaście różnych kolorów. Pola te zostały zebrane w ta-
beli 8.1. Aby zatem zmienić kolor wyświetlanego tekstu w aplecie z listingu 8.9 na
czerwony, należałoby przed wywołaniem drawString dodać do metody paint wywołanie
metody setColor(Color.red) lub setColor(Color.RED). Metoda paint miałaby wtedy
następującą postać:
public void paint (Graphics gDC) {
//tutaj początkowe instrukcje metody

gDC.clearRect(0, 0, getSize().width, getSize().height);


gDC.setColor(Color.RED);
gDC.drawString(tekst, x, y);
}

b4ac40784ca6e05cededff5eda5a930f
b
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 odpowia-
dającego nam koloru, musimy postąpić inaczej. Otóż należy w takim przypadku utwo-
rzyć nowy obiekt klasy Color. Do dyspozycji jest siedem konstruktorów, są one dokład-
niej 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 wy-
mienionych konstruktorów przyjmuje natomiast wartość składowych w postaci trzech
argumentów typu int. Warto może w tym miejscu przypomnieć, że w modelu RGB
kolor jest określany za pomocą trzech składowych: czerwonej (R, ang. red), zielonej
(G, ang. green) i niebieskiej (B, ang. blue). Każdy z parametrów przekazanych drugie-
mu konstruktorowi może przyjmować wartości z zakresu 0 – 255 (jest to więc 8-bitowy
model RGB), łącznie można zatem przedstawić 16 777 216 różnych kolorów. W ta-
beli 8.2 zostały przedstawione składowe RGB dla kilkunastu przykładowych kolorów.

Napiszmy zatem aplet wyświetlający w centrum powierzchni swojego okna napis, którego
treść oraz kolor będą zdefiniowane poprzez zewnętrzne parametry określone w kodzie
HTML. Kolor będzie określany za pomocą trzech parametrów odzwierciedlających
poszczególne składowe RGB. Kod klasy realizującej takie zadanie został przedstawiony
na listingu 8.10.

9
Rodzaj purpury, w modelu RGB z reguły określany przez składowe R = 255, G = 0, B = 255.

b4ac40784ca6e05cededff5eda5a930f
b
364 Java. Praktyczny kurs

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);

b4ac40784ca6e05cededff5eda5a930f
b
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 na-
zwach paramR, paramG i paramB. Ponieważ metoda getParameter zwraca wartość typu
String, próbujemy dokonać konwersji na typ int za pomocą metody parseInt z klasy
Integer. Jeśli któraś z konwersji się nie powiedzie, zostanie wygenerowany wyjątek
NumberFormatException, który przechwytujemy w bloku try. Jeśli taki wyjątek wystąpi,
przywracamy początkowe wartości zmiennym paramR, paramG i paramB. Tworzymy
następnie nowy obiekt klasy Color i nowy obiekt klasy Font. Odczytujemy także wartość
parametru tekst i przypisujemy go polu o takiej samej nazwie. Jeśli okaże się, że zosta-
nie zwrócona wartość pusta null, czyli parametr o nazwie tekst nie został przekazany,
pole tekst otrzyma pusty ciąg znaków. Wyświetlaniem napisu w centrum okna zaj-
muje się metoda paint, której zasada działania jest identyczna z tą przedstawioną na
początku tego podrozdziału. Na listingu 8.11 został zaprezentowany przykładowy kod
znacznika <object> zawierający wywołanie apletu.

Listing 8.11.
<object
type="application/x-java-applet"
code="Aplet.class"
width="400"
height="210"
>
<param name="paramr" value="168">
<param name="paramg" value="18">
<param name="paramb" value="240">
<param name="tekst" value="Przykładowy tekst">
</object>

b4ac40784ca6e05cededff5eda5a930f
b
366 Java. Praktyczny kurs

Ćwiczenia do samodzielnego wykonania

Ćwiczenie 38.1.
Napisz program, który w trybie tekstowym wyświetli na konsoli systemowej listę czcio-
nek dostępnych w systemie.

Ćwiczenie 38.2.
Napisz aplet wyświetlający na środku powierzchni okna tekst, którego treść oraz wielkość
i styl czcionki będą przekazywane w postaci parametrów zawartych w kodzie HTML.

Ćwiczenie 38.3.
Napisz aplet, który będzie wyświetlał tekst o kolorze określonym przez parametr
przekazywany z kodu HTML. Parametrem tym powinna być jedna liczba typu int.

Ćwiczenie 38.4.
Wyświetl na ekranie apletu krótki tekst, w którym każda litera będzie miała inny kolor
(kolory mogą być losowe)10. Zrób tak, aby odległość pomiędzy literami była stała
(możesz użyć czcionki o stałej szerokości znaku). Tekst powinien być wyśrodkowany
w pionie i poziomie.

Ćwiczenie 38.5.
Zmodyfikuj kod ćwiczenia 38.4 w taki sposób, aby przy każdym odświeżeniu okna
apletu litery zmieniały kolory.

Lekcja 39. Grafika


Aplety pracują w środowisku graficznym, czas więc poznać podstawowe operacje gra-
ficzne, 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. Przedsta-
wiony 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. Ryso-
wać 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).

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 367

Do dyspozycji jest zestaw metod pozwalających na tworzenie m.in. linii, kół, elips
i okręgów oraz wielokątów. Najpopularniejsze metody zostały zebrane w tabeli 8.3.

Tabela 8.3. Wybrane metody klasy Graphics


Deklaracja metody Opis
void drawLine(int x1, int y1, Rysuje linię rozpoczynającą się w punkcie
int x2, int y2) o współrzędnych x1, y1 i kończącą się w punkcie x2, y2.
void drawOval(int x, int y, Rysuje owal wpisany w prostokąt opisany parametrami
int width, int height) x, y oraz width i height.
void drawPolygon(int[] xPoints, Rysuje wielokąt o punktach wskazywanych przez
int[] yPoints, int nPoints) 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, Rysuje sekwencję połączonych ze sobą odcinków (linii)
int[] yPoints, int nPoints) 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, Rysuje prostokąt zaczynający się w punkcie
int width, int height) 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, Rysuje prostokąt o zaokrąglonych rogach zaczynający
int width, int height, int arcWidth, się w punkcie o współrzędnych x, y oraz szerokości
int arcHeight) 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, Rysuje koło lub elipsę wpisaną w prostokąt opisany
int width, int height) parametrami x, y oraz width i height.
void fillPolygon(int[] xPoints, Rysuje wypełniony bieżącym kolorem wielokąt o punktach
int[] yPoints, int nPoints) 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, Rysuje wypełniony bieżącym kolorem prostokąt
int width, int height) 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 Rysuje wypełniony bieżącym kolorem prostokąt
width, int height, int arcWidth, int o zaokrąglonych rogach, zaczynający się w punkcie
arcHeight) 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.

b4ac40784ca6e05cededff5eda5a930f
b
368 Java. Praktyczny kurs

Punkty i linie
Klasa Graphics nie oferuje oddzielnej metody, która umożliwiałaby rysowanie poje-
dynczych punktów, zamiast tego można jednak skorzystać z metody rysującej linie. Wy-
starczy, jeśli punkt początkowy i docelowy będą miały te same współrzędne. Tak więc
aby narysować punkt o współrzędnych x = 100 i y = 200, można zastosować następującą
instrukcję:
drawLine(100, 200, 100, 200);

Prosty aplet rysujący prostokąt (składający się z czterech oddzielnych linii) z poje-
dynczym punktem w środku jest widoczny na listingu 8.12, a efekt jego działania na
rysunku 8.8.

Listing 8.12.
import javax.swing.JApplet;
import java.awt.*;

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.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 369

Koła i elipsy
Do tworzenia kół i elips służą dwie metody klasy Graphics: drawOval oraz fillOval.
Pierwsza rysuje sam kontur figury, druga figurę wypełnioną aktualnym kolorem. Kolor
oczywiście można zmieniać przy użyciu metody setColor. Czemu do tworzenia koła
i elipsy została zdefiniowana tylko jedna metoda? Ponieważ koło jest po prostu szczegól-
nym przypadkiem elipsy, w której oba promienie są sobie równe. Nie czas, by zagłębiać
się w niuanse matematyki, trzeba tylko wiedzieć, w jaki sposób określać rozmiary figur
w wymienionych metodach. Przyjmują one bowiem cztery parametry opisujące pro-
stokąt, w który można wpisać koło lub elipsę. Ponieważ w prostokąt o określonych
rozmiarach można wpisać tylko jeden kształt owalny, określenie prostokąta jedno-
znacznie wyznacza koło lub elipsę.

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

b4ac40784ca6e05cededff5eda5a930f
b
370 Java. Praktyczny kurs

Wielokąty
Do rysowania wielokątów wykorzystuje się kilka różnych metod — można rysować
zarówno same kontury (metody zaczynające się słowem draw), jak i figury wypełnio-
ne kolorem (metody zaczynające się słowem fill). Do rysowania prostokątów służą
metody drawRectangle oraz fillRectangle. Przyjmują one cztery argumenty — dwa
pierwsze określają współrzędne lewego górnego rogu, natomiast dwa kolejne — szero-
kość oraz wysokość figury. Istnieje również możliwość narysowania prostokąta o zaokrą-
glonych rogach11. Służą do tego metody drawRoundRect oraz fillRoundRect. W takim
przypadku do wymienionych przed chwilą argumentów dochodzą dwa dodatkowe
określające średnicę łuku zaokrąglenia w poziomie oraz w pionie (deklaracje wszystkich
wymienionych metod znajdują się w tabeli 8.3). Przykładowy aplet rysujący opisane
figury jest przedstawiony na listingu 8.14, a efekt jego działania — na rysunku 8.10.

Listing 8.14.
import javax.swing.JApplet;
import java.awt.*;

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.

b4ac40784ca6e05cededff5eda5a930f
b
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 oma-
wianych kształtów, mogą to być zarówno jedynie kontury, jak i figury wypełnione kolo-
rem. 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

b4ac40784ca6e05cededff5eda5a930f
b
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 argu-


ment trzeci to liczba punktów. Przykładowy sześciokąt można zatem utworzyć w na-
stę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 umiejsco-
wienie pliku (np. http://host.domena/java/obrazy/) i drugiego, określającego nazwę
pliku. Jeśli obraz znajduje się w strukturze katalogów serwera, na którym jest umiesz-
czony dokument HTML i (lub) kod apletu, wygodne będzie użycie drugiej postaci kon-
struktora. Nie trzeba wtedy tworzyć nowego obiektu klasy URL bezpośrednio — wy-
starczy wywołać jedną z dwóch metod:
getDocumentBase()

lub:
getCodeBase()

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ć ja-
ko argumentu metody drawImage rysującej obraz na powierzchni apletu. Metoda drawImage
występuje w kilku przeciążonych wersjach (ich opis można znaleźć w dokumentacji JDK),
w najprostszej postaci należy zastosować wywołanie:
gDC.drawImage (img, wspX, wspY, this);

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 373

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, nato-
miast 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 me-
todzie init wywołujemy metodę getImage, przekazując jej w argumencie lokalizację
pliku. Ta metoda zwraca obiekt klasy Image niezależnie od tego, czy wskazany plik
graficzny istnieje, czy nie. Ładowanie danych rozpocznie się dopiero w momencie pierw-
szego wywołania metody drawImage.

b4ac40784ca6e05cededff5eda5a930f
b
374 Java. Praktyczny kurs

Sama metoda drawImage działa natomiast w taki sposób, że po jej wywołaniu jest wy-
świetlana dostępna część obrazu (czyli albo nic, albo część obrazu, albo cały obraz)
i metoda kończy działanie. Jeśli cały obraz był dostępny, jest zwracana wartość true, jeśli
nie — wartość false. Jeśli obraz nie był w pełni dostępny i zwrócona została wartość
false, jest on ładowany w tle. W trakcie tego ładowania, czyli napływania kolejnych
danych z sieci, jest wywoływana metoda imageUpdate obiektu implementującego interfejs
ImageObserver, który został przekazany jako czwarty argument metody drawImage.

Ponieważ klasa JApplet implementuje ten interfejs, można jej obiekt wykorzystać jako
czwarty argument metody. Powstanie wtedy sytuacja, w której obiekt apletu jest infor-
mowany o postępach ładowania obrazu. Aby mieć możliwość kontrolowania procesu
wczytywania i wyświetlania obrazu, należy przeciążyć metodę imageUpdate z klasy
JApplet. Przykładowo: jeśli w trakcie ładowania obrazu na pasku stanu miałaby być wy-
świetlana informacja o tym procesie, należałoby zastosować kod widoczny na listingu 8.17.

Listing 8.17.
import javax.swing.JApplet;
import java.awt.*;
import java.awt.image.*;

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żniej-
szy dla nas w tej chwili — flags. Argument flags to wartość typu int, w której po-
szczególne bity informują o stanie ładowanego obrazu. Informacje o ich dokładnym
znaczeniu można znaleźć w dokumentacji JDK w opisie interfejsu ImageObserver.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 375

Najważniejszy dla nas jest bit piąty, którego ustawienie na 1 oznacza, że obraz został
całkowicie załadowany i może zostać wyświetlony w ostatecznej formie. Do zbadania
stanu tego bitu wykorzystuje się zdefiniowaną w klasie ImageObserver stałą (pole
statyczne i finalne typu int) ImageObserver.ALLBITS. Jeśli wynikiem operacji bitowej
AND między argumentem flags oraz stałą ALLBITS jest 0, oznacza to, że bit ten jest wyłą-
czony, a zatem obraz nie jest załadowany, jeśli natomiast wynik tej operacji jest różny
od 0, to bit jest włączony i dysponujemy pełnym obrazem.

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, wyko-
nana zostaje metoda showStatus ustawiająca na pasku stanu przeglądarki tekst (rysu-
nek 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 dodat-
kowo 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.

b4ac40784ca6e05cededff5eda5a930f
b
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ż ko-
rzystać 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 1.1
myszy.
void mouseEntered(MouseEvent e) Metoda wywoływana, kiedy kursor myszy wejdzie 1.1
w obszar komponentu.
void mouseExited(MouseEvent e) Metoda wywoływana, kiedy kursor myszy opuści 1.1
obszar komponentu.
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 1.1
myszy.

Każda klasa implementująca ten interfejs musi zatem zawierać definicję wszystkich
wymienionych metod, nawet jeśli nie będzie ich wykorzystywała. Szkielet apletu będzie
miał postać widoczną na listingu 8.18. Jak widać, możliwe jest reagowanie na pięć róż-
nych zdarzeń opisanych w tabeli 8.4 i w komentarzach w zaprezentowanym kodzie.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 377

Listing 8.18.
import javax.swing.JApplet;
import java.awt.event.*;

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 otrzy-
mać, wywołując metody getX i getY. Metoda getButton zwraca wartość typu int, którą
należy porównywać ze stałymi zdefiniowanymi w klasie MouseEvent:
 MouseEvent.BUTTON1,
 MouseEvent.BUTTON2,
 MouseEvent.BUTTON3,
 MouseEvent.NOBUTTON.

Pierwsze trzy określają numer przycisku, natomiast ostatnia informuje, że żaden przycisk
podczas danego zdarzenia nie był wciśnięty. Na listingu 8.19 jest widoczny przykła-
dowy aplet, który wyświetla współrzędne ostatniego kliknięcia myszą oraz informację
o tym, który przycisk został użyty.

Listing 8.19.
import javax.swing.JApplet;
import java.awt.*;
import java.awt.event.*;

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ą.

b4ac40784ca6e05cededff5eda5a930f
b
378 Java. Praktyczny kurs

}
public void paint (Graphics gDC) {
gDC.clearRect(0, 0, getSize().width, getSize().height);
gDC.drawString(tekst, 20, 20);
}
public void mouseClicked(MouseEvent evt) {
int button = evt.getButton();
switch(button){
case MouseEvent.BUTTON1 : tekst = "Przycisk 1, ";break;
case MouseEvent.BUTTON2 : tekst = "Przycisk 2, ";break;
case MouseEvent.BUTTON3 : tekst = "Przycisk 3, ";break;
default : tekst = "";
}
tekst += "współrzędne: x = " + evt.getX() + ", ";
tekst += "y = " + evt.getY();
repaint();
}
public void mouseEntered(MouseEvent evt){}
public void mouseExited(MouseEvent evt){}
public void mousePressed(MouseEvent evt){}
public void mouseReleased(MouseEvent evt){}
}

Ponieważ interesuje nas jedynie zdarzenie polegające na kliknięciu myszą, treść me-
tod niezwiązanych z nim, czyli mouseEntered, mouseExited, mousePressed, mouseReleased,
pozostaje pusta. Niemniej ich definicje muszą znajdować się w klasie Aplet, gdyż
wymaga tego interfejs MouseListener (por. lekcje 26. i 27.).

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ęp-
nie 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 odpowied-
ni 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ęd-
nych odczytujemy dzięki metodom getX i getY. Na końcu metody mouseClicked wy-
woł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óż wy-
wołujemy tam metodę addMouseListener, której w argumencie przekazujemy wskazanie
do apletu. Takie wywołanie oznacza, że wszystkie zdarzenia związane z obsługą myszy,
określone przez interfejs MouseListener, będą obsługiwane przez obiekt wskazany
w argumencie metody. Ponieważ argumentem jest referencja do obiektu apletu (choć
może być to obiekt dowolnej klasy implementującej interfejs MouseListener), w tym
przypadku informujemy po prostu maszynę wirtualną, że nasz aplet samodzielnie będzie
obsługiwał zdarzenia związane z myszą.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 379

O wywołaniu metody addMouseListener trzeba koniecznie pamiętać, gdyż jeśli jej za-
braknie, kompilator nie zgłosi żadnego błędu, a program po prostu nie będzie działał.
Jest to błąd stosunkowo często popełniany przez początkujących programistów. Jeśli
jednak będziemy o tej instrukcji pamiętać, to po skompilowaniu i uruchomieniu
apletu oraz kliknięciu pierwszego przycisku na ekranie pojawi się widok podobny do
zaprezentowanego na rysunku 8.15.

Rysunek 8.15.
Wynik działania
apletu wyświetlającego
informacje
o kliknięciach myszą

Skoro metoda addActionListener może przekazać obsługę zdarzeń dowolnemu obiektowi


implementującemu interfejs MouseListener, warto sprawdzić, jak by to wyglądało w prakty-
ce. 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świe-
tlenie na konsoli współrzędnych ostatniego kliknięcia. Kod obu klas został zaprezen-
towany 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){}
}

b4ac40784ca6e05cededff5eda5a930f
b
380 Java. Praktyczny kurs

Klasa Aplet nie implementuje w tej chwili interfejsu MouseListener, gdyż obsługa myszy
jest przekazywana innej klasie. Pozostała w niej jedynie metoda init, w której wywołuje
się metodę addActionListener. Argumentem przekazanym metodzie addActionListener
jest nowy obiekt klasy MyMouseListener. Oznacza to, że aplet ma reagować na zdarzenia
myszy, ale ich obsługa została przekazana obiektowi klasy MyMouseListener.

Klasa MyMouseListener jest klasą pakietową, może być zatem zdefiniowana w tym samym
pliku co klasa Aplet (lekcja 17.). Implementuje ona oczywiście interfejs MouseListener,
inaczej obiekt tej klasy nie mógłby być argumentem metody addActionListener.
Wewnątrz klasy MyMouseListener zostały zdefiniowane metody z interfejsu, jednak
kod wykonywalny zawiera jedynie metoda mouseClicked. Jej treść jest bardzo podobna do
kodu metody mouseClicked z poprzedniego przykładu. Jedyną różnicą jest to, że zmienna
tekst jest zdefiniowana wewnątrz tej metody i zamiast metody repaint jest wykony-
wana instrukcja System.out.println wyświetlająca na konsoli współrzędne kliknięcia.
Uruchomienie takiego apletu spowoduje, że współrzędne kliknięcia będą się pojawiały na
konsoli, tak jak jest to widoczne na rysunku 8.16.

Rysunek 8.16.
Współrzędne kliknięcia
pojawiają się na konsoli

Tym razem nie da się wyświetlić tekstu bezpośrednio w obszarze apletu, gdyż klasa
MyMouseListener nie ma do niego dostępu. Jak go uzyskać? Można by na przykład prze-
kazać referencję do obiektu apletu w konstruktorze klasy MyMouseListener (ćwiczenie 40.4
z podpunktu „Ćwiczenia do samodzielnego wykonania”). O wiele lepszym rozwiązaniem
byłoby jednak zastosowanie klasy wewnętrznej, a jeszcze lepiej anonimowej klasy we-
wnętrznej (lekcje 28. – 30.). Takie rozwiązanie zostanie pokazane już w kolejnej lekcji.

Interfejs MouseMotionListener
Interfejs MouseMotionListener pozwala na obsługiwanie zdarzeń związanych z ruchem
myszy. Zawiera definicję dwóch metod — zostały przedstawione w tabeli 8.5. Pierwsza
jest wywoływana, jeśli przycisk myszy został wciśnięty i mysz się porusza, natomiast
druga przy każdym ruchu myszy bez wciśniętego przycisku.

Tabela 8.5. Metody interfejsu MouseMotionListener


Deklaracja metody Opis Od
JDK
void mouseDragged(MouseEvent e) Metoda wywoływana podczas ruchu myszy, 1.1
kiedy wciśnięty jest jeden z przycisków.
void mouseMoved(MouseEvent e) Metoda wywoływana przy każdym ruchu myszy, 1.1
o ile nie jest wciśnięty żaden przycisk.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 381

Opierając się na przykładzie z listingu 8.19, bez problemu można napisać aplet, który
będzie wyświetlał aktualne współrzędne położenia kursora myszy. Kod realizujący to
zadanie został przedstawiony na listingu 8.21.

Listing 8.21.
import javax.swing.JApplet;
import java.awt.*;
import java.awt.event.*;

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 spraw-
dzenie, 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 poszcze-
gó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 do-
stę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ą tech-
nikę do stwierdzenia, które z klawiszy funkcyjnych były wciśnięte podczas przesu-
wania kursora myszy, jest widoczny na listingu 8.22, a efekt działania tego apletu
przedstawiono na rysunku 8.17.

Listing 8.22.
import javax.swing.JApplet;
import java.awt.*;
import java.awt.event.*;

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.

b4ac40784ca6e05cededff5eda5a930f
b
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 od-
twarza klip dźwiękowy. Występuje ona w dwóch przeciążonych wersjach:
play(URL url)
play(URL url, String name)

Pierwsza z nich przyjmuje jako argument obiekt klasy URL bezpośrednio wskazujący
na plik dźwiękowy, druga wymaga podania argumentu klasy URL wskazującego na umiej-
scowienie pliku (np. http://host.domena/java/sound/) i drugiego określającego nazwę
pliku. Jeśli plik znajduje się w strukturze katalogów serwera, na którym umieszczony
jest dokument HTML i (lub) kod apletu, wygodniejsze może być użycie drugiej po-
staci konstruktora, podobnie jak w przypadku metody getImage omawianej w lekcji 39.
Przykład apletu, który podczas uruchamiania odtwarza plik dźwiękowy, jest widoczny
na listingu 8.23.

Listing 8.23.
import javax.swing.JApplet;

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 rozpo-
czyna 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

b4ac40784ca6e05cededff5eda5a930f
b
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 li-
stingu 8.24. Po uruchomieniu apletu rozpoczyna się odtwarzanie dźwięku, a gdy aplet
kończy pracę, odtwarzanie jest zatrzymywane.

Listing 8.24.
import java.applet.Applet;
import java.applet.AudioClip;

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){}
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 385

Klasa Aplet implementuje interfejs MouseListener, a zatem zawiera definicje wszyst-


kich 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 kur-
sora myszy na konsoli.

Ćwiczenie 40.3.
Napisz aplet, który będzie odtwarzał plik dźwiękowy, kiedy użytkownik zbliży kursor
myszy na mniej niż 10 pikseli od brzegów powierzchni apletu. Wysokość oraz szerokość
obszaru apletu można uzyskać dzięki wywołaniu metody getWidth oraz getHeight
klasy JApplet (Applet).

Ćwiczenie 40.4.
Napisz aplet, który przy kliknięciu będzie odtwarzał dźwięki. Odtwarzanie powinno być
realizowane przez pakietową klasę MyAudioClip implementującą interfejs AudioClip.

Ćwiczenie 40.5.
Napisz aplet, który będzie pozwalał na rysowanie na jego powierzchni kolorowych
odcinków. Kolor każdego odcinka powinien być losowy.

b4ac40784ca6e05cededff5eda5a930f
b
386 Java. Praktyczny kurs

Aplikacje
Lekcja 41. Tworzenie aplikacji
Na początku rozdziału 8. była mowa o różnicach między aplikacją i apletem, wiadomo
zatem, że aplikacja potrzebuje do uruchomienia jedynie maszyny wirtualnej, a aplet jest
programem wbudowanym, zagnieżdżonym w innym programie, najczęściej w przeglą-
darce internetowej. Wszystkie programy, które powstawały w rozdziałach 1. – 7.,
były właśnie aplikacjami, pracującymi jednak w trybie tekstowym. W tej lekcji będzie
pokazane, w jaki sposób tworzy się aplikacje pracujące w trybie graficznym, czyli po-
pularne aplikacje okienkowe.

Pierwsze okno
W lekcji 37. powstał nasz pierwszy aplet, przedstawiony na listingu 8.1. Zobaczmy teraz,
jak napisać aplikację, która będzie wykonywała to samo zadanie, czyli wyświetli napis na
ekranie. Będzie to wymagało napisania klasy np. o nazwie PierwszaAplikacja, która
będzie dziedziczyć po klasie JFrame. Jest to klasa zawarta w pakiecie javax.swing. Alter-
natywnie można użyć również klasy Frame z pakietu java.awt, jednak jest ona uznawana
za przestarzałą. Kod pierwszej aplikacji został przedstawiony na listingu 8.26.

Listing 8.26.
import javax.swing.*;
import java.awt.*;

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 in-
strukcję:
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

b4ac40784ca6e05cededff5eda5a930f
b
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 komponen-
tem) nie powinny być obsługiwane bezpośrednio, ale mają trafiać do kolejki zdarzeń.
Dotyczy to również samego uruchamiania aplikacji. Należy użyć metody invokeLater
klasy SwingUtilities, która umieści wywołanie w kolejce zdarzeń. Argumentem tej
metody musi być obiekt implementujący interfejs Runnable, a operacja, którą chce się
wykonać, powinna się znaleźć w metodzie run tego interfejsu. Zgodnie z tym stan-
dardem metoda main powinna mieć postać:
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new PierwszaAplikacja();
}
});
}

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 poinfor-
mować 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.

b4ac40784ca6e05cededff5eda5a930f
b
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 1.1
zamknięte poprzez wywołanie metody dispose.
void windowClosing(WindowEvent e) Metoda wywoływana, kiedy następuje próba 1.1
zamknięcia okna z menu systemowego.
void windowDeactivated(WindowEvent e) Metoda wywoływana po dezaktywacji okna. 1.1
void windowDeiconified(WindowEvent e) Metoda wywoływana, kiedy okno zmieni stan 1.1
ze zminimalizowanego na normalny.
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 apli-
kacji) 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ływa-
na, 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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 389

okna. W takiej sytuacji wywoływana jest metoda dispose, która powoduje zwolnienie
zasobów związanych z oknem, zamknięcie okna i jeżeli jest to ostatnie okno aplikacji,
zakończenie pracy aplikacji.

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 wy-
prowadzi 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 odpo-
wiadać klasa MyWindowAdapter.

b4ac40784ca6e05cededff5eda5a930f
b
390 Java. Praktyczny kurs

Warto przy tym zauważyć, że ta klasa mogłaby być z powodzeniem klasą anonimową
(por. lekcja 30.), jej nazwa w przedstawionej sytuacji tak naprawdę do niczego nie jest
potrzebna. Program mógłby więc przyjąć postać widoczną na listingu 8.29.

Listing 8.29.
import javax.swing.*;
import java.awt.event.*;

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 po-
chodnej od WindowAdapter. Ponieważ klasa anonimowa jest z natury rzeczy klasą we-
wnę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 javax.swing.JApplet;
import javax.swing.event.MouseInputAdapter;
import java.awt.Graphics;
import java.awt.event.*;

b4ac40784ca6e05cededff5eda5a930f
b
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 ele-
menty menu. Pasek menu dodaje się do okna aplikacji za pomocą metody setJMenuBar,
natomiast menu do paska — przy użyciu metody add. Jak to wygląda w praktyce, zo-
brazowano na listingu 8.31.

Listing 8.31.
import javax.swing.*;

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();

b4ac40784ca6e05cededff5eda5a930f
b
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);

b4ac40784ca6e05cededff5eda5a930f
b
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 po-


szczegó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 Pozy-
cja 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 spowo-
dować, aby program reagował na wybranie jednej z pozycji. Łatwo się domyślić, że trze-
ba będzie skorzystać z jakiegoś interfejsu typu MenuListener i przekazać zdarzenia do
pewnego obiektu, być może obiektu aplikacji, być może obiektu dodatkowej klasy.

Rzeczywiście tak należy postąpić. Nie ma jednak interfejsu o nazwie MenuListener, trzeba
zatem skorzystać z interfejsu ActionListener. Jest w nim zdefiniowana tylko jedna meto-
da o nazwie actionPerformed. Do reagowania na wybór danej pozycji menu można więc
zastosować technikę przedstawioną na listingu 8.33.

Listing 8.33.
import javax.swing.*;
import java.awt.event.*;

public 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);

b4ac40784ca6e05cededff5eda5a930f
b
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 (wi-
dać to na rysunku 8.21). Sposób utworzenia menu jest analogiczny do wykorzystane-
go w poprzednich przykładach, z tą różnicą, że zmienna określająca pozycję menu
Zamknij jest prywatnym polem klasy Aplikacja. Chodzi o to, aby mieć do tej pozycji
dostęp nie tylko w obrębie konstruktora, ale w całej klasie. Po utworzeniu paska menu
(obiekt mb), samego menu (obiekt menu) oraz pozycji (obiekt miZamknij) do tej pozycji
dodaje się obsługę zdarzeń, wywołując instrukcję:
miZamknij.addActionListener(al);

Rysunek 8.21.
Menu aplikacji
z listingu 8.33

Tym samym zdarzenia związane z obsługą tego menu będą przekazywane do metody
actionPerformed obiektu al. Obiekt ten jest obiektem klasy anonimowej implementującej
interfejs ActionListener i został zdefiniowany na początku klasy Aplikacja.

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 sa-
mym 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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 395

Command. W argumencie należy przekazać wymyśloną nazwę komendy. Z kolei w kla-


sie ActionEvent znajduje się metoda getActionCommand pozwalająca na ustalenie, jaka
komenda jest powiązana z danym zdarzeniem. Aplikacja z listingu 8.33 mogłaby za-
tem 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 odczy-
tać z obiektu obsługującego zdarzenie przekazywanego metodzie actionPreformed.
Konstrukcja tej metody również przypomina tę z listingu 8.33, jednak inaczej wyglą-
da instrukcja warunkowa. Tym razem sprawdzane jest, czy komenda pobrana za po-
mocą metody getActionCommand z klasy ActionEvent odpowiada ciągowi znaków Zamknij
(czyli czy została wybrana pozycja menu z przypisaną komendą Zamknij). Jeśli tak,
zostaje wywołana metoda dispose zamykająca okno i kończąca pracę aplikacji.

15
Tak naprawdę w tym konkretnym przypadku jest to czynność nadmiarowa, gdyż jeśli nie zostanie
wywołana metoda setActionCommand, za komendę przypisaną zdarzeniu uznaje się tekst danej
pozycji menu.

b4ac40784ca6e05cededff5eda5a930f
b
396 Java. Praktyczny kurs

Menu kaskadowe
Pozycje menu można łączyć kaskadowo i uzyskiwać w ten sposób rozbudowane struk-
tury podmenu. Utworzenie tego typu konstrukcji jest możliwe poprzez dodanie do menu
innego menu, czyli przekazanie metodzie add z klasy JMenu (w postaci argumentu) in-
nego obiektu tej klasy. Przykład tego typu konstrukcji jest widoczny na listingu 8.35.

Listing 8.35.
import javax.swing.*;
import java.awt.event.*;

public 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);
}

b4ac40784ca6e05cededff5eda5a930f
b
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. Ety-
kieta menu dodawanego staje się ostatnią pozycją menu, do którego zostało ono dodane.
Jeśli więc etykietą submenu1 jest Pozycja 4, to po dodaniu submenu1 do menu etykieta
Pozycja 4 staje się ostatnią pozycją w menu. Jeśli opis nadal nie jest do końca jasny, ry-
sunek 8.22 powinien rozwiać wszelkie wątpliwości. Jest to ilustracja struktury menu
z listingu 8.34.

Rysunek 8.22.
Menu kaskadowe
z listingu 8.34

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 po-
brania tej nazwy jest wykonywana złożona instrukcja:
String text = ((JMenuItem)e.getSource()).getText();

b4ac40784ca6e05cededff5eda5a930f
b
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ż spo-
sób obsługi zdarzeń. Należy wykorzystać dodatkowy interfejs o nazwie ItemListener.
Definiuje on tylko jedną metodę o nazwie itemStateChanged, która jest wywoływana
za każdym razem, kiedy zmieni się stan komponentu (w naszym przypadku stan pozycji
menu). Kod aplikacji zawierającej przykładowe menu z możliwością zaznaczania po-
szczególnych pozycji został przedstawiony na listingu 8.36.

Listing 8.36.
import javax.swing.*;
import java.awt.event.*;

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{

b4ac40784ca6e05cededff5eda5a930f
b
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 imple-


mentacji 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 na-
leży sprawdzić, która pozycja spowodowała wygenerowanie zdarzenia: menuItem1 czy
menuItem2. Referencję uzyskuje się przez wywołanie metody getSource. Aby sprawdzić,

b4ac40784ca6e05cededff5eda5a930f
b
400 Java. Praktyczny kurs

czy dana pozycja jest zaznaczona, czy nie, należy skorzystać z kolei z metody get
State. Jeżeli zwróci ona wartość true, pozycja jest zaznaczona, jeśli false — nie jest.

Menu kontekstowe
Istnieje także możliwość wyposażenia aplikacji w menu kontekstowe. Należy wtedy sko-
rzystać z klasy JPopupMenu. Konstrukcja taka jest bardzo podobna do konstrukcji zwyczaj-
nego menu, z tą różnicą, że menu kontekstowego nie dodaje się do paska menu. Przykła-
dowa aplikacja zawierająca menu kontekstowe została przedstawiona na listingu 8.37.

Listing 8.37.
import javax.swing.*;
import java.awt.event.*;

public 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);

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 401

}
public static void main(String args[]) {
//treść metody main
}
}

Obiekt menu kontekstowego tworzy się, wywołując konstruktor klasy JPopupMenu. Kon-
struktor ten może być bezargumentowy, tak jak na powyższym listingu, lub też może
przyjmować jeden argument typu String. W tym drugim przypadku menu otrzyma
identyfikującą je nazwę. Struktura menu jest tworzona dokładnie tak samo jak w przypad-
ku wcześniej omawianych zwyczajnych menu — dodaje się po prostu kolejne obiekty
klasy JMenuItem, które staną się pozycjami menu. Dodatkowo za pomocą wywołania
metody addSeparator dodany został również separator pozycji menu. Otrzymaną strukturę
przedstawiono na rysunku 8.24. Obsługa zdarzeń jest również taka sama jak w poprzed-
nich przykładach. Obiektem obsługującym zdarzenia powiązanym z każdym z obiek-
tów JMenuItem jest obiekt klasy anonimowej pochodnej od ActionListener. W metodzie
actionPerformed sprawdzane jest, czy wybrana pozycja menu to Zamknij (czyli czy
obiektem źródłowym zdarzenia jest miZamknij). Jeśli tak, wywołana zostaje metoda
dispose obiektu aplikacji, która zwalnia zasoby związane z oknem i zamyka okno.
Ponieważ jest to jedyne okno aplikacji, czynność ta jest równoznaczna z zakończeniem
pracy całego programu.

Rysunek 8.24.
Aplikacja wyposażona
w menu kontekstowe

Aby jednak menu kontekstowe pojawiło się na ekranie, użytkownik aplikacji musi
nacisnąć prawy klawisz myszy. To oznacza, że aplikacja musi reagować na zdarzenia
związane z obsługą myszy, a jak wiadomo z lekcji 38., wymaga to implementacji inter-
fejsu MouseListener. W tym celu został użyty obiekt klasy anonimowej pochodnej od
MouseAdapter, dzięki czemu obsługiwane są metody mousePressed oraz mouseReleased.
Jest to niezbędne, gdyż menu kontekstowe jest wywoływane w różny sposób w róż-
nych systemach. W obu przypadkach metoda isPopupTrigger pozwala sprawdzić, czy
zdarzenie jest wynikiem wywołania przez użytkownika menu kontekstowego. Jeśli tak
(wywołanie isPopupTrigger zwróciło wartość true), menu jest wyświetlane w miejscu
wskazywanym przez kursor myszy.

Wyświetlenie menu odbywa się za pomocą metody show obiektu wskazywanego przez
popupMenu. Pierwszym parametrem jest obiekt, na którego powierzchni ma się pojawić
menu i względem którego będą obliczane współrzędne wyświetlania, przekazywane
jako drugi i trzeci argument. W naszym przypadku pierwszym argumentem jest po pro-

b4ac40784ca6e05cededff5eda5a930f
b
402 Java. Praktyczny kurs

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 przyci-
ski, 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”.

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 403

Klasa Component
Praktycznie wszystkie graficzne komponenty, które pozwalają na zastosowanie typo-
wych elementów środowiska okienkowego, dziedziczą, pośrednio lub bezpośrednio,
po klasie JComponent, a zatem są wyposażone w jej metody17. Metod tych jest dosyć
dużo, ich pełną listę można znaleźć w dokumentacji JDK. W tabeli 8.7 zostały zebrane
niektóre z nich, przydatne przy wykonywaniu typowych operacji. Większość została
odziedziczona po klasie Component biblioteki AWT. W kolumnie Od JDK została zawarta
informacja na temat tego, kiedy po raz pierwszy dana metoda pojawiła się w JDK.
Wartości w nawiasach oznaczają natomiast, kiedy dana metoda została redefiniowana
w klasie JComponent.

Tabela 8.7. Wybrane metody klasy Component


Deklaracja metody Opis Od JDK
void addKeyListener Ustala obiekt, który będzie obsługiwał zdarzenia 1.1
(KeyListener l) związane z klawiaturą.
void addMouseListener Ustala obiekt, który będzie obsługiwał zdarzenia 1.1
(MouseListener l) związane z klikaniem przyciskami myszy.
void addMouseMotionListener Ustala obiekt, który będzie obsługiwał zdarzenia 1.1
(MouseMotionListener l) związane z ruchami myszy.
boolean contains(int x, Sprawdza, czy komponent zawiera punkt 1.1
int y) o współrzędnych x, y.
boolean contains(Point p) Sprawdza, czy komponent zawiera punkt wskazywany 1.1
przez argument p.
Color getBackground() Pobiera kolor tła komponentu. 1.0
Rectangle getBounds() Zwraca rozmiar komponentu w postaci obiektu klasy 1.0
Rectangle.
Cursor getCursor() Zwraca kursor związany z komponentem. 1.1
Font getFont() Zwraca font związany z komponentem. 1.0
FontMetrics getFontMetrics Zwraca obiekt opisujący właściwości fontu 1.0 (1.5)
(Font font) określonego przez argument font.
Color getForeground() Zwraca pierwszoplanowy kolor komponentu. 1.0
Graphics getGraphics() Zwraca tzw. graficzny kontekst urządzenia, 1.0 (1.2)
obiekt pozwalający na wykonywanie operacji
graficznych na komponencie.
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 1.1
klasy Dimension.
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.

b4ac40784ca6e05cededff5eda5a930f
b
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, Odrysowuje wskazany obszar komponentu. 1.0
int width, int height)
void setBackground(Color c) Ustala kolor tła komponentu. 1.0 (1.2)
void setBounds(int x, int y, Ustala rozmiar i położenie komponentu. 1.1
int width, int height)
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, Ustala rozmiary komponentu. 1.1
int height)
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. Konstruk-
tor klasy JLabel może być bezargumentowy, tworzy wtedy pustą etykietę, może też
przyjmować jako argument tekst, który ma być na niej wyświetlany. Po utworzeniu
etykiety znajdujący się na niej tekst można pobrać za pomocą metody getText, natomiast
aby go zmienić, trzeba skorzystać z metody setText. Etykietę umieszcza się w oknie
lub na innym komponencie, wywołując metodę add. Prosty przykład obrazujący wykorzy-
stanie klasy JLabel jest widoczny na listingu 8.38, natomiast efekt jego działania na
rysunku 8.25.

Listing 8.38.
import javax.swing.*;

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);

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 405

add(label1);
add(label2);

setSize(320, 200);
setVisible(true);
}
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Aplikacja();
}
});
}
}

Rysunek 8.25.
Wygląd przykładowych
etykiet tekstowych

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ą dodawa-
ne 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 przy-
padku klasy JLabel, konstruktor może być bezargumentowy, powstaje wtedy przycisk
bez napisu na jego powierzchni, może również przyjmować argument klasy String.
W tym drugim przypadku przekazany napis pojawi się na przycisku. Jeśli zostanie zasto-
sowany konstruktor bezargumentowy, późniejsze przypisanie tekstu przyciskowi będzie

18
Czytelnicy zainteresowani rozkładami automatycznymi powinni zapoznać się z opisem klasy
LayoutManager oraz klas pochodnych, a także z opisem metody setLayout klasy Container.

b4ac40784ca6e05cededff5eda5a930f
b
406 Java. Praktyczny kurs

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 zaimple-
mentowanie 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

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 407

Pierwszy przycisk jest tworzony za pomocą konstruktora przyjmującego w argumen-


cie 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ła-
nie jest identyczne z omawianym w przypadku przedstawionych wcześniej etykiet. Ponie-
waż przyciski muszą reagować na kliknięcia, tworzony jest nowy obiekt (al) anonimowej
klasy implementującej interfejs ActionListener. W metodzie actionPerformed spraw-
dzamy, 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śred-
nio bądź pośrednio po klasie JTextComponent, która z kolei dziedziczy po JComponent.
Nie ma niestety miejsca na dokładne omówienie wszystkich tych komponentów,
przedstawione zostaną więc jedynie przykłady użycia dwóch podstawowych typów:
JTextField oraz JTextArea. Pierwszy z nich pozwala na wprowadzenie tekstu w jednej linii,
drugi — tekstu wielowierszowego. Ich obsługa, jak okaże się za chwilę, jest podobna.

Klasa JTextField
Klasa JTextField tworzy jednowierszowe pole tekstowe, takie jak zaprezentowane na
rysunku 8.27. Oferuje ona pięć konstruktorów przedstawionych w tabeli 8.8. Przykład
wykorzystujący pole tekstowe jest widoczny na listingu 8.40.

Tabela 8.8. Konstruktory klasy JTextField


Konstruktor Opis
JTextField() Tworzy nowe, puste pole tekstowe.
JTextField(Document doc, Tworzy nowe pole tekstowe o zadanej liczbie kolumn i zawartości,
String text, int columns) 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, Tworzy nowe pole tekstowe o zadanej liczbie kolumn zawierające
int columns) 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)

b4ac40784ca6e05cededff5eda5a930f
b
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ń wykorzystu-
jemy interfejs ActionListener i metodę actionPerformed, podobnie jak miało to miejsce
w poprzednich przykładach. Tekst zapisany w polu tekstowym odczytujemy za pomocą
metody getText, natomiast do zmiany tytułu okna aplikacji wykorzystujemy metodę
setTitle.

Sytuacja nieco się komplikuje, jeśli chcemy mieć możliwość reagowania na każdą
zmianę tekstu, jaka zachodzi w polu tekstowym JTextField. Otóż należy monitoro-
wać zmiany dokumentu (obiektu klasy Document) powiązanego z komponentem JText
Field. Trzeba zatem skorzystać z interfejsu DocumentListener. Najpierw tworzy się no-
wy obiekt implementujący ten interfejs, a następnie przekazuje się go jako argument me-
tody setDocumentListener wywołanej na rzecz obiektu zwróconego przez wywołanie

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 409

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
}
}

b4ac40784ca6e05cededff5eda5a930f
b
410 Java. Praktyczny kurs

Klasa JTextArea
Klasa JTextArea pozwala na tworzenie komponentów umożliwiających wprowadzenie
większej ilości tekstu. Oferuje ona pięć konstruktorów przedstawionych w tabeli 8.9.
Przykład wykorzystania obiektu tej klasy jest widoczny na listingu 8.42.

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, Tworzy nowe pole tekstowe zgodne z modelem dokumentu
String text, int rows, wskazanym przez argument doc, o zadanej zawartości oraz
int columns) określonej liczbie wierszy i kolumn.
JTextArea(int rows, Tworzy pusty komponent JTextArea o określonej liczbie wierszy
int columns) i kolumn.
JTextArea(String text) Tworzy komponent JTextArea zawierający tekst określony
przez argument text.
JTextArea(String text, Tworzy komponent JTextArea zawierający tekst określony przez
int rows, int columns) 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);

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 411

setSize(320, 300);
setVisible(true);
}
public void save() {
FileWriter fileWriter = null;
String text = textArea.getText();

try{
fileWriter = new FileWriter("test.txt", true);
fileWriter.write(text, 0, text.length());
fileWriter.close();
}
catch(IOException e){
//System.out.println("Błąd podczas zapisu pliku.");
}
}
public static void main(String args[]) {
//treść metody main
}
}

Tworzony jest jeden komponent klasy JTextArea (reprezentowany przez pole textArea)
oraz jeden przycisk, tak jak widać na rysunku 8.28. Aplikacja działa w taki sposób, że
po kliknięciu przycisku cały tekst z pola tekstowego zostaje zapisany w pliku o nazwie
test.txt (jeżeli pliku nie będzie na dysku, zostanie utworzony, a jeśli będzie, zawartość
pola zostanie dopisana na jego końcu). Obsługę zdarzeń związanych z klikaniem przyci-
sku zapewnia obiekt klasy anonimowej implementującej interfejs ActionListener,
dokładnie tak samo jak w przypadku wcześniej prezentowanych przykładów. Po klik-
nięciu przycisku jest zatem wywoływana metoda actionPerformed, która sprawdza,
czy sprawcą zdarzenia jest rzeczywiście przycisk button1. Jeśli tak, wywoływana jest
metoda save z klasy Aplikacja. Metoda save korzysta z obiektu klasy FileWriter do
zapisania zawartości komponentu textArea w pliku (por. lekcja 34.).

Rysunek 8.28.
Aplikacja
wykorzystująca pole
tekstowe klasy TextArea

Pola JCheckBox
Klasa JCheckBox tworzy komponenty będące polami wyboru, które umożliwiają zazna-
czanie opcji. Konstruktory klasy zostały przedstawione w tabeli 8.10, a program wyświe-
tlający sześć pól typu JCheckBox pokazano na listingu 8.43. Wygląd okna takiej aplikacji
jest widoczny na rysunku 8.29.

b4ac40784ca6e05cededff5eda5a930f
b
412 Java. Praktyczny kurs

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, Tworzy pole wyboru z ikoną, jego stan jest określony
boolean selected) przez argument selected.
JCheckBox(String text) Tworzy niezaznaczone pole wyboru z przypisanym tekstem.
JCheckBox(String text, Tworzy pole wyboru z przypisanym tekstem, którego stan jest
boolean selected) określony przez argument selected.
JCheckBox(String text, Tworzy niezaznaczone pole wyboru z przypisaną ikoną oraz tekstem.
Icon icon)
JCheckBox(String text, Tworzy pole wyboru, z przypisaną ikoną i tekstem, którego stan
Icon icon, boolean selected) 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 cb1 = new JCheckBox("Opcja 1");


JCheckBox cb2 = new JCheckBox("Opcja 2");
JCheckBox cb3 = new JCheckBox("Opcja 3");
JCheckBox cb4 = new JCheckBox("Opcja 4");
JCheckBox cb5 = new JCheckBox("Opcja 5");
JCheckBox cb6 = new JCheckBox("Opcja 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
}
}

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 413

Rysunek 8.29.
Pola JCheckBox
umieszczone w oknie
aplikacji korzystającej
z rozkładu siatkowego

Od razu widać pewną różnicę w stosunku do poprzednich przykładów: tym razem nie
są podawane żadne współrzędne miejsc, w których miałyby się znaleźć poszczególne
elementy okna. Zamiast tego zastosowano jeden z rozkładów automatycznych, tzw. roz-
kład tabelaryczny lub siatkowy (ang. grid layout). Za jego ustawienie odpowiada linia:
setLayout(new GridLayout(3, 2));

Parametrem metody setLayout jest nowy obiekt klasy GridLayout. Dzięki temu po-
wierzchnia okna aplikacji zostanie podzielona na sześć części, tabelę o trzech wierszach
i dwóch kolumnach. Komponenty — dodawane do okna aplikacji — będą od tej chwili
automatycznie umieszczane w kolejnych komórkach takiej tabeli, dzięki czemu uzyskamy
ich równomierny rozkład.

Listy rozwijane
Tworzenie list rozwijalnych umożliwia klasa o nazwie JComboBox. Jej konstruktory
zostały przedstawione w tabeli 8.11, a wybrane metody w tabeli 8.12. Oczywiście
w rzeczywistości metod jest o wiele więcej, jednak te zaprezentowane poniżej umoż-
liwiają wykonywanie podstawowych operacji. Przykład użycia listy został zobrazo-
wany w kodzie widocznym na listingu 8.44.

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;

b4ac40784ca6e05cededff5eda5a930f
b
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 Wstawia nowy element we wskazanej pozycji listy.
anObject, int index)
boolean isEditable() Zwraca true, jeśli istnieje możliwość edycji listy.
void removeAllItems() Usuwa wszystkie elementy listy.
void removeItem Usuwa wskazany element listy.
(Object anObject)
void removeItemAt(int anIndex) Usuwa element listy znajdujący się pod wskazanym indeksem.
void setEditable Ustala, czy lista ma mieć możliwość edycji.
(boolean aFlag)
void setSelectedIndex Zaznacza element listy znajdujący się pod wskazanym
(int anIndex) indeksem.
void setSelectedItem Zaznacza wskazany element listy.
(Object anObject)

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);

b4ac40784ca6e05cededff5eda5a930f
b
Rozdział 8.  Aplikacje i aplety 415

add(cmb);
add(label);

setSize(340, 200);
setVisible(true);
}
public static void main(String args[]) {
//kod metody main
}
}

Została tu utworzona aplikacja zawierająca jedną listę rozwijaną oraz jedną etykietę, tak
jak jest to widoczne na rysunku 8.30. Działanie programu jest następujące: po wybraniu
z listy nowej pozycji przypisany jej tekst pojawi się na etykiecie.

Rysunek 8.30.
Wygląd aplikacji
z listingu 8.44

Sposób utworzenia listy oraz etykiety nie powinien budzić żadnych wątpliwości, odbywa
się to podobnie jak we wcześniejszych przykładach z bieżącej lekcji. W przypadku li-
sty została użyta składnia typów uogólnionych, dzięki czemu wiadomo, że na liście
będą umieszczane elementy typu String (ciągi znaków). Elementy listy są dodawane
za pomocą metody addItem z klasy JComboBox. Zdarzenia nadchodzące z listy obsługuje
obiekt klasy anonimowej implementującej interfejs ActionListener (obiekt reprezen-
towany przez zmienną al). Jest w nim oczywiście zdefiniowana tylko jedna metoda —
actionPerformed. Za pomocą wywołania cmb.getSelectedIndex() pobiera ona naj-
pierw indeks zaznaczonego elementu listy. Jeśli okaże się, że indeks ten jest mniejszy
od 1 (co by oznaczało, że albo nie został zaznaczony żaden z elementów, albo też za-
znaczony jest element o indeksie 0), metoda kończy działanie, wywołując instrukcję
return. Jeśli indeks jest większy od 0, pobierany jest tekst przypisany zaznaczonemu
elementowi listy:
String itemText = cmb.getSelectedItem().toString();

który następnie jest używany jako argument metody setText obiektu etykiety:
label.setText(itemText);

b4ac40784ca6e05cededff5eda5a930f
b
416 Java. Praktyczny kurs

Ć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, zapi-
sywanie, modyfikację treści).

b4ac40784ca6e05cededff5eda5a930f
b
Skorowidz
A D
adapter MouseInputAdapter, 390 deklaracja, 24
aplety, 351, 354 metody, 120
konstrukcja, 355 tablicy, 77, 80, 85
parametry, 356 typu uogólnionego, 345
tworzenie, 352 zmiennej typu klasowego, 93
aplikacja, 351, 386 deklaracje
Eclipse, 6 proste, 24
jEdit, 6 wielu zmiennych, 25
NetBeans, 6 dekrementacja, 35
Notepad++, 6 domyślna strona kodowa, 293
argumenty domyślne wartości typów danych, 99
finalne, 162 dookreślanie typu, 341
konstruktorów, 111 dostęp
metod, 100 chroniony, protected, 133
ASCII, 270 do katalogu, 298
do klasy wewnętrznej, 253
B do klasy zewnętrznej, 245
do obiektu, 248
blok do składowych, 261
default, 56 prywatny, private, 131
else, 48 publiczny, public, 130
finally, 192 dynamiczna zmiana pojemności, 327
switch, 55 dynamiczny wektor elementów, 328
try…catch, 165, 169–171, 180 działania arytmetyczne, 34
błąd działanie
arytmetyczny, 179 etykietowanej instrukcji break, 72
implementacji interfejsu, 234 etykietowanej instrukcji continue, 70, 72
kompilacji, 31, 39, 127, 143, 197, 256 konstruktorów odziedziczonych, 126
w pętli while, 274 operatora dekrementacji, 37
w programie, 275 operatora inkrementacji, 36
bufor, 315 pętli for, 77
przeciążonej metody, 203
C dziedziczenie, 121, 122
interfejsów, 235, 238
ciąg po klasie zewnętrznej, 252
formatujący, 289 dzielenie przez zero, 173
znaków quit, 330 dźwięki, 383
czcionki, 359

b4ac40784ca6e05cededff5eda5a930f
b
418 Java. Praktyczny kurs

E if…else if, 51
Math.sqrt(), 50
edytor tekstowy, 6 return, 97
elementy tablic, 76 switch, 53–57
elipsa, 369 System.out.print, 33, 87
enkapsulacja, 134 System.out.println, 28
etykiety, 71 throw, 183, 190
puste, 405 instrukcje sterujące, 47
tekstowe, 404, 405 interfejs, 222, 224
ActionListener, 393, 397
AudioClip, 383
F DocumentListener, 408
figury geometryczne, 366 Drawable, 224, 228, 229
finalne Iterable, 333–335
argumenty metod, 162 Iterator, 335
klasy, 156 jako typ zwracany, 254
metody, 161 MouseListener, 376–380, 385, 390, 401
pola, 157, 159 MouseMotionListener, 380
formatowanie, 290 WindowListener, 387
interfejsy funkcjonalne, 261, 264
G
J
grafika, 366
jawna konwersja typu, 197
JDK, Java Development Kit, 6
H JRE, Java Runtime Environment, 6, 10
hermetyzacja, 134
hierarchia
klas, 224
K
wyjątków, 176 katalogi, 298, 304
pobranie zawartości, 298
tworzenie, 302
I usuwanie, 304
iloczyn logiczny, 43 klasa, 17, 91
implementacja ActionEvent, 394, 398
interfejsu, 223 Aplet, 365
interfejsu MouseListener, 401 Applet, 352, 383
wielu interfejsów, 230 ArrayList, 328
importowanie pakietu, 271 BufferedInputStream, 321
inferowanie typu, 341 BufferedOutputStream, 321
informacje BufferedReader, 272, 320
o kliknięciach, 379 CheckBoxMenu, 398
o stanie załadowania obrazu, 375 Color, 363
inicjalizacja Component, 403
obiektu, 110 Console, 288, 289
pól finalnych, 160 Dane, 118
tablicy, 80, 85 DivideByZeroException, 190
inkrementacja, 35 Double, 278
instalacja Exception, 183
JDK, 9 File, 295, 302
w systemie Linux, 11 FileInputStream, 316
w systemie Windows, 10 FileOutputStream, 318
instrukcja, 23 FileReader, 316, 318
break, 55, 66–68 FileWriter, 318, 411
continue, 68–71 Font, 359
if…else, 47 FontMetrics, 361

b4ac40784ca6e05cededff5eda5a930f
b
Skorowidz 419

Frame, 386 pochodne, 225


GeneralException, 189 potomne, 122
Graphics, 362, 366 prywatne, 251
GraphicsEnvironment, 360 publiczne, 141, 250
ImageObserver, 375 statyczne, 260
InputStream, 270, 271, 272 statyczne zagnieżdżone, 239, 260
Integer, 277 wewnętrzne, 141, 238, 240, 246, 249
JApplet, 352, 374 wewnętrzne lokalne, 244
JButton, 405 zagnieżdżone, 254
JCheckBox, 411 zewnętrzne, 241, 245
JCheckBoxMenuItem, 398, 399 klasyfikacja klas, 250
JComboBox, 413, 415 kod
JFrame, 386 ASCII, 270
JLabel, 404 pośredni, 15
JMenu, 391, 396 Unicode, 293
JMenuBar, 391 kodowanie, 292
JMenuItem, 391, 392 kolory, 362
JPopupMenu, 400 koło, 369
JTextArea, 410 komenda
JTextComponent, 407 java, 13
JTextField, 407, 409 javac, 13
Main, 94 komentarz, 19
Matcher, 301 blokowy, 20
MouseEvent, 377, 382 liniowy, 21
MouseInputAdapter, 390 kompilacja, 14, 15
MouseMotionAdapter, 390 kompilacja wyrażenia regularnego, 301
MyFilenameFilter, 300, 301 kompilator, 6, 15
MyMouseListener, 380 kompilator javac, 7, 14
MyWindowAdapter, 389 komponenty, 402
Object, 201 komunikat o błędzie, 175, 237
OutputStreamWriter, 293 konflikt nazw, 232, 237, 238
Pattern, 301 koniec pliku, 314
Polygon, 372 konsola, 288
PrintStream, 291 konsola systemowa, 12
PrintWriter, 293, 294 konstrukcja apletu, 355
Punkt, 93, 103 konstruktor, 110, 212
Punkt3D, 124 argumenty, 111
RandomAccessFile, 307–312 bezargumentowy, 111
Reader, 272 domyślny, 93, 218
RuntimeException, 176, 177 klasy bazowej, 125
Scanner, 286–288 klasy potomnej, 125
Shape, 208, 213 przeciążanie, 112
Stack, 331 wywoływanie metod, 115, 219
StreamTokenizer, 279–286 konstruktory klasy
SwingUtilities, 387 JCheckBox, 412
Tablica, 167 ComboBox, 413
uruchomieniowa, 108 JTextArea, 410
WindowAdapter, 389 JTextField, 407
klasy OutputStreamWriter, 293
abstrakcyjne, 212, 223 kontenery, 323, 339
anonimowe, 257, 264 kontrola typów, 330, 337, 348
bazowe, 122 konwersja typów prostych, 195, 196, 199
chronione, 251 kopiowanie plików, 312, 314, 320
finalne, 156 kroje pisma, 358, 359
kontenerowe, 328
pakietowe, 141, 249, 250

b4ac40784ca6e05cededff5eda5a930f
b
420 Java. Praktyczny kurs

L hasNextInt, 287
imageUpdate, 374
liczba, 277 invokeLater, 387
linie, 368 list, 298
lista czcionek, 359, 361 main, 106
listy rozwijane, 413 mkdir, 303
mkdirs, 303
mouseClicked, 378, 380, 385
M mouseDragged, 380
maszyna wirtualna, 6 next, 335
menu, 391 nextLine, 287
kaskadowe, 396, 397 nextToken, 280–282
kontekstowe, 400, 401 paint, 362
umożliwiające zaznaczanie, 399 parseDouble, 278
metoda parseInt, 277
actionPerformed, 393, 408 play, 385
actionPreformed, 395 printf, 289
add, 330, 392 read, 270, 271, 313
addActionListener, 379 readLine, 272, 275
addMouseListener, 379, 385 remove, 335
addWindowListener, 387–390 resize, 325, 327
clearRect, 352 save, 411
createNewFile, 302 set, 325, 326
delete, 304 setActionCommand, 395
dispose, 390, 395 setBounds, 405
draw, 213 setColor, 362
drawImage, 373 setDocumentListener, 408
drawOval, 369 setFont, 362
drawPolygon, 371 setJMenuBar, 392
drawRectangle, 370 setLayout, 405
drawShape, 215 setSize, 386
drawString, 386 setText, 405
exists, 303, 310 show, 345, 401
fillOval, 369 size, 325
fillPolygon, 371 start, 286
fillRectangle, 370 stringWidth, 362
finalize, 118 super, 127
get, 326, 343 System.gc, 118
getActionCommand, 395 toString, 202
getAllFonts, 360 windowClosing, 389
getAudioClip, 384 write, 313
getAvailableFontFamilyNames, 360 metody, 95
getButton, 377 argumenty, 100
getCodeBase, 372 domyślne, 227, 229
getDocumentBase, 372 finalne, 161
getFontMetrics, 362 interfejsu DocumentListener, 409
getHeight, 362 interfejsu Iterable, 334
getImage, 372, 373 interfejsu MouseListener, 376
getItem, 397 interfejsu MouseMotionListener, 380
getLocalGraphicsEnvironment, 360 interfejsu WindowListener, 388
getMessage(), 174 klasy anonimowej, 259
getParameter, 357 klasy ArrayList, 329
getSource, 394, 399 klasy Component, 403, 404
getValues, 347 klasy Console, 289
getX, 377 klasy File, 296, 297, 298
getY, 377 klasy FileInputStream, 316
hasNext, 335 klasy FileOutputStream, 318

b4ac40784ca6e05cededff5eda5a930f
b
Skorowidz 421

klasy FileReader, 317 odczyt danych z pliku, 309


klasy FileWriter, 318 odniesienie, 27
klasy Graphics, 366 odśmiecacz, 120
klasy InputStream, 270 odtwarzanie dźwięków, 383
klasy JComboBox, 414 odwołanie
klasy PrintStream, 291, 292 do nieistniejącego elementu, 166
klasy RandomAccessFile, 307, 308 do prywatnego pola, 246
klasy Scanner, 286 do pustej zmiennej obiektowej, 179
klasy Stack, 331 odzyskiwanie pamięci, 118
prywatne, 210 okno wyboru składników JDK, 10
przeciążanie, 108, 144 opcja encoding, 31, 293
przesłanianie, 146
operacje
statyczne, 152, 277
na plikach, 306
wywołanie polimorficzne, 208, 220, 259
wywoływane w konstruktorach, 115, 219 na tablicach, 74
zwracanie wyniku, 97 na zmiennych, 33
mnożenie liczb, 284 operator
model RGB, 363 +, 28
mysza, 376 dekrementacji, 35
inkrementacji, 35
kropki, 94
N new, 81, 85, 93
narzędzia, 6 warunkowy, 57
narzędzia JDK, 11 operatory
nawias arytmetyczne, 34
kątowy, 343 bitowe, 41, 42
klamrowy, 47, 95, 170 logiczne, 42, 43
kwadratowy, 75, 80, 82 porównywania, 44, 45
okrągły, 170, 196 przypisania, 44
nazwa
klasy, 92
pakietu, 138 P
pliku, 92
pakiet, 137, 250
zmiennych, 26
java.awt, 352, 358
negacja logiczna, 43
nieprawidłowa implementacja interfejsów, 234 java.io, 271
niewłaściwy typ danych, 340, 344 java.util, 328
niszczenie obiektu, 121 javax.swing, 352, 402
JDK, 6
parametr
O mode, 308
obiekt, 17, 94 this, 388
file, 298 parametry
InputStreamReader, 272 apletu, 356
jako argument, 102 zdarzenia, 381
klasy wewnętrznej, 246, 249 pasek menu, 391
klasy zewnętrznej, 249 pętla, 59
outp, 294 do…while, 63, 64
strTok, 284 for, 59–61
System.in, 269, 272, 287 foreach, 65, 333
obsługa while, 62, 63, 273
dźwięków, 383 pętle zagnieżdżone, 70, 82
konsoli, 288 pierwiastki równania kwadratowego, 284
menu, 391, 394 plik, 304
myszy, 376 index.html, 354
okna, 390 Main.class, 15
przycisków, 405
Main.java, 14, 95
zdarzeń, 389, 394, 397

b4ac40784ca6e05cededff5eda5a930f
b
422 Java. Praktyczny kurs

pliki
dźwiękowe, 383
R
kopiowanie, 312, 314, 320 referencja, reference, 27
odczyt danych, 309, 316 referencje do obiektu, 102, 114, 207
o dostępie swobodnym, 307 reprezentacja liczb, 42
operacje strumieniowe, 316 RGB, 363
tworzenie, 302 rodzaje
usuwanie, 304 klas wewnętrznych, 246, 249
zapis danych, 310, 318 pól tekstowych, 407
pobranie wyjątków, 173
tokena, 281 rodziny czcionek, 361
zawartości katalogu, 298 rozmiar tablicy, 78, 326
pola, 91, 149 równanie kwadratowe, 50, 284, 286
finalne, 157 rysowanie
finalne typów prostych, 158 elips, 369
finalne typów referencyjnych, 159 figur, 366
interfejsów, 226 kół, 369
JCheckBox, 411 linii, 368
niezainicjowane, 98 prostokątów, 368, 370
statyczne, 153 punktów, 368
tekstowe, 407, 408 wielokątów, 370, 371
pole rzeczywisty typ obiektu, 204
nval, 281 rzutowanie
sval, 281 na interfejs, 256
ttype, 280 na typ Object, 201
polecenie na typy interfejsowe, 254
chcp, 30 na typy klasowe, 257
cmd, 12 obiektów, 195
import, 138 typów obiektowych, 128, 197, 201, 341
javac, 14 w górę, 206
polimorficzne wywoływanie metod, 208, 220, 259
polimorfizm, 195, 206
polskie znaki, 292 S
ponowne zgłaszanie wyjątków, 187 sekcja finally, 190
poprawność danych, 165 sekwencja ucieczki, 32
późne wiązanie, 204, 206 sekwencje znaków specjalnych, 32
praklasa, 202 składnia
priorytety operatorów, 45 ExtendedOutside.Inside, 253
problem kontroli typów, 337 Outside.Inside, 253
programowanie obiektowe, 91, 195 składowe
przechowywanie wielu danych, 323 klas wewnętrznych, 242
przechwytywanie wielu wyjątków, 177 klasy, 92
przeciążanie RGB, 364
konstruktorów, 112 statyczne, 152
metod, 108 skrypt startowy powłoki, 13
przeglądanie kontenerów, 333 słowo kluczowe
przekroczenie zakresu wartości, 41 else, 47
przesłanianie extends, 235
metod, 144–149 final, 156
pól, 146, 149, 151 implements, 226
przyciski, 405 package, 137
przypisanie, 24 super, 211
punkty, 368 this, 114, 116
void, 96, 100

b4ac40784ca6e05cededff5eda5a930f
b
Skorowidz 423

specyfikator dostępu, 129 etykiet, 405


private, 131 interfejsów, 222
protected, 133 katalogów, 302
public, 130 klas wewnętrznych, 239
sprawdzanie poprawności danych, 165 klasy anonimowej, 257–259
stała list rozwijalnych, 413
EXIT_ON_CLOSE, 387 menu, 392
napisowa, 276 nieregularnej tablicy dwuwymiarowej, 86
stałe klasy obiektów, 105
Color, 363 obiektów klas wewnętrznych, 248
MouseEvent, 382 obiektu, 96, 98
stan zmiennej iteracyjnej, 60 obiektu strTok, 281
standard obiektu wyjątku, 183
CP1250, 292 pakietów, 137
CP852, 293 paska menu, 394
Unicode, 30, 293 plików, 302
standardowe tablicy, 80, 82
wejście, 269 wielu klas wewnętrznych, 240
wyjście, 279 własnych wyjątków, 188
statyczność klasy wewnętrznej, 260 tymczasowa zmienna referencyjna, 395
sterta, heap, 94 typ
stos, stack, 94 boolean, 19
stosowanie uogólnień, 341 byte, 18
strona kodowa, 293 char, 19
strona kodowa 1250, 30 double, 19
struktura float, 19
programu, 13 int, 18
tablicy, 74 long, 18
tablicy dwuwymiarowej, 80 Object, 201
strumieniowe operacje na plikach, 316 short, 18
strumień wejściowy, 269 typy
suma logiczna, 43 arytmetyczne całkowitoliczbowe, 18
symbol T, 343 arytmetyczne zmiennopozycyjne, 19
system plików, 295 danych, 16
system wejścia-wyjścia, 269 generyczne, 336
obiektowe, 26, 197
proste, primitive types, 18
Ś uogólnione, 336
ścieżka dostępu do katalogu, 298
średnik, 258
środowisko
U
programistyczne, 6 Unicode, 293
uruchomieniowe, 7, 10 uogólnianie metod, 345
uogólnienia, 341
uruchamianie apletu, 354
T usuwanie
tablice, 74 katalogów, 304
dwuwymiarowe, 80 komentarzy, 256
dynamiczne, 326, 328 plików, 304
nieregularne, 84, 89
tekst, 271
terminal, 12
W
testowanie apletów, 354 wartości
tokeny, 279, 280 domyślne pól, 98
tworzenie parametru mode, 308
apletu, 352 pola ttype, 280
aplikacji, 386

b4ac40784ca6e05cededff5eda5a930f
b
424 Java. Praktyczny kurs

wartość wywołania
aktualnego tokena, 280 konstruktorów, 216
null, 27, 120, 275 metod, 115, 219
warunek zakończenia pętli, 274 metod klas pochodnych, 204
wbudowane typy danych, 18 metod klasy wewnętrznej, 247
wczesne wiązanie, 208 metod nieistniejących, 225
wczytywanie metod przesłoniętych, 147
danych, 277, 283, 286, 288 złożone konstruktorów, 219
grafiki, 372
tekstu, 271, 273, 277
wersje Javy, 7 Z
wiązanie zagnieżdżanie bloków try…catch, 180, 186
czasu wykonania, 206 zakończenie działania programu, 274
dynamiczne, 206 zakres
wielokąty, 370 wartości zmiennej, 40
wiersz typów arytmetycznych, 18
poleceń, 12 typów zmiennoprzecinkowych, 19
tekstu, 273 zapis do pliku, 310, 318
wirtualna maszyna Javy, 15 zaznaczanie pozycji, 399
właściwość length, 78, 83 zdarzenia
wprowadzanie, Patrz wczytywanie myszy, 377
współrzędne związane z oknem, 387
biegunowe, 134 zgłaszanie
kliknięcia, 380 wyjątku, 182
wybór składników JDK, 10 ponowne wyjątku, 185
wyjątek, exception, 165, 169 zmiana
ArithmeticException, 175, 179, 181 czcionki, 31
ArrayIndexOutOfBoundsException, 76, 165, strony kodowej, 30
169, 324 zmienna środowiskowa
ClassCastException, 205, 206, 337 CLASSPATH, 139, 140
DivideByZeroException, 189 PATH, 13
FileNotFoundException, 308, 310 zmienne, 23, 102
IOException, 271, 303 referencyjne, 94
NullPointerException, 179, 181, 276 typów odnośnikowych, 26
NumberFormatException, 277, 278, 365 znacznik
PatternSyntaxException, 300 <applet>, 353
UnsupportedEncodingException, 293, 294 <object>, 353
UnsupportedOperationException, 335 znaczniki formatujące printf, 290
wyjątki własne, 182, 188 znak %, 289
wykonanie programu, 15 znaki
wykorzystanie polskie, 29
kontenerów, 339 specjalne, 32
obiektów, 140 zwracanie
pakietów, 139 wielu wartości, 346
wyrażenia wyniku, 97
lambda, 266
regularne, 299–302
wyświetlanie
elementów tablicy nieregularnej, 87
menu, 401
napisu, 386
polskich znaków, 294
przycisków, 405
systemowego komunikatu, 175
wartości zmiennych, 27
zawartości katalogu, 299
zawartości tablicy, 81

b4ac40784ca6e05cededff5eda5a930f
b
b4ac40784ca6e05cededff5eda5a930f
b

You might also like