Spis treści Wstęp .............................................................................................. 5 Rozdział 1. Podstawy ......................................................................................... 9 Instalacja JDK .................................................................................................................. 9 Instalacja w systemie Windows ................................................................................ 10 Instalacja w systemie Linux ..................................................................................... 11 Przygotowanie do pracy z JDK ................................................................................ 11 Podstawy programowania ............................................................................................... 13 Lekcja 1. Struktura programu, kompilacja i wykonanie ........................................... 13 Lekcja 2. Podstawy obiektowości i typy danych ...................................................... 16 Lekcja 3. Komentarze ............................................................................................... 19 Rozdział 2. Instrukcje języka ............................................................................ 23 Zmienne .......................................................................................................................... 23 Lekcja 4. Deklaracje i przypisania ........................................................................... 24 Lekcja 5. Wyprowadzanie danych na ekran ............................................................. 27 Lekcja 6. Operacje na zmiennych ............................................................................. 33 Instrukcje sterujące ......................................................................................................... 47 Lekcja 7. Instrukcja warunkowa if…else ................................................................. 47 Lekcja 8. Instrukcja switch i operator warunkowy ................................................... 53 Lekcja 9. Pętle .......................................................................................................... 59 Lekcja 10. Instrukcje break i continue ...................................................................... 66 Tablice ............................................................................................................................ 74 Lekcja 11. Podstawowe operacje na tablicach .......................................................... 74 Lekcja 12. Tablice wielowymiarowe ........................................................................ 79 Rozdział 3. Programowanie obiektowe. Część I ................................................. 91 Podstawy ........................................................................................................................ 91 Lekcja 13. Klasy, pola i metody ............................................................................... 91 Lekcja 14. Argumenty i przeciążanie metod .......................................................... 100 Lekcja 15. Konstruktory ......................................................................................... 110 Dziedziczenie ............................................................................................................... 121 Lekcja 16. Klasy potomne ...................................................................................... 122 Lekcja 17. Specyfikatory dostępu i pakiety ............................................................ 129 Lekcja 18. Przesłanianie metod i składowe statyczne ............................................ 144 Lekcja 19. Klasy i składowe finalne ....................................................................... 156 4 Java. Praktyczny kurs Rozdział 4. Wyjątki .......................................................................................... 165 Lekcja 20. Blok try…catch ..................................................................................... 165 Lekcja 21. Wyjątki to obiekty ................................................................................ 173 Lekcja 22. Własne wyjątki ..................................................................................... 182 Rozdział 5. Programowanie obiektowe. Część II .............................................. 195 Polimorfizm .................................................................................................................. 195 Lekcja 23. Konwersje typów i rzutowanie obiektów .............................................. 195 Lekcja 24. Późne wiązanie i wywoływanie metod klas pochodnych ..................... 204 Lekcja 25. Konstruktory oraz klasy abstrakcyjne ................................................... 212 Interfejsy ....................................................................................................................... 222 Lekcja 26. Tworzenie interfejsów .......................................................................... 222 Lekcja 27. Wiele interfejsów .................................................................................. 230 Klasy wewnętrzne ........................................................................................................ 238 Lekcja 28. Klasa w klasie ....................................................................................... 238 Lekcja 29. Rodzaje klas wewnętrznych i dziedziczenie ......................................... 246 Lekcja 30. Klasy anonimowe i zagnieżdżone ......................................................... 254 Rozdział 6. System wejścia-wyjścia ................................................................ 269 Lekcja 31. Standardowe wejście ............................................................................ 269 Lekcja 32. Standardowe wejście i wyjście ............................................................. 279 Lekcja 33. System plików ...................................................................................... 295 Lekcja 34. Operacje na plikach .............................................................................. 306 Rozdział 7. Kontenery i typy uogólnione .......................................................... 323 Lekcja 35. Kontenery ............................................................................................. 323 Lekcja 36. Typy uogólnione ................................................................................... 336 Rozdział 8. Aplikacje i aplety ......................................................................... 351 Aplety ........................................................................................................................... 351 Lekcja 37. Podstawy apletów ................................................................................. 351 Lekcja 38. Kroje pisma (fonty) i kolory ................................................................. 358 Lekcja 39. Grafika .................................................................................................. 366 Lekcja 40. Dźwięki i obsługa myszy ...................................................................... 376 Aplikacje ...................................................................................................................... 386 Lekcja 41. Tworzenie aplikacji .............................................................................. 386 Lekcja 42. Komponenty ......................................................................................... 402 Skorowidz .................................................................................... 417 Wstęp Krótka historia Javy Trudno znaleźć osobę interesującą się zagadnieniami informatycznymi, która nie słyszałaby o Javie. Java jest językiem, który w bardzo krótkim czasie zdobył popularność i uznanie programistów na całym świecie. Dawniej kojarzono go głównie z internetem i siecią WWW, a także z telefonami komórkowymi, choć tak naprawdę zawsze był to doskonały obiektowy (zorientowany obiektowo) język programowania, nadający się do różnorodnych zastosowań: od krótkich apletów do bardzo poważnych i rozbudowanych aplikacji. Jego początki były jednak dużo skromniejsze. Zapewne wielu Czytelnikom trudno będzie w to uwierzyć, ale Java była pierwotnie znana jako Oak (Dąb) i miała być językiem programowania służącym do sterowania urządzeniami elektronicznymi powszechnego użytku, czyli pralkami, lodówkami czy kuchenkami mikrofalowymi, a więc praktycznie każdym urządzeniem, które zawiera mikroprocesor. Stąd właśnie wywodzi się jedna z największych zalet Javy, czyli przenośność — ten sam program, przynajmniej teoretycznie, będzie bowiem działał na różnych urządzeniach i platformach sprzętowo-systemowych. Będzie go więc można uruchomić i na komputerze PC, i na Macintoshu, i w systemie Windows, i w Uniksie. Takie były właśnie początki Javy w roku 1990, czyli dwadzieścia kilka lat temu. Język został opracowany przez zespół kierowany przez Jamesa Goslinga w firmie Sun Microsystems, choć w rzeczywistości prace były prowadzone na Uniwersytecie Stanford. Projekt ten był gotowy bardzo szybko, już w roku 1991, jednak tak naprawdę aż do roku 1994 nie udało się go spopularyzować. Z tego też powodu prace zostały zawieszone. Historia informatyki pełna jest jednak przypadków. Były to lata ogromnej ekspansji i wręcz eksplozji internetu. Nagle okazało się, że Oak przecież doskonale sprawdzałby się w tak różnorodnym środowisku, jakim jest globalna sieć. W ten oto sposób w roku 1995 ujrzała światło dzienne Java, a to, co nastąpiło później, zaskoczyło chyba wszystkich, łącznie z twórcami tego języka. Java niezwykle szybko, nawet jak na technologię informatyczną, została zaakceptowana przez społeczność internautów i programistów na całym świecie. Z pewnością nie można tu pominąć wpływu kampanii marketingowej producenta, nie osiągnęłaby ona jednak sukcesu, gdyby nie niewątpliwe zalety tego języka. Java 6 Java. Praktyczny kurs to doskonale skonstruowany język programowania, który programistom przypada do gustu zwykle już przy pierwszym kontakcie. Od Javy po prostu nie ma już odwrotu. Obecnie to dojrzała, choć wciąż rozwijana technologia, z której korzystają rzesze programistów na całym świecie. Narzędzia Do programowania w Javie będzie potrzebne odpowiednie środowisko programistyczne zawierające niezbędne narzędzia: minimum to kompilator oraz tzw. maszyna wirtualna. Wszystkie przykłady prezentowane w tej książce są oparte na pakiecie JDK (Java Development Kit). Można skorzystać z wersji sygnowanej przez oficjalnego producenta Javy — firmę Oracle1 — lub rozwijanej na zasadach wolnego oprogramowania wersji OpenJDK. Wersja oficjalna dostępna jest pod adresami http://java.sun.com (po wpisaniu tego adresu nastąpi przekierowanie do domeny oracle.com) i http://www.java.com, a OpenJDK pod adresem http://openjdk.java.net/. Najlepiej korzystać z jak najnowszej wersji JDK, tzn. 6 (1.6), 7 (1.7), 8 (1.8) lub wyższej (o ile taka będzie dostępna2), choć podstawowe przykłady będą działać nawet na wiekowej już wersji 1.1. Jeśli będą stosowane funkcje dostępne jedynie w konkretnej wersji środowiska, będzie to zaznaczane w tekście. Do testowania apletów przedstawionych w rozdziale 8. można użyć dowolnej przeglądarki internetowej obsługującej język Java lub zawartej w JDK aplikacji appletviewer. Większość obecnie dostępnych na rynku przeglądarek udostępnia Javę poprzez mechanizm wtyczek, umożliwiając zastosowanie najnowszych wersji JRE (Java Runtime Environment), czyli środowiska uruchomieniowego. Oprócz JDK będzie potrzebny jedynie dowolny edytor tekstowy umożliwiający wpisywanie tekstu programów. Może to być nawet tak prosty produkt jak np. Notatnik z Windowsa. Lepszym rozwiązaniem byłby jednak bardziej rozbudowany produkt oferujący takie udogodnienia jak kolorowanie składni i numerowanie wierszy kodu. Dla środowiska Windows można polecić np. doskonały i darmowy Notepad++ (http://notepad-plus-plus.org/). Dobrym rozwiązaniem będzie też wieloplatformowy jEdit (http://www. jedit.org/), który został napisany w… Javie. Istnieje również możliwość zastosowania zintegrowanych graficznych środowisk programistycznych, takich jak Eclipse (http://www.eclipse.org/) czy NetBeans (http://netbeans. org/), które ułatwiają projektowanie aplikacji (oba zostały napisane w Javie). Dla osób początkujących lepszy byłby jednak najprostszy edytor tekstowy — takie rozwiązanie pozwoli najpierw poznać dobrze sam język, a dopiero potem bardziej zaawansowane środowiska programistyczne. 1 Pierwotny producent — Sun Microsystems — został zakupiony przez Oracle w 2009 roku. Tym samym obecnie to Oracle oficjalnie odpowiada za rozwój Javy. 2 W trakcie powstawania tej książki kolejna wersja Javy była zapowiadana na rok 2016, biorąc jednak pod uwagę wcześniejszą historię języka, jest mało prawdopodobne, aby taki termin został dotrzymany. Wstęp 7 Wersje Javy Pierwsza wersja Javy pojawiła się w roku 1995 i nosiła numer 1.0, większą popularność zdobyła jednak dopiero usprawniona wersja 1.1 z roku 1997. Po kolejnych dwóch latach (rok 1999) światło dzienne ujrzała Java 1.2, która ze względu na znaczne usprawnienia (zawierała już wtedy ok. 60 pakietów i ponad 1500 klas) została nazwana Platformą Java 2 (ang. Java 2 Platform). W ramach projektu Java 2 powstały trzy wersje narzędzi JDK i JRE3: 1.2 (1999), 1.3 (2000) i 1.4 (2002), a każda z nich miała od kilku do kilkunastu podwersji. Kolejnym krokiem w rozwoju projektu była wersja 1.5 (rok 2004), która, jak się wydaje, ze względów czysto marketingowych została przemianowana na 5.0; określa się ją również mianem Java 5. W 2006 roku pojawiła się Java 6 (1.6) zawierająca nowe rozwiązania, takie jak obsługa typów uogólnionych (ang. generics). Zrezygnowano też wtedy z używania określenia Java 2 Platform. W roku 2010 pojawiła się kolejna wersja — 7 (1.7). Najnowsza w chwili opracowywania materiałów do tej książki była wersja 8 (czyli inaczej 1.8) z roku 2014. Dodatkowo JDK występuje w trzech osobnych edycjach: standardowej (ang. Standard Edition), dla dużych organizacji (ang. Enterprise Edition) i dla urządzeń mobilnych (ang. Mobile Edition); są one oznaczane skrótami SE, EE i ME. Określenie Java SE 8 oznacza w związku z tym platformę Java Standard Edition w wersji 8. Warto też zauważyć, że wewnętrzna numeracja narzędzi (widoczna np. w niektórych opcjach kompilatora javac) wciąż bazuje na „starej”, logicznej numeracji (Java 2 5.0 oznacza to samo co Java 1.5, Java 6 — to samo co Java 1.6, Java 7 — to samo co Java 1.7, a Java 8 — to samo co Java 1.8). 3 Java Runtime Environment — środowisko uruchomieniowe potrzebne do uruchamiania programów w Javie. Java. Praktyczny kurs w w w .e bo o k4 al l.p l 8 Rozdział 1. Podstawy � Przed wypłynięciem na szerokie wody programowania w Javie�eba zapoznać się z pod­ stawami. W tym rozdziale zostanie pokazane, jak wygląda �'b,.ra programu w Javie, czym jest i jak przeb�ega ko�p lacja ?ra� jak t pro�am . c?omić.. Prz�dstawione zostaną podstawy ob1ektowosc1, czyh naJważmeJS � d!u ma z dz1edzmy progra­ mowania obiektowego, oraz pojęcie typu danych. Z · · się tu także krótkie omówie­ eścić w progra­ ni� typów danych występujących w Javie. Okaż�i , jak sposób _ '-.w rme komentarz, co, wbrew pozorom, Jes zynnoscią, pomeważ pozwala na t dokumentowanie kodu, poprawia jego czy i ułatwia analizę. � �� �� � � �� � � Czytelnicy, którzy znają podstawy p wania, mogą spokojnie pominąć ten roz­ dział i przejść do drugiego, aby��' jak w Javie wyglądają typowe konstrukcje programistyczne, takie jak pęt��trukcje warunkowe, deklaracje zmiennych oraz tablice. Natomiast osoby, które\c:W#ero zaczynają swoją przygodę z programowaniem, powinny przeczytać ten r ziAł dokładnie- nie jest on długi i stanowi wprowadzeię przejmować, jeśli nie wszystko od razu będzie całkiem nie w świat Javy. Nie jasne. Z wieloma P. ·ami i konstrukcjami programistycznymi trzeba się po prostu oswoić, wyko tyczne przykłady. Temu przecież służy ta książka. � �p Instalacja JDK Aby móc wykonywać prezentowane w książce przykłady, trzeba najpierw zainstalo­ wać w systemie pakiet programistyczny nazywany JDK, czyli Java Development Kit. Instalacja przebiega typowo, tak jak w przypadku dowolnej innej aplikacji dla danego systemu operacyjnego, więc nikomu z pewnością nie przysporzy problemów. Poniżej znajdują się krótkie opisy tego procesu dla JDK 8 i systemów Windows oraz Linux. 10 Java. Praktyczny kurs Instalacja w systemie Windows W chwilę po uruchomieniu pliku instalacyjnego (np. jdk-8-windows-i586.exe dla JDK 81 lub jdk-1. 7.0.windows-i586.exe dla JDK 7; są dostępne pod adresami podanymi we wstę­ pie) na ekranie ukaże się okno startowe instalatora, w któ-rym należy kliknąć przycisk Next. Kolejne okno, widoczne na rysunku 1.1, pozwala na wybranie składników in­ stalacji oraz katalogu docelowego (standardowo C: \Program FilesVava!Jdkl.8.0). Po kliknięciu Next rozpocznie się właściwy proces instalacji. Rysunek 1.1. Java SE Development Kit 8 - Custom Setup Okno wyboru składnikó w JDK �)ja a·· -.:::: ORACLE" V - --- - ------ Select optional feab.Jres to install from the list below, You can change your choice of feab.Jres after installation by using the Add/Remove Programs ulility in the eontroi Panel � Fe e Descriplion velopment Kit 8, g e JavaFX SDK" a JRE, and the Java eontroi mols suite, lhis l ill require 180MB on your hard drive. J Source eode Public: JRE S lnstall to: c,�mg<Om ' ' "�"''''';.:._a __________ V Jeżeli w oknie wyboru JD L < !łack ]l LQ�h ange.� _ _ �e)(t > lL -_ _ _ _ �ancel J � ostała wyłączona opcja instalacji JRE (ang. Java · lrodowiska uruchomieniowego pozwalającego na uruchamianie aplikacji i a e napisanych w Javie, a JRE nie było wcześniej instalowane w systemie jako o produkt, w trakcie instalacji JDK na ekranie pojawi się okno pozwalające na� bór katalogu docelowego JRE (rysunek 1.2). Po kliknięciu Next JRE również �ie zainstalowane. Runtime Environment), Rysunek 1.2. Okno wyboru składnikó w JDK Java Setup - Destination Folder � java· ORACLE" Install to: Qange� e: \program Files\Java\jre8 \ eancel 1 Uwaga: Java 8 oficjalnie nie współpracuje z systemami Windows poniżej wersji 7. Rozdział 1. + Podstawy 11 Instalacja w systemie Linux Do wyboru mamy instalację za pomocą dystrybucji binarnej lub w postaci pakietu RPM. W pierwszym przypadku do dyspozycji jest plik o schematycznej nazwie: jdk-wersja-linux-proc.tar.gz np.: jdk-8-linux-i586.tar.gz jdk-1.7.0-linux-i586.tar.gz Należy go skopiować lub przenieść do wybranego katalogu (do katalogu, w którym ma być zainstalowana Java, np. /usrljava), wydając jedno z poleceń: cp .jjdk-8-linux-i586.tar.gz jusrjjava/ mv .jjdk-8-linux-i586.tar.gz jusrjjava/ Następnie można przejść do tego katalogu, wydając korne cd jusrjjava � � � � �� Wtedy w bieżącym katalogu (� omawiany��kładzi � bę�ie 06 1i;m znajdą Się phki pakietu. utworzony podkatalog nazwie oraz rozpakować plik, wpisując polecenie: • tar xvfz jdk-8-linux-i586.tar.gz O jdkJ.8. �o_lusr1"aval) zostanie �ać plik do wybranego katalogu, korzystając �- Alternatywnie można od razu ri)z z opcji C archiwizatora tar, np.: � � � �· tar xvfz jdk-8-linux-i586.ta W drugim przypa jdk-wersjanp. -C jusrjjava M) do dyspozycji jest plik o nazwie: rpm jdk-8-linux-i586-rpm jdk-1.7.0-linux-i586-rpm Instalacja odbywa się przez wydanie polecenia: rpm -ivh jdk-8-linux-i586.rpm Po jej zakończeniu pakiet JDK jest gotowy do użycia. Przygotowanie do pracy z JDK Narzędzia zawarte w JDK pracują w trybie tekstowym i w nim też będą uruchamiane pierwsze programy ilustrujące cechy języka. Ponieważ w dobie systemów oferujących graficzny interfejs użytkownika z takiego trybu korzysta się coraz rzadziej, warto przy­ pomnieć, jak uruchomić go w systemach Linux i Windows. 12 Java. Praktyczny kurs Jeśli pracujemy w Linuksie na konsoli tekstowej, nie trzeba oczywiście wykonywać żadnych dodatkowych czynności. Przy korzystaniu z interfejsu graficznego wystarczy natomiast uruchomić program konsoli (terminala). Pojawi się wtedy okno (podobne do przedstawionego na rysunku 1.3), w którym można wydawać polecenia tekstowe. Rysunek 1.3. Konsola systemowa w Lżnuksże kowalski@localhost:� .Eiik Edycja Widok Terminal [ j kmo�alski@localhos t --] $ Zakłagki Pomo� l Jeśli pracujemy w systemie Windows, wciskamy na klawiaturze kombinację klawiszy 2 Windows+R , w polu Otwórz (w niektórych wersjach systemu- Uruchom) wpisujemy 3 cmd lub cmdexe i klikamy przycisk OK lub wciskamy klawisz Enter (rysunek 1.4). W systemach XP i starszych pole Uruchom jest też dostępne bezpośrednio w menu Start. Można też w dowolny inny sposób uruchomić apli ję cmdexe. Pojawi się ie można wydawać wtedy okno wiersza poleceń (wiersza polecenia), w który B komendy. Jego wygląd dla systemów z rodziny Window stał przedstawiony na rysunku 1.5 (w innych wersjach wygląda ono bardz � � � �001 . Rysunek 1.4. Uru Uruchamianie wżersza poleceń w systemie Windows Qtwórz: OK l LAnuluj-.J [iZeglądaj=:J Rysunek 1.5. Konsola systemowa w systemie Windows Po instalacji, aby usprawnić pracę z JDK, warto dodać do zmiennej środowiskowej PATH ścieżkę do podkatalogu bin tego pakietu (dla typowych instalacji jest to C: \Program FilesVavdydkl.8.0\bin lub /usrljavaljdkl.8.0/bin/- zależnie od użytego systemu). 2 Klawisz funkcyjny Windows jest też opisywany jako Start. W starszych systemach (Windows 98, Me) należy uruchomić aplikację commandexe (Start/Uruchom/commandexe ). 4 W systemach Vista i 7 pole Uruchom standardowo nie jest dostępne w menu startowym, ale można je do niego dodać, korzystając z opcji Dostosuj. Rozdział 1. + Podstawy 13 Dzięki temu nie będzie trzeba za każdym razem podawać pełnej ścieżki dostępu, aby uruchomić kompilator czy też inne narzędzie. Ta czynność powinna zostać wykonana automatycznie, jeśli został wykorzystany instalator w Windowsie lub pakiet RPM w Linuksie. Jeżeli jednak w wierszu poleceń nie są rozpoznawane komendy javac i java, aktualizację zmiennej PATH trzeba przeprowadzić samodzielnie lub też za każdym razem przy wywoływaniu tych poleceń podawać pełną ścieżkę dostępu, np. w Windowsie: C:\Program Files\Java\jdkl.8.0\bin\javac lub w Linuksie: jusrjjavajjdkl.8.0/bin/javac Aktualizacja zmiennej środowiskowej PATH dla bieżącej sesji konsoli w systemie Win­ dows polega na wydaniu polecenia o ogólnej postaci: path=%path%;ścieżka_dostępu �� np.: path=%path%;c:\Program Files\java\jdkl.8.0\bin »� Aby natomiast wykonać tę operację w Linuksie w bash, trzeba wydać polecenie: � "'�� � PATH=$PATH:jścieżka_dostępu np.: PATH=$PATH:jusrjjavajjdkl.8.0/bin/ u powłoki systemowej o0v � e na stałe, należy skorzystać z odpowiednich Jeżeli zmiany ścieżki mają b� uksie jest to dopisanie odpowiedniego polecenia do mechanizmów systemowych. skryptu startowego powło np. .bash_profile), w Windowsie w oknie właściwości sys­ temu należy odnaleźć R ennych środowiskowych i tam wprowadzić modyfikację do zmiennej PATH. � i.._f<{ � � � Podstawy programowania Lekcja 1. Struktura programu, kompilacja i wykonanie Większość kursów programowania zaczyna się od napisania prostego programu, któ­ rego zadaniem jest jedynie wyświetlenie napisu na ekranie komputera. To bardzo dobry początek pozwalający oswoić się ze strukturą kodu, wyglądem instrukcji oraz proce­ sem kompilacji. Jak zatem wyglądałby taki program w Javie? Jest zaprezentowany na listingu 1.1. 14 Java. Praktyczny kurs Listing 1.1. class Main { public static void main(String args[]) System.out.println("To jest napis."); Kod widoczny na lisringu to tak zwany kod źródłowy, czyli zapisany w języku progra­ mowania. Aby taki program wykonać, najpierw trzeba przetworzyć go na instrukcje zro­ zumiałe dla danego procesora (ściślej: dla danej platformy sprzętowo-systemowej). Proces ten nazywamy kompilacją Kompilacja z kolei jest wykonywana przez program nazywany kompilatorem. Pakiet JDK oczywiście zawiera odpowiedni kompilator- javac (ang. java compiler). Program z listingu 1.1 należy zapisać w pliku o nazwie Main.java, a następnie podać tę nazwę jako parametr dla kompilatora wywołanego z wiers leceń, pisząc: � � � �� a� javac Main.java 0 Należy przy tym zwrócić uwagę, aby to polecenie ać, gdy katalogiem bieżą­ a I �żąc�, używa się polecema cd, cymJest ten z phkiemM_ażn.!a�a (aby �emc mż bieżący, trzeba dodatkowo np. cd c:\java\). Jeżeh phk Jest w mn m · ·ava jest zapisany na dysku c: w kapodać jego lokalizację. Przykładowo jeśli ć postać: talogu java, to komenda kompilująca mo e � javac c:\java\Main.java � Uwaga <::) ""'' Jeśli po wykonaniu polecenia t?v aXai n .java na ekranie pojawi się komunikat o nie­ rozpoznanym polecen ·u va �sunki 1.6 i 1.7), należy postąpić zgodnie z opisem przedstawionym w pu Przygotowanie do pracy z JDK" lub przy kompilacji podać pełną ścieżkę dosl_ � u kompilatora. Komenda będzie miała wtedy postać (o ile . �· ę kompilator znajd c:\Progr � w lokalizacji c:\Program FilesVavaVdk1.8.0\bin): s\java\jdkl,B,O\bin\javac Main,java lub (o ile konieczne jest również podanie pełnej ścieżki dostępu do pliku Main.java): c:\Program Files\java\jdkl.8.0\bin\javac c:\java\Main.java c,\ E:\WINDOWS\system32\cmd.exe lli1E1 E:\java>javac Main.java Nazwa 'javac' nie jest roz�oznawana jako polecenie wewnętrzne lub zewnętrzne, program wykonywalny lub pl1k wsadowy. E:\java>_ Rysunek 1.6. Komunikat o nierozpoznanym poleceniu javac w Windows XP 11 l Rozdział 1. + Podstawy 15 Rysunek 1.7. Komunikat o nierozpoznanym poleceniu javac w Windows 8 O ile nie został popełniony błąd, kompilator nie pokaże żadnego komunikatu, natomiast w katalogu, w którym znajdował się plik z kodem źródłowym, pojawi się plik Main.class. To właśnie plik z kodem wykonywalnym. Aby go uruchomić, należy w wierszu poleceń wydać kolejną komendę: java Main � �, Na ekranie pojawi się, co nie powinno budzić najmniejszego nap i s. (rysunek 1.8). ienia, napis: To jest • Rysunek 1.8. Kompilacja i wykonanie pierwszego programu w Javie U:WiiQ'il Zarówno przy pisaniu , jak i przy wydawaniu komend w wierszu pole­ ceń należy zwrócić uw·�� :,ielkość wpisywanych liter. Jakakolwiek pomyłka w większości przypadków uje, że kompilacja się nie uda. � �� W tym miejscu- .a zazwyczaj pytanie, czemu do uruchomienia skompilowanego pro­ gramu używa lecenia java i dlaczego powstały plik ma rozszerzenie class. Otóż projektanci Javy założyli, że będzie to język przenośny, zatem stworzone w nim programy będą mogły być uruchamiane na różnych platformach sprzętowo-systemowych, czyli różnych komputerach, systemach operacyjnych i innych urządzeniach. Skoro program skompilowany na pececie pod systemem Windows ma się również uruchomić pod Linuksem działającym na komputerze wyposażonym w procesor Motoroli, podczas kompilacji nie może powstawać kod charakterystyczny dla jednego procesora (jednej rodziny procesorów). Dlatego też kompilator Javy generuje tak zwany kod pośredni- b-kod (ang. b-code, byte-code). Kod ten jest zawarty właśnie w plikach z rozszerzeniem class. Ponieważ b-kod nie jest zrozumiały bezpośrednio dla żadnego procesora (z wyjątkiem niektó­ rych specjalistycznych procesorów zaprojektowanych do tego celu), potrzeba dodatkowe­ go programu tłumaczącego. Ten program to wirtualna maszyna Javy (ang. Java Virtual Machine). Zatem java Mai n. c l ass to polecenie oznaczające: uruchom wirtualną ma­ szynę Javy i przekaż jej do wykonania kod zawarty w plikuMain.class. 16 Java. Praktyczny kurs Każdy program w Javie składa się ze zbioru tzw. klas (to pojęcie zostanie bliżej wyjaśnio­ ne w lekcji 2. oraz w rozdziale 3.). Każda klasa powinna być zapisana w oddzielnym pliku o nazwie takiej samej jak nazwa klasi i rozszerzeniu java. Zatem klasa o nazwie Main musi znajdować się w pliku o nazwie Mainjava, natomiast b-kod tej klasy musi znajdować się w pliku Main.class. Na potrzeby pierwszych dwóch rozdziałów przyj­ miemy, że każdy program musi mieć dokładnie taką strukturę jak ta przedstawiona na li­ stingu 1.2. Jej dokładne znaczenie będzie wyjaśnione dopiero w rozdziale 3. Na razie trzeba uwierzyć na słowo, że wykonywanie takiego programu rozpocznie się w miej­ scu oznaczonym ll i właśnie tam należy wpisywać instrukcje przeznaczone do wykonania (instrukcję można traktować jako polecenie dla komputera wydane w danym języku programowania). Listing l. 2. class Main { public static void main(String args[]) l/tutaj instrukcje do wykonania } C- w1czen1a . " do samod"l z1e nego wy konan1a . Ćwiczenie 1.1. .� �� � To jest mój drugi program. Q �V Napisz program wyświetlający na ek piluj go i uruchom. Ćwiczenie 1.2. � �{ � apis: Skom- 0 � o rozszerzeniu innym niż java, np. txt. Spróbuj dokonać �� Zapisz kod z listin kompilacji tego pli . Lekcja 2. Po obserwuj, co się stanie. �awy obiektowaści i typy danych Wiadomo już, jak napisać w Javie najprostszy program wyświetlający na ekranie napis, jak go skompilować i uruchomić. Teraz czas poznać nieco teorii dotyczącej podstaw programowania obiektowego oraz typów danych. W lekcji 2. zostaną przedstawione klasy i obiekty, pojęcie typu danych, a także to, jakie typy danych występują w Javie. Znajomość tych informacji będzie potrzebna przy zapoznawaniu się z kolejnymi lekcjami w rozdziale 2., omawiającym instrukcje języka, oraz w rozdziale 3., poświęconym budowaniu klas. O tym, że nie jest to tak do końca prawda, przekonamy się w rozdziałach 3. i 5. podczas omawiania rodzajów klas. Na razie przyjmijmy, że każda klasa musi znajdować się w oddzielnym pliku zgodnym z jej nazwą. Rozdział 1. Podstawy 17 Klasy i obiekty Każdy program w Javie składa się z klas, które są z kolei opisami obiektów. To podstawowe pojęcia związane z programowaniem obiektowym. Będzie o tym mowa w rozdziale 3. Zanim jednak pozna się dokładnie klasy i obiekty, lepiej najpierw zapoznać się z podstawowymi konstrukcjami języka Java. Osoby, które nie zetknęły się dotychczas z programowaniem obiektowym, mogą potraktować obiekt jako pewien byt programistyczny. Może on przechowywać dane i wykonywać operacje, czyli różne zadania. Klasa to z kolei definicja, opis takiego obiektu. Skoro klasa definiuje obiekt, jest również jego typem. Czym jest typ obiektu? Oto jedna z definicji: „Typ jest przypisany zmiennej, wyrażeniu lub innemu bytowi programistycznemu (danej, obiektowi, funkcji, procedurze, operacji, metodzie, parametrowi, modułowi, wyjątkowi, zdarzeniu). Specyfikuje on rodzaj wartości, które może przybierać ten byt. (…) Jest to również ograniczenie kontekstu, w którym odwołanie do tego bytu może być użyte w programie”6. Innymi słowy, typ obiektu określa po prostu, czym jest dany obiekt. Obiektem może być tak naprawdę wszystko. W życiu realnym tym mianem określimy stół, krzesło, komputer, dom, samochód, radio… Każdy z obiektów ma pewne swoje cechy, właściwości, które go opisują: wielkość, kolor, powierzchnię, wysokość. Co więcej, każdy obiekt może składać się z innych obiektów (rysunek 1.9). Na przykład mieszkanie składa się z poszczególnych pomieszczeń, z których każde może być obiektem; w każdym pomieszczeniu mamy z kolei inne obiekty: sprzęty domowe, meble itd. Rysunek 1.9. Obiekt może zawierać inne obiekty Obiekty, oprócz tego, że mają właściwości, mogą wykonywać różne funkcje, zadania. Innymi słowy, każdy obiekt ma przypisany pewien zestaw poleceń, które potrafi wykonywać. Na przykład samochód wypełnia polecenia „uruchom silnik”, „zgaś silnik”, „skręć w prawo”, „przyspiesz” itp. Funkcje te składają się na interfejs udostępniany przez ten samochód. Dzięki interfejsowi można wpływać na zachowanie samochodu i wydawać mu polecenia. W programowaniu jest bardzo podobnie. Za pomocą klas programista stara się opisać obiekty, ich właściwości, zbudować konstrukcje, interfejs, dzięki któremu będzie można 6 K. Subieta, Wytwarzanie, integracja i testowanie systemów informatycznych, PJWSTK, Warszawa 1997. 18 Java. Praktyczny kurs wydawać polecenia realizowane potem przez obiekty. Obiekt powstaje jednak dopiero w trakcie działania programu jako instancja (wystąpienie, egzemplarz) danej klasy. Obiektów danej klasy może być bardzo dużo. Przykładowo: jeśli klasą będzie Samochód, to instancją tej klasy będzie konkretny egzemplarz o danym numerze seryjnym. Wbudowane typy danych Java udostępnia pewną liczbę wbudowanych typów danych, czyli takich, które oferuje sam język i z których można korzystać bez potrzeby ich definiowania. Nazywa się je typami prostymi lub podstawowymi (ang. primitive types). Można je podzielić na typy arytmetyczne, typ char oraz typ logiczny (boolean). Typy arytmetyczne dzielą się z kolei na typy całkowitoliczbowe i zmiennopozycyjne (zmiennoprzecinkowe). Oprócz tego istnieje typ obiektowy (odnośnikowy, referencyjny), który pozwala na posługiwanie się obiektami, jednak nim zajmiemy się dopiero w rozdziale 3. Typy arytmetyczne całkowitoliczbowe Typy arytmetyczne całkowitoliczbowe służą do reprezentacji liczb całkowitych i dzielą się na cztery rodzaje: byte, short, int, long. Różnią się one od siebie zakresem liczb, które można za ich pomocą reprezentować — przedstawiono to w tabeli 1.1. Znajduje się w niej również informacja, ile bitów potrzeba, aby zapisać liczbę przy zastosowaniu danego typu danych. Oczywiście im szerszy zakres liczb, które można przedstawić, tym więcej potrzeba bitów. Tabela 1.1. Zakresy typów arytmetycznych w Javie Typ Liczba bitów Zakres byte 8 Od –128 do 127 short 16 Od –32 768 do 32 767 int 32 Od –231 do 231–1 long 64 Od –263 do 263–1 Osoby programujące w C lub C++ powinny zwrócić uwagę, że zakres możliwych do przedstawienia wartości jest ustalony z góry i nie zależy od platformy systemowej, na której jest uruchamiany program w Javie. To oznacza, że niezależnie od tego, na jakim systemie się pracuje (16-, 32- czy 64-bitowym) oraz z jakiego kompilatora się korzysta, dokładnie wiadomo, ile bitów zajmie zmienna danego typu oraz jaki zakres liczb może być przedstawiony. Dzięki temu w Javie unikniemy występujących w C i C++ problemów z przenośnością programów związanych z różną reprezentacją typów liczbowych7. 7 Uwaga ta dotyczy również pozostałych typów podstawowych. Rozdział 1. Podstawy 19 Typy arytmetyczne zmiennopozycyjne Typy zmiennopozycyjne występują w dwóch odmianach różniących się, podobnie jak w przypadku typów całkowitoliczbowych, rozmiarem oraz zakresem liczb możliwych do zaprezentowania. Są to: float (pojedynczej precyzji), double (podwójnej precyzji). Zakres liczbowy oraz liczbę bitów niezbędnych do zapisania liczby przy zastosowaniu danego typu przedstawiono w tabeli 1.2. Tabela 1.2. Zakresy typów zmiennoprzecinkowych w Javie8 Typ Liczba bitów Zakres float 32 Od –3,4e38 do 3,4e38 double 64 Od –1,8e308 do 1,8e308 Typ char Typ char służy do reprezentacji znaków, czyli liter, cyfr, znaków przestankowych itp. — ogólnie wszelkich znaków alfanumerycznych. W Javie ten typ jest 16-bitowy (czyli do zapisania jednego znaku potrzeba dwóch bajtów) i opiera się na standardzie Unicode (umożliwiającym przedstawienie znaków występujących w większości języków świata). Ponieważ znaki są tak naprawdę reprezentowane jako 16-bitowe kody liczbowe, typ ten zalicza się czasem do typów arytmetycznych. Typ boolean Typ logiczny ma nazwę boolean. Może on reprezentować jedynie dwie wartości — true (prawda) i false (fałsz). Może być wykorzystywany przy sprawdzaniu różnych warunków w instrukcjach if, a także w pętlach i innych konstrukcjach programistycznych. Zostanie to pokazane na konkretnych przykładach już w rozdziale 2. Lekcja 3. Komentarze W większości języków programowania dostępna jest konstrukcja komentarza, która służy do opisywania kodu źródłowego w języku naturalnym. Innymi słowy, służy do wyrażenia tego, co programista miał na myśli, stosując daną instrukcję programu. Choć początkowo komentowanie kodu źródłowego programu może wydawać się zupełnie niepotrzebne, okazuje się, że jest to bardzo pożyteczny nawyk. Bardzo często bowiem bywa tak, że po pewnym czasie sam programista ma problemy z analizą napisanego przez siebie programu, nie mówiąc już o innych osobach, które miałyby wprowadzać poprawki czy modyfikacje. 8 W tabeli podano wartości przybliżone. Zapis 3,4e38 (notacja wykładnicza, naukowa) oznacza 3,4×1038. 20 Java. Praktyczny kurs W Javie istnieją dwa rodzaje komentarzy, oba zapożyczone z języków C i C++. Są to: komentarz blokowy, komentarz liniowy (wierszowy). Komentarz blokowy Komentarz blokowy rozpoczyna się od znaków /* i kończy znakami */. Wszystko, co znajduje się pomiędzy tymi znakami, jest traktowane przez kompilator jako komentarz i pomijane w procesie kompilacji. Przykład takiego komentarza jest widoczny na listingu 1.3. Listing 1.3. class Main{ public static void main(String args[]){ /* To mój pierwszy program w Javie Wyświetla on na ekranie napis */ System.out.println("To jest napis."); } } Umiejscowienie komentarza blokowego jest praktycznie dowolne. Co ciekawe, może on nawet znaleźć się w środku instrukcji (pod warunkiem że nie zostanie przedzielone żadne słowo); zobrazowano to na listingu 1.4. Okazuje się to możliwe, ponieważ — zgodnie z tym, co zostało napisane wcześniej — wszystko, co znajduje się między znakami /* i */ (oraz same te znaki), jest ignorowane przez kompilator. Oczywiście należy potraktować to raczej jako ciekawostkę niż praktyczne zastosowanie. Komentarze mają przecież przyczyniać się do zwiększenia czytelności kodu, a nie jego zaciemniania. Listing 1.4. class Main { public /*komentarz*/ static void main(String args[]) { System.out.println /*komentarz*/ ("To jest napis."); } } Komentarzy blokowych nie wolno zagnieżdżać, to znaczy jeden nie może znaleźć się w środku drugiego. Jeśli zatem spróbujemy dokonać kompilacji kodu przedstawionego na listingu 1.5, kompilator zgłosi błąd. Ilustracja takiego błędu jest widoczna na rysunku 1.10. Rysunek 1.10. Próba zagnieżdżenia komentarza blokowego powoduje błąd kompilacji Rozdział 1. Podstawy 21 Listing 1.5. class Main { public static void main(String args[]) { /* Komentarzy blokowych nie /*w tym miejscu wystąpi błąd*/ wolno zagnieżdżać */ System.out.println ("To jest napis."); } } Wbrew pozorom to nie jest całkiem hipotetyczna sytuacja. Może się bowiem zdarzyć, że programista nie zauważy, że w komentarz blokowy został ujęty większy fragment kodu, i w jego środku ponownie zastosuje ten typ komentarza (choć trudno o to przy korzystaniu z dobrego edytora programistycznego z kolorowaniem składni — wtedy blok komentarza jest wyraźnie widoczny). Ważne, aby w takiej sytuacji od razu rozpoznać typ błędu i wprowadzić odpowiednie poprawki. Komentarz liniowy (wierszowy) Komentarz liniowy zaczyna się od znaków // i obowiązuje do końca danej linii programu. To znaczy, że wszystko, co występuje po tych dwóch znakach aż do końca bieżącej linii, jest ignorowane przez kompilator. Przykład wykorzystania takiego komentarza przedstawiono na listingu 1.6. Listing 1.6. class Main { public static void main(String args[]) { //teraz wyświetlam napis System.out.println ("To jest napis."); } } Takiego komentarza nie można oczywiście użyć w środku instrukcji, gdyż wtedy jej część stałaby się komentarzem i powstałby błąd kompilacji. Można natomiast w środku komentarza liniowego wstawić komentarz blokowy, o ile zaczyna się i kończy w tej samej linii. Taka konstrukcja wyglądałaby następująco: //komentarz /*komentarz blokowy*/ liniowy Jest to dopuszczalne i zgodne z regułami składni języka, jednak w praktyce mało przydatne. Komentarz liniowy może natomiast znaleźć się w środku komentarza blokowego: /* //ta konstrukcja jest poprawna */ 22 Java. Praktyczny kurs Ćwiczenia do samodzielnego wykonania Ćwiczenie 3.1. Na początku programu z listingu 1.1 dołącz komentarz blokowy opisujący jego działanie. Wykonaj kompilację kodu. Rozdział 2. Instrukcje języka Java, tak jak każdy język programowania, jest wyposażona w szereg podstawowych instrukcji, które pozwalają programiście na wykonywanie najróżniejszych operacji. Ten rozdział jest im w całości poświęcony. Przedstawione zostanie pojęcie zmiennej oraz to, w jaki sposób należy deklarować zmienne i jakie operacje można na nich wykonywać. Okaże się, jak wykorzystywać typy danych prezentowane w rozdziale 1. (przy opisie zmiennych nie pojawi się jednak dokładne omówienie zmiennych obiektowych, które zostaną opisane bliżej dopiero w kolejnym rozdziale). Następnie omówimy występujące w Javie instrukcje, które sterują wykonywaniem programu. Będą to instrukcje warunkowe pozwalające wykonywać różny kod w zależności od tego, czy zadany warunek jest prawdziwy, czy fałszywy, oraz pętle pozwalające na łatwe wykonywanie powtarzających się instrukcji. Ostatnie dwie lekcje rozdziału 2. zostaną poświęcone tablicom, i to zarówno jedno-, jak i wielowymiarowym. Osoby, które znają dobrze języki C lub C++, mogą jedynie przejrzeć ten rozdział, gdyż większość podstawowych instrukcji sterujących w Javie została zaczerpnięta właśnie z tych języków. Bliżej powinny zapoznać się tylko z materiałem lekcji 5. omawiającej wyprowadzanie danych na ekran oraz lekcji dotyczących tablic. Zmienne Zmienne to konstrukcje programistyczne, które pozwalają na przechowywanie danych. Każda zmienna ma swoją nazwę oraz typ. Nazwa to jednoznaczny identyfikator, dzięki któremu istnieje możliwość odwoływania się do zmiennej w kodzie programu, natomiast typ określa, jakiego rodzaju dane zmienna może przechowywać (zmienne można więc potraktować jak nazwane „pudełka” przechowujące dane). Podstawowe typy danych zostały przedstawione w lekcji 2. (w rozdziale 1.). Lekcja 4. jest poświęcona problemowi deklaracji i przypisywania wartości zmiennym, lekcja 5. — wyświetlaniu wartości zmiennych (ale także znaków specjalnych i napisów) na ekranie, natomiast najdłuższa w tym podrozdziale lekcja 6. pokaże, jakie operacje (arytmetyczne, logiczne, bitowe) można wykonywać na zmiennych. 24 Java. Praktyczny kurs Lekcja 4. Deklaracje i przypisania Lekcja 4. jest poświęcona deklaracjom zmiennych oraz przypisywaniu danych zadeklarowanym zmiennym. Znajdują się w niej informacje o tym, jak tworzy się zmienne i jaki ma to związek z typami danych omówionymi w lekcji 2., jak zadeklarować wiele zmiennych w jednej instrukcji oraz jak spowodować, aby przechowywały one dane, a także jakie zasady obowiązują w ich nazewnictwie. Na zakończenie słowo o deklaracjach typów odnośnikowych — więcej o nich dopiero w rozdziale 3. Proste deklaracje Każda zmienna, zanim zostanie wykorzystana w kodzie programu, musi zostać zadeklarowana. Deklaracja polega na podaniu typu oraz nazwy zmiennej. Ogólnie taka konstrukcja wygląda następująco: typ_zmiennej nazwa_zmiennej; Należy zwrócić uwagę na średnik kończący deklarację. Jest on niezbędny, informuje bowiem kompilator o zakończeniu instrukcji programu (a deklaracja zmiennej jest instrukcją). Jeśli w programie zechcemy przechowywać liczby całkowite, zadeklarujemy zmienną typu int. To, jak taka deklaracja będzie wyglądała w strukturze programu, widać na listingu 2.1. Listing 2.1. class Main { public static void main(String args[]) { int liczba; } } Powstała tu zmienna o nazwie liczba i typie int. Jak wiadomo z lekcji 2., typ int pozwala na przechowywanie liczb całkowitych w zakresie od –232 (–2 147 483 648) do 232–1 (2 147 483 647), można zatem przypisać tej zmiennej dowolną liczbę mieszczącą się w tym przedziale. Przypisanie takie odbywa się za pomocą znaku równości. Jeśli więc chcemy, aby zmienna liczba zawierała wartość 100, napiszemy: liczba = 100; Pierwsze przypisanie wartości zmiennej nazywamy inicjacją lub inicjalizacją1 zmiennej (listing 2.2). A ponieważ przypisanie wartości zmiennej jest instrukcją programu, nie można również zapomnieć o kończącym je znaku średnika. 1 Z formalnego punktu widzenia prawidłowym terminem jest inicjacja. Wynika to jednak wyłącznie z tego, że termin ten został zapożyczony do języka polskiego znacząco wcześniej niż kojarzona chyba wyłącznie z informatyką inicjalizacja (kalka ang. initialization). Co więcej, np. w języku angielskim funkcjonują oba terminy (initiation oraz initialization) i mają nieco inne znaczenia. Nie wnikając jednak w niuanse językowe, można powiedzieć, że w przedstawionym znaczeniu oba te terminy mogą być (i są) używane zamiennie. Rozdział 2. Instrukcje języka 25 Listing 2.2. class Main { public static void main(String args[]) { int liczba; liczba = 100; } } Inicjalizacja zmiennej może odbywać się w dowolnym miejscu programu po deklaracji (jak na listingu 2.2), ale może być również równoczesna z deklaracją. W tym drugim przypadku jednocześnie deklaruje się i inicjuje zmienną, co schematycznie wygląda następująco: typ_zmiennej nazwa_zmiennej = wartość_zmiennej; Zatem w konkretnym przypadku, jeśli chcemy zadeklarować zmienną liczba typu int i od razu przypisać jej wartość 100, użyjemy konstrukcji w postaci: int liczba = 100; Deklaracje wielu zmiennych Zmienne, inaczej niż w Pascalu, ale podobnie jak w C i C++, można deklarować w momencie, kiedy są potrzebne w kodzie programu (w Pascalu zmienne wykorzystywane w procedurze lub funkcji muszą być zadeklarowane na ich początku). W jednej linii da się również zadeklarować kilka zmiennych, ale uwaga: o ile są one tego samego typu. Struktura takiej deklaracji wygląda następująco: typ_zmiennej nazwa1, nazwa2, nazwa3; W praktyce mogłoby to wyglądać tak: int pierwszaLiczba, drugaLiczba, trzeciaLiczba; Przy takiej deklaracji można również część zmiennych od razu zainicjować: int pierwszaLiczba = 100, drugaLiczba, trzeciaLiczba= 200; W tym ostatnim przypadku powstały trzy zmienne o nazwach: pierwszaLiczba, druga Liczba oraz trzeciaLiczba. Zmiennej pierwszaLiczba została przypisana wartość 100, zmiennej trzeciaLiczba wartość 200, natomiast drugaLiczba pozostała niezainicjowana. Na listingu 2.3 zaprezentowany jest kod wykorzystujący różne sposoby deklaracji i inicjacji zmiennych. Listing 2.3. class Main { public static void main(String args[]) { int liczba1; byte liczba2, liczba3 = 100; liczba1 = 10000; liczba2 = 25; } } 26 Java. Praktyczny kurs Powstały tutaj w sumie trzy zmienne: jedna typu int (liczba1) oraz dwie typu byte (liczba2, liczba3). Przypomnijmy, że typ byte pozwala na przechowywanie wartości całkowitych z zakresu od –128 do 127 (lekcja 2.). Jedynie zmienna liczba3 została zainicjowana już w trakcie deklaracji i otrzymała wartość 100. Inicjacja zmiennych liczba1 i liczba2 odbyła się po deklaracji i otrzymały one wartości odpowiednio: 10000 i 25. Nazwy zmiennych Przy nazywaniu zmiennych2 obowiązują pewne zasady, których należy przestrzegać. Nazwa taka może składać się z liter (zarówno małych, jak i wielkich), cyfr oraz znaku podkreślenia3, nie może jednak zaczynać się od cyfry. Do tej zasady trzeba się bezwzględnie stosować, gdyż umieszczenie innych znaków w nazwie (np. dwukropka, wykrzyknika itp.) spowoduje natychmiast błąd kompilacji (rysunek 2.1). Rysunek 2.1. Niezgodna z zasadami nazwa zmiennej powoduje błąd kompilacji Można wykorzystać dowolne znaki będące literami, również te spoza ścisłego alfabetu łacińskiego. Dopuszczalne jest zatem stosowanie wszelkich znaków narodowych (w tym oczywiście polskich). To, czy będą one używane, zależy wyłącznie od indywidualnych preferencji programisty i (lub) od specyfiki danego projektu programistycznego4. Nazwy powinny odzwierciedlać funkcje, które zmienna pełni w programie. Jeśli ma ona określać szerokość ekranu, najlepiej nazwać ją po prostu szerokoscEkranu (szero kośćEkranu) czy też screenWidth. Bardzo poprawia to czytelność kodu oraz ułatwia jego późniejszą analizę. Przyjmuje się też, że nazwa zmiennej rozpoczyna się od małej litery, natomiast poszczególne słowa wchodzące w skład tej nazwy rozpoczynają się wielkimi literami5. Dokładnie tak jak w zaprezentowanych w tym akapicie przykładach. Zmienne typów odnośnikowych Zmienne typów odnośnikowych, inaczej obiektowych lub referencyjnych (ang. reference types, object types), deklaruje się w sposób bardzo podobny do zmiennych zaprezentowanych już typów (tak zwanych typów prostych). Występuje tu jednak bardzo ważna różnica. Otóż pisząc: typ_zmiennej nazwa_zmiennej; 2 Ogólniej rzecz ujmując — wszelkich identyfikatorów. 3 Dopuszczalny jest także znak dolara, ale przyjmuje się, że jest zarezerwowany dla narzędzi przetwarzających kod i nie należy go stosować w nazwach zmiennych. 4 Należy zwrócić uwagę, że stosowanie w kodzie znaków spoza alfabetu łacińskiego może powodować problemy przy przenoszeniu kodu na inne systemy. W takiej sytuacji najlepiej zapisywać pliki w kodowaniu UTF-8. 5 Ten sposób zapisu określa się jako Lower Camel Case, w odróżnieniu od Upper Camel Case, gdzie pierwsza litera pierwszego członu nazwy jest wielka. Rozdział 2. Instrukcje języka 27 w przypadku typów prostych tworzy się zmienną, z której od razu można korzystać. W przypadku typów referencyjnych zostanie w ten sposób zadeklarowane jedynie odniesienie, inaczej referencja (ang. reference), któremu domyślnie zostanie przypisana wartość pusta, nazywana w Javie null (w Pascalu stosowane jest słowo nil). Zmiennej referencyjnej po deklaracji należy przypisać odniesienie do utworzonego oddzielną instrukcją obiektu. Dopiero wtedy można zacząć z niej korzystać. Dla osób, które nie zetknęły się z językami obiektowymi takimi jak C++ czy Object Pascal, jest to zapewne mało zrozumiałe. Pierwsze praktyczne przykłady zostaną przedstawione już w tym rozdziale, w lekcji omawiającej tablice, natomiast dokładniejsze wyjaśnienie znajdzie się w rozdziale 3. opisującym podstawy programowania obiektowego. Ćwiczenia do samodzielnego wykonania Ćwiczenie 4.1. Zadeklaruj i jednocześnie zainicjuj dwie zmienne typu short. Nazwy zmiennych i przypisywane wartości możesz wybrać dowolnie. Pamiętaj o zasadach nazewnictwa zmiennych oraz zakresie wartości, jakie mogą być reprezentowane przez typ short. Lekcja 5. Wyprowadzanie danych na ekran Aby zobaczyć wyniki działania programu, można wyświetlić je na ekranie. W lekcji 5. zostanie pokazane, w jaki sposób wykonać to zadanie, jak wyświetlić zwykły napis oraz wartości wybranych zmiennych. Ponadto okaże się, co to są znaki specjalne i co należy zrobić, aby one również mogły pojawić się na ekranie. Zajmiemy się także problemem wyświetlania polskich znaków diakrytycznych na konsoli w środowisku Windows, wymaga to bowiem wykonania kilku dodatkowych czynności niezwiązanych bezpośrednio z samym językiem programowania. Wyświetlanie wartości zmiennych W lekcji 4. zostały przedstawione sposoby deklarowania zmiennych oraz przypisywania im różnych wartości. W jaki sposób przekonać się jednak, że dane przypisanie rzeczywiście odniosło skutek? Jak zobaczyć efekty działania programu? Najlepiej wyświetlić je na ekranie. Sposób wyświetlenia określonego napisu został omówiony już w lekcji 1., była to instrukcja: System.out.println("napis"); (por. listing 1.1, rysunek 1.8). Ta instrukcja będzie wykorzystywana w dalszych przykładach. Wiadomo już, jak wyświetlić ciąg znaków, jak jednak wyświetlić wartość zmiennej? Jest to równie proste. Zamiast ujętego w znaki cudzysłowu6 napisu należy podać nazwę zmiennej — schematycznie taka konstrukcja będzie wyglądała następująco: System.out.println(nazwa_zmiennej); 6 Ściślej rzecz ujmując, należałoby tu mówić o znakach cudzysłowu prostego lub o znakach zastępczych cudzysłowu. 28 Java. Praktyczny kurs Zmodyfikujmy zatem program z listingu 2.2 tak, aby deklaracja i inicjalizacja odbywały się w jednej linii, oraz wyświetlmy wartość zmiennej liczba na ekranie. Zadanie to można zrealizować za pomocą kodu przedstawionego na listingu 2.4. Wynik jego działania zaprezentowano z kolei na rysunku 2.2. Listing 2.4. class Main { public static void main(String args[]) { int liczba = 100; System.out.println(liczba); } } Rysunek 2.2. Wyświetlenie na ekranie wartości zmiennej typu int W jaki sposób poradzić sobie, kiedy chce się jednocześnie mieć na ekranie zdefiniowany przez siebie ciąg znaków oraz wartość danej zmiennej? Można dwukrotnie użyć instrukcji System.out.println. Lepszym pomysłem jest jednak zastosowanie operatora + (plus) w postaci: System.out.println("napis" + nazwa_zmiennej); Konkretny przykład zastosowania takiej konstrukcji jest widoczny na listingu 2.5, a wynik jego działania na rysunku 2.3. Listing 2.5. class Main { public static void main(String args[]) { int liczba = 100; System.out.println("Zmienna liczba ma wartość " + liczba); } } Rysunek 2.3. Zastosowanie operatora + do jednoczesnego wyświetlenia napisu i wartości zmiennej Jeśli wartość zmiennej ma znaleźć się w środku napisu albo też w jednej linii mają być wyświetlone wartości kilku zmiennych, wystarczy kilkukrotnie użyć operatora + Rozdział 2. Instrukcje języka 29 i w ten sposób złożyć ciąg znaków, który ma się znaleźć na ekranie, z mniejszych kawałków. Możliwa jest zatem konstrukcja w postaci: System.out.println("napis1" + zmienna1 + "napis2" + zmienna2); Załóżmy więc, że w programie zostaną zadeklarowane dwie zmienne typu short o nazwach pierwszaLiczba oraz drugaLiczba, o wartościach początkowych równych 25 i 75. Zadaniem będzie wyświetlenie napisu: Wartość zmiennej pierwszaLiczba to 25, a wartość zmiennej drugaLiczba to 75. Program wykonujący opisane czynności jest przedstawiony na listingu 2.6. Listing 2.6. class Main { public static void main(String args[]) { short pierwszaLiczba = 25; short drugaLiczba = 75; System.out.println( "Wartość zmiennej pierwszaLiczba to " + pierwszaLiczba + ", a wartość zmiennej drugaLiczba to " + drugaLiczba + "." ); } } Pewnym zaskoczeniem może być rozbicie instrukcji wyświetlającej dane aż na siedem linii. Powód jest prosty: pełna linia ze względu na swoją długość nie zmieściłaby się ani na ekranie, ani na wydruku. Lepiej więc samodzielnie podzielić ją na mniejsze części, aby poprawić czytelność listingu. Przy takim podziale najlepiej kierować się zasadą, że nie wolno przedzielić łańcucha znaków ujętego w cudzysłów, natomiast w każdym innym miejscu, gdzie występuje spacja, można zamiast niej postawić znak końca linii (przejść do nowego wiersza po wciśnięciu klawisza Enter). Polskie znaki na konsoli Windows Jeśli środowiskiem, z którego korzystamy, jest system Windows, przy próbie uzyskania na konsoli polskich znaków zapewne spotka nas niemiła niespodzianka. Można się o tym przekonać, wykonując prosty test: wystarczy skompilować i uruchomić testowy program przedstawiony na listingu 2.7. Efekt jego wykonania prawdopodobnie będzie podobny do widocznego na rysunku 2.4 (zależy od wersji i konfiguracji systemu wraz z JDK i JRE oraz sposobu kompilacji). Listing 2.7. class Main { public static void main(String args[]) { System.out.println("Test polskich znaków:"); System.out.println("Małe litery: ąćęłńóśźż"); System.out.println("Wielkie litery: ĄĆĘŁŃÓŚŹŻ"); } } 30 Java. Praktyczny kurs Rysunek 2.4. Uzyskanie polskich znaków na konsoli Windows wymaga dodatkowych zabiegów Skąd problem? Otóż podczas pisania treści programu np. w Notatniku polskie znaki są kodowane w standardzie Windows 1250 (strona kodowa 1250). W Javie jest stosowany system Unicode. Z kolei na konsoli Windows jest standardowo wykorzystywana strona kodowa 852. Z przekodowaniem znaków z kodu źródłowego ze strony kodowej 1250 na standard Unicode kompilator radzi sobie sam, przyjmuje on jednak, że wyprowadzanie znaków ma się odbywać również przy użyciu strony kodowej 1250 (a nie 852). To właśnie powoduje, że zamiast polskich liter widać nieprzyjemne krzaczki (choć w niektórych wersjach JDK i Windows, np. w Java 8 i Windows 8, kodowanie jest poprawne). Co zatem można w tej sprawie zrobić? Jednym ze sposobów jest zastosowanie niestandardowego sposobu wyprowadzania znaków na ekran (za pomocą dodatkowych obiektów). Ponieważ wymaga to wykonania szeregu instrukcji, które byłyby w tej chwili niezrozumiałe, sposób ten będzie pokazany dopiero w rozdziale 6., w którym omówimy system wejścia-wyjścia. Na szczęście drugim rozwiązaniem, i to bardzo prostym w realizacji, jest zmiana strony kodowej oraz czcionki stosowanej w konsoli (wierszu poleceń) systemu Windows. W tym celu, po pierwsze, wydajemy polecenie chcp (ang. change code page), podając numer strony kodowej, czyli: chcp 1250 (rysunek 2.5), a następnie z menu systemowego konsoli wybieramy pozycję Właściwości oraz zakładkę Czcionka (rysunek 2.6). Rodzaj czcionki należy zmienić na True Type (np. czcionka Lucida Console). Od tej chwili polskie znaki będą wyświetlane poprawnie. Warto jednak pamiętać, że o ile zmiany czcionki tym sposobem można dokonać na stałe (czyli każde kolejne okno będzie korzystało z tego ustawienia, jeśli taka opcja została wybrana przy zatwierdzaniu zmian), o tyle ustawienie strony kodowej za pomocą polecenia chcp dotyczy tylko okna bieżącego. Rysunek 2.5. Polecenie chcp pozwala na zmianę strony kodowej konsoli Trzeba też wiedzieć, że kody źródłowe programów (np. treści listingów zapisywane w plikach z rozszerzeniem .java) również muszą być zapisane w odpowiednim kodowaniu. Dla Windowsa kompilator przyjmuje standardowo, że ma do czynienia z kodowaniem CP 1250 (Windows-1250). Jeżeli jest to inny standard, np. UTF-8, przy kompilacji mogą pojawić się komunikaty o błędach podobne do przedstawionych na rysunku 2.7. Rozdział 2. Instrukcje języka 31 Rysunek 2.6. Okno Właściwości pozwala na zmianę rodzaju używanej czcionki Rysunek 2.7. Niewłaściwe kodowanie pliku z kodem źródłowym spowodowało błędy kompilacji W takiej sytuacji najlepiej wskazać kompilatorowi, z jakim standardem ma do czynienia, stosując opcję –encoding. Kompilację wykonuje się wtedy za pomocą polecenia w postaci: javac –encoding kodowanie nazwa_pliku.java np.: javac –encoding utf8 Main.java 32 Java. Praktyczny kurs Znaki specjalne Wiadomo już, że aby wyświetlić na ekranie napis, należy ująć go w znaki cudzysłowu oraz skorzystać z instrukcji System.out.println, np. System.out.println("napis"). Może więc nasunąć się pytanie: w jaki sposób wyprowadzić na ekran sam znak cudzysłowu, skoro jest on częścią instrukcji? Odpowiedź jest prosta: należy go poprzedzić znakiem ukośnika, tak jak zaprezentowano na listingu 2.8. Listing 2.8. class Main { public static void main(String args[]) { System.out.println("Oto znak cudzysłowu: \""); } } Jest to tak zwana sekwencja ucieczki (ang. escape sequence). Zaczyna się ona od znaku \ (ang. backslash, lewy ukośnik7), po którym występuje określenie znaku specjalnego, jaki ma być wyświetlony na ekranie. Powstaje jednak kolejny problem: w jaki sposób wyświetlić teraz sam znak ukośnika \? Odpowiedź jest dokładnie taka sama jak w poprzednim przypadku: należy poprzedzić go dodatkowym znakiem \. Taka konstrukcja będzie zatem wyglądać następująco: System.out.println("Oto ukośnik: \\"); W ten sam sposób można wyprowadzić również inne znaki specjalne, takie jak znak nowej linii czy znak apostrofu8. Stosowane w praktyce sekwencje znaków specjalnych są przedstawione w tabeli 2.1. Tabela 2.1. Wybrane sekwencje znaków specjalnych Sekwencja Znaczenie \b Cofnięcie o jeden znak (ang. backspace) \t Znak tabulacji \n Nowa linia (ang. new line) \r Powrót karetki (przesunięcie na początek wiersza, ang. carriage return) \" Znak cudzysłowu \' Znak apostrofu \\ Lewy ukośnik (ang. backslash) 7 Spotyka się też określenie „ukośnik wsteczny”. 8 W rzeczywistości należałoby mówić o znaku zastępczym apostrofu lub pseudoapostrofie; spotykane jest też określenie apostrof prosty. Rozdział 2. Instrukcje języka 33 Instrukcja System.out.print Do wyświetlania danych na ekranie, oprócz znanej nam już dobrze instrukcji System.out. println, można wykorzystać również bardzo podobną instrukcję System.out.print. Jej działanie jest analogiczne, z tą różnicą, że nie następuje przejście do nowego wiersza. Zatem wykonanie instrukcji w postaci: System.out.println("napis1"); System.out.println("napis2"); spowoduje wyświetlenie dwóch wierszy tekstu, z których pierwszy będzie zawierał tekst napis1, a drugi napis2, natomiast wykonanie instrukcji w postaci: System.out.print("napis1"); System.out.print("napis2"); spowoduje wyświetlenie na ekranie tylko jednego wiersza, który będzie zawierał połączony tekst w postaci: napis1napis2. Ćwiczenia do samodzielnego wykonania Ćwiczenie 5.1. Zadeklaruj dwie zmienne typu double. Przypisz im dwie różne wartości zmiennoprzecinkowe, np. 14.5 i 24.45. Wyświetl wartości tych zmiennych na ekranie w dwóch wierszach. Nie korzystaj z instrukcji System.out.println. Ćwiczenie 5.2. Napisz program, który wyświetli na ekranie zbudowany ze znaków alfanumerycznych napis widoczny na rysunku 2.8. Pamiętaj o wykorzystaniu sekwencji znaków specjalnych. Rysunek 2.8. Efekt wykonania ćwiczenia 5.2 Lekcja 6. Operacje na zmiennych Na zmiennych typów prostych (czyli tych poznanych w lekcji 2., z wyjątkiem typów obiektowych) można wykonywać różnorodne operacje, na przykład dodawanie, odejmowanie itp. Operacji tych dokonuje się przy użyciu operatorów, np. operacji dodawania — za pomocą operatora plus zapisywanego jako +, a odejmowania — za pomocą operatora minus zapisywanego jako -. W tej lekcji zostanie pokazane, w jaki sposób są wykonywane 34 Java. Praktyczny kurs operacje na zmiennych i jakie rządzą nimi prawa. Przedstawione zostaną operatory arytmetyczne, bitowe, logiczne, przypisania i porównywania, a także ich priorytety, czyli to, które z nich są silniejsze, a które słabsze. Operatory arytmetyczne Operatory arytmetyczne służą, jak nietrudno się domyślić, do wykonywania operacji arytmetycznych, czyli znanego wszystkim dobrze mnożenia, dodawania itp. W tej grupie występują jednak również mniej znane operatory, takie jak operator inkrementacji i dekrementacji. Wszystkie one są zebrane w tabeli 2.2. Tabela 2.2. Operatory arytmetyczne w Javie Operator Wykonywane działanie * Mnożenie / Dzielenie + Dodawanie - Odejmowanie % Dzielenie modulo (reszta z dzielenia) ++ Inkrementacja (zwiększanie) -- Dekrementacja (zmniejszanie) Podstawowe działania W praktyce korzystanie z większości z tych operatorów sprowadza się do wykonywania typowych działań znanych z lekcji matematyki. Jeśli zatem trzeba dodać do siebie dwie zmienne lub liczbę do zmiennej, używa się operatora +, gdy konieczne jest mnożenie — operatora * itp. Oczywiście operacje arytmetyczne wykonuje się na zmiennych arytmetycznych. Załóżmy zatem, że w kodzie są trzy zmienne typu całkowitoliczbowego int i chcemy wykonać na nich kilka prostych operacji. Zobrazowano to na listingu 2.9; dla zwiększenia czytelności opisu poszczególne linie zostały ponumerowane. Listing 2.9. class Main { public static void main(String args[]) { /*1*/ int a, b, c; /*2*/ a = 10; /*3*/ b = 20; /*4*/ System.out.println("a = " + a + ",b = " + b); /*5*/ c = b - a; /*6*/ System.out.println("b - a = " + c); /*7*/ c = a / 2; /*8*/ System.out.println("a / 2 = " + c); /*9*/ c = a * b; /*10*/ System.out.println("a * b = " + c); /*11*/ c = a + b; /*12*/ System.out.println("a + b = " + c); } } Rozdział 2. Instrukcje języka 35 Linie od 1. do 4. to deklaracje zmiennych a, b i c, przypisanie zmiennej a wartości 10, zmiennej b wartości 20 oraz wyświetlenie tych wartości na ekranie. W linii 5. do zmiennej c trafia wynik odejmowania b – a, czyli wartość 10 (20 – 10 = 10). W linii 7. zmienna c otrzymuje wartość wynikającą z działania a / 2, czyli 5 (10 / 2 = 5). Podobnie jest w linii 9. i 11., gdzie wykonywane są działania mnożenia (c = a * b) oraz dodawania (c = a + b). W liniach 6., 8., 10. i 12. do wyświetlania wyników poszczególnych działań używana jest dobrze nam już znana instrukcja System.out.println. Efekt działania całego programu jest widoczny na rysunku 2.9. Rysunek 2.9. Proste działania arytmetyczne nie sprawiają żadnych problemów Do operatorów arytmetycznych należy również znak %, przy czym, jak zostało wspomniane wcześniej, nie oznacza on obliczania procentów, ale dzielenie modulo, czyli resztę z dzielenia. Przykładowo: działanie 10 % 3 da w wyniku 1. Trójka zmieści się bowiem w dziesięciu trzy razy, pozostawiając resztę 1 (3×3 = 9, 9+1 = 10). Podobnie 21%8 = 5, bo 2×8 = 16, 16+5 = 21. Inkrementacja i dekrementacja Operatory inkrementacji, czyli zwiększania (++), oraz dekrementacji, czyli zmniejszania (--), są z pewnością nieobce osobom znającym języki takie jak C, C++ czy PHP, nowością będą natomiast dla programujących w Pascalu. Operator ++ zwiększa po prostu wartość zmiennej o 1, a -- zmniejsza ją o 1. To jednak nie wszystko. Mogą one bowiem występować w formie przedrostkowej (preinkrementacyjnej) lub przyrostkowej (postinkrementacyjnej). Przykładowo: dla zmiennej o nazwie x forma przedrostkowa będzie miała postać ++x, natomiast przyrostkowa — x++. Obie te postacie powodują zwiększenie wartości zapisanej w zmiennej liczba o 1, ale w przypadku formy przedrostkowej (++x) odbywa się to przed wykorzystaniem zmiennej, a w przypadku formy przyrostkowej (x++) dopiero po jej wykorzystaniu. Osobom początkującym może się to wydawać zupełnie niezrozumiałe, ale wszelkie wątpliwości powinien rozwiać praktyczny przykład. Spójrzmy na listing 2.10. Jakie będą wyniki działania takiego programu? Listing 2.10. class Main { public static void main(String args[]) { /*1*/ int x = 3, y; /*2*/ System.out.println (x++); 36 Java. Praktyczny kurs /*3*/ /*4*/ /*5*/ /*6*/ /*7*/ /*8*/ /*9*/ } } System.out.println System.out.println y = x++; System.out.println y = ++x; System.out.println System.out.println (++x); (x); (y); (y); (++y); Wynikiem jego działania będzie ciąg liczb 3, 5, 5, 5, 7, 8 (rysunek 2.10). Dlaczego? Otóż w linii 1. deklarowane są zmienne x i y, a zmiennej x przypisywana jest wartość 3. W linii 2. zastosowano formę przyrostkową operatora ++, zatem najpierw następuje wyświetlenie wartości zmiennej x (x = 3) na ekranie, a dopiero potem zwiększenie jej wartości o 1 (x = 4). W linii 3. postępowanie jest dokładnie odwrotnie, to znaczy przez zastosowanie formy przedrostkowej najpierw wartość zmiennej x zwiększa się o 1 (x = 5), a dopiero potem jest ona wyświetlana na ekranie. W linii 4. jedyną operacją jest ponowne wyświetlenie wartości x (x = 5). Rysunek 2.10. Efekt wykonania programu ilustrującego działanie operatora ++ W linii 5. najpierw aktualna wartość x (x = 5) trafia do zmiennej y (y = 5) i dopiero potem wartość x jest zwiększana o 1 (x = 6). Linia 6. to wyświetlenie wartości y. W linii 7. najpierw wartość x zwiększa się o 1 (x = 7), a następnie jest przypisywana zmiennej y (y = 7). W ostatniej linii, 9., najpierw wartość y jest zwiększana o 1 (y = 8), a dopiero po tym wyświetlana na ekranie. Wynik działania programu jest widoczny na rysunku 2.10. Operator dekrementacji (--) działa analogicznie do ++, z tą różnicą, że zmniejsza wartość zmiennej o 1. Zmodyfikujmy zatem program z listingu 2.10 w taki sposób, aby wszystkie wystąpienia ++ zastąpić operatorem --. Otrzymamy wtedy kod widoczny na listingu 2.11. Tym razem wynikiem będzie ciąg liczb: 3, 1, 1, 1, -1, -2 (rysunek 2.11). Jak zatem działa ten program? Listing 2.11. class Main { public static void main(String args[]) { /*1*/ int x = 3, y; /*2*/ System.out.println (x--); /*3*/ System.out.println (--x); /*4*/ System.out.println (x); Rozdział 2. Instrukcje języka 37 Rysunek 2.11. Ilustracja działania operatora dekrementacji /*5*/ /*6*/ /*7*/ /*8*/ /*9*/ } } y = x--; System.out.println (y); y = --x; System.out.println (y); System.out.println (--y); W linii 1. powstały zmienne x i y. Pierwszej z nich przypisano wartość 3, dokładnie tak jak w programie z listingu 2.10. W linii 2. zastosowano formę przyrostkową operatora --, zatem najpierw wartość zmiennej x (x = 3) pojawi się na ekranie, a dopiero potem będzie zmniejszona o 1 (x = 2). W linii 3. jest dokładnie odwrotnie, to znaczy przez zastosowanie formy przedrostkowej najpierw wartość zmiennej x zmniejsza się o 1 (x = 1), a dopiero potem pojawia się na ekranie. W linii 4. jedyną operacją jest ponowne wyświetlenie wartości x (x = 1). W linii 5. najpierw aktualna wartość x (x = 1) trafia do zmiennej y (y = 1) i dopiero potem wartość x jest zmniejszana o jeden (x = 0). Linia 6. to wyświetlenie wartości y (y = 1). W 7. linii najpierw wartość x zmniejsza się o 1 (x = –1), a następnie ta wartość trafia do y (y = –1). W linii 8. wyświetlana jest wartość y (y = –1). W ostatniej linii, 9., najpierw następuje zmniejszenie wartości y o 1 (y = –2), a dopiero potem wyświetlenie tej wartości na ekranie. Kiedy napotkamy problemy… Zrozumienie sposobu działania operatorów arytmetycznych nikomu z pewnością nie przysporzyło większych problemów. Jednak i tutaj czekają pewne pułapki. Wróćmy na chwilę do listingu 2.9. Wykonywane było tam m.in. działanie c = a / 2;. Zmienna a miała wartość 10, zmiennej c został zatem przypisany wynik działania 10 / 2, czyli 5. To nie budzi żadnych wątpliwości. Jednak zarówno a, jak i c były typu int, czyli mogły przechowywać jedynie liczby całkowite. Co się stanie, jeśli wynikiem dzielenia a / 2 nie będzie liczba całkowita? Czy pojawi się ostrzeżenie kompilatora lub program podczas działania niespodziewanie zasygnalizuje błąd? Można się o tym przekonać, kompilując i uruchamiając kod widoczny na listingu 2.12. 38 Java. Praktyczny kurs Listing 2.12. class Main { public static void main(String args[]) { int a, b; a = 9; b = a / 2; System.out.println("a = " + a); System.out.println("Wynik działania a / 2 = " + b); b = 8 / 3; System.out.println("b = 8 / 3"); System.out.println("Wartość b to " + b); } } Zmienne a i b są typu int i mogą przechowywać liczby całkowite. Zmiennej a przypisano wartość 9, zmiennej b wynik działania a / 2. Z matematyki wiadomo, że wynikiem działania 9/2 jest 4,5. Nie jest to zatem liczba całkowita. Co się stanie w programie? Otóż wynik zostanie zaokrąglony w dół do najbliższej liczby całkowitej (dokładniej: odrzucona zostanie część ułamkowa). Zatem zmienna b otrzyma wartość 4. Jest to widoczne na rysunku 2.12. Rysunek 2.12. Wynik działania programu z listingu 2.12 Podobnie jeśli do zmiennej b trafi wynik bezpośredniego dzielenia liczb (linia b = 8 / 3;), prawdziwy wynik zostanie zaokrąglony w dół. Innymi słowy, Java sama dopasowuje typy danych (mówiąc fachowo: dokonuje konwersji typu). Początkujący programiści powinni zwrócić na ten fakt szczególną uwagę, gdyż niekiedy prowadzi to do wystąpienia trudnych do wykrycia błędów w aplikacjach. Czemu jednak kompilator nie ostrzega o tym, że taka konwersja zostanie dokonana? Przede wszystkim wynik większości działań jest znany dopiero w trakcie działania programu, zazwyczaj nie ma więc możliwości sprawdzenia już na etapie kompilacji, czy wynik jest właściwego typu. Nie można też dopuścić, by program zgłaszał błędy lub ostrzeżenia za każdym razem, kiedy wynik działania nie będzie dokładnie takiego typu jak zmienna, której jest przypisywany. Dlatego też istnieją zdefiniowane w języku programowania ogólne zasady konwersji typów, które są stosowane automatycznie (tzw. konwersje automatyczne), kiedy tylko jest to możliwe. Jedna z takich reguł mówi właśnie, że jeśli wynikiem operacji jest wartość zmiennoprzecinkowa, a wynik ten ma być przypisany zmiennej całkowitoliczbowej, to część ułamkowa zostaje odrzucona. Rozdział 2. Instrukcje języka 39 Nie oznacza to jednak, że można bezpośrednio przypisać wartość zmiennoprzecinkową zmiennej typu całkowitoliczbowego. Kompilator zaprotestuje wtedy z całą stanowczością. Aby się o tym przekonać, wystarczy spróbować wykonać kompilację kodu z listingu 2.13. Listing 2.13. class Main { public static void main(String args[]) { int a = 9.5; System.out.println("Zmienna a ma wartość = " + a); } } Jak widać na rysunku 2.13, kompilacja się nie udała, a kompilator zgłosił błąd. Tym razem bowiem następuje próba bezpośredniego przypisania liczby ułamkowej zmiennej typu int, która takich wartości przechowywać nie może. Rysunek 2.13. Próba przypisania wartości ułamkowej zmiennej typu int kończy się błędem kompilacji Podobna sytuacja wystąpi przy próbie przekroczenia dopuszczalnego zakresu wartości, który może być reprezentowany przez określony typ danych. Warto to sprawdzić. Zmienna typu int może przechowywać wartości z zakresu od –2 147 483 648 (to jest –231) do 2 147 483 647 (to jest 231–1). Co się zatem stanie, jeśli zmiennej typu int spróbujemy przypisać wartość przekraczającą 2 147 483 647 choćby o 1? Można się spodziewać, że kompilator zgłosi błąd. I tak jest w istocie. Na listingu 2.14 widoczny jest kod realizujący taką instrukcję. Próba kompilacji da efekt widoczny na rysunku 2.14. Błąd tego typu zostanie od razu zasygnalizowany — nie trzeba się takiej pomyłki obawiać. Da się ją naprawić praktycznie od ręki, tym bardziej że kompilator wskazuje konkretne miejsce jej wystąpienia. Listing 2.14. class Main { public static void main(String args[]) { int a = 2147483648; System.out.println("Zmienna a ma wartość = " + a); } } 40 Java. Praktyczny kurs Rysunek 2.14. Próba przypisania do zmiennej liczby przekraczającej dopuszczalny zakres wartości Niestety najczęściej przypisanie wartości przekraczającej zakres danego typu odbywa się już w trakcie działania programu. Jest to sytuacja podobna do przykładu z automatyczną konwersją liczby zmiennoprzecinkowej, tak aby mogła zostać przypisana zmiennej typu int (listing 2.12). Jeśli przypisanie wartości zmiennej jest wynikiem obliczeń wykonanych w trakcie działania programu, kompilator nie będzie w stanie ostrzec programisty, że przekracza dopuszczalny zakres wartości. Taka sytuacja została zobrazowana na listingu 2.15. Listing 2.15. class Main { public static void main(String args[]) { int liczba = 2147483647; liczba = liczba + 1; System.out.println("Zmienna liczba ma wartość = " + liczba); } } Na początku została zadeklarowana zmienna liczba typu int i przypisano jej maksymalną wartość, którą można za pomocą tego typu przedstawić. Następnie do wartości tej zmiennej dodano 1 (liczba = liczba + 1). Tym samym maksymalna wartość została przekroczona. W ostatnim wierszu znajduje się instrukcja wyświetlająca zawartość zmiennej liczba na ekranie. Czy taki program da się skompilować? Oczywiście tak. Tym razem, inaczej niż w poprzednim przykładzie, zakres zostaje przekroczony dopiero w trakcie działania aplikacji, w wyniku wykonania działań arytmetycznych. Co zatem zostanie wyświetlone na ekranie? To, co widać na rysunku 2.15. Rysunek 2.15. Wynik działania programu z listingu 2.15 Początkowo wynik może wydawać się dziwny. Da się jednak zauważyć, że wartość, która pojawiła się na ekranie, to dolny zakres dla typu int (por. tabela 1.1), czyli minimalna wartość, jaką może przyjąć zmienna tego typu. Zatem przekroczenie dopuszczalnej Rozdział 2. Instrukcje języka 41 wartości nie powoduje błędu, ale „zawinięcie” liczby. Zobrazowano to na rysunku 2.16. Arytmetyka wygląda w tym przypadku następująco: INT_MAX+1 = INT_MIN INT_MAX+2 = INT_MIN+1 INT_MAX+3 = INT_MIN+2, jak również: INT_MIN–1 = INT_MAX INT_MIN–2 = INT_MAX+1 INT_MIN–3 = INT_MAX+2, gdzie INT_MIN to minimalna wartość, jaką może przyjąć zmienna typu int, czyli –231, a INT_MAX to wartość maksymalna, czyli 231–1. Rysunek 2.16. Przekroczenie zakresu wartości dla typu int Operatory bitowe Operatory bitowe służą, jak sama nazwa wskazuje, do wykonywania operacji na bitach. Aby się z nimi bliżej zapoznać, warto przypomnieć sobie najpierw przynajmniej podstawowe informacje na temat systemów liczbowych. W systemie dziesiętnym, z którego korzystamy na co dzień, używanych jest dziesięć cyfr, od 0 do 9. W systemie dwójkowym będą z kolei wykorzystywane jedynie dwie cyfry: 0 i 1 (w systemie szesnastkowym cyfry od 0 do 9 i dodatkowo litery od A do F, a w systemie ósemkowym cyfry od 0 do 7). Kolejne liczby są budowane z tych dwóch cyfr dokładnie tak samo jak w systemie dziesiętnym; przedstawiono to w tabeli 2.3. Widać wyraźnie, że np. 4 dziesiętnie to 100 dwójkowo, a 10 dziesiętnie to 1010 dwójkowo. Operatory bitowe pozwalają na wykonywanie operacji właśnie na poszczególnych bitach liczb. Są to znane nam ze szkoły operacje AND (iloczyn), OR (suma), NOT (negacja) oraz XOR (alternatywa wykluczająca) oraz mniej może znane operacje przesunięć bitów. Zebrano je w tabeli 2.4. 42 Java. Praktyczny kurs Tabela 2.3. Reprezentacja liczb w różnych systemach liczbowych System dwójkowy System ósemkowy System dziesiętny System szesnastkowy 0 0 0 0 1 1 1 1 10 2 2 2 11 3 3 3 100 4 4 4 101 5 5 5 110 6 6 6 111 7 7 7 1000 10 8 8 1001 11 9 9 1010 12 10 A 1011 13 11 B 1100 14 12 C 1101 15 13 D 1110 16 14 E 1111 17 15 F Tabela 2.4. Operatory bitowe w Javie Operator Symbol AND & OR | NOT ~ XOR ^ Przesunięcie bitowe w prawo >> Przesunięcie bitowe w lewo << Przesunięcie bitowe w prawo z wypełnieniem zerami >>> Operatory logiczne Operacje logiczne można wykonywać na argumentach, które mają wartość logiczną prawda lub fałsz. W językach programowania wartości te są oznaczane jako true i false. W przypadku Javy oba te słowa muszą być zapisane dokładnie w taki sposób, to znaczy małymi literami, inaczej nie zostaną rozpoznane przez kompilator. Jeśli zatem mamy przykładowe wyrażenie 0 < 1, to ma ono wartość logiczną true (prawda), jako że niewątpliwie zero jest mniejsze od jedności, z kolei wyrażenie 2 > 5 ma wartość logiczną false (fałsz), bo dwa nie jest większe od pięciu. Operacje logiczne to znane ze szkoły logiczne AND (iloczyn), OR (suma) oraz NOT (negacja). Przedstawiono je w tabeli 2.5. Rozdział 2. Instrukcje języka 43 Tabela 2.5. Operatory logiczne w Javie Operator Symbol AND && OR || NOT ! Iloczyn logiczny Wynikiem operacji AND (iloczyn logiczny) jest wartość true wtedy i tylko wtedy, kiedy oba argumenty mają wartość true. W każdym innym przypadku wynikiem jest false. Przedstawiono to w tabeli 2.6. Tabela 2.6. Iloczyn logiczny Argument 1 Argument 2 Wynik true true true true false false false true false false false false Suma logiczna Wynikiem operacji OR (suma logiczna) jest wartość false wtedy i tylko wtedy, kiedy oba argumenty mają wartość false. W każdym innym przypadku wynikiem jest true. Przedstawiono to w tabeli 2.7. Tabela 2.7. Suma logiczna Argument 1 Argument 2 Wynik true true true true false true false true true false false false Negacja logiczna Operacja NOT (negacja logiczna) zmienia po prostu wartość argumentu na przeciwną. Jeśli więc argument miał wartość true, będzie miał wartość false i odwrotnie — jeśli miał wartość false, będzie miał wartość true. Zobrazowano to w tabeli 2.8. Tabela 2.8. Negacja logiczna Argument Wynik true false false true 44 Java. Praktyczny kurs Operatory przypisania Operacje przypisania są dwuargumentowe i powodują przypisanie argumentu prawostronnego argumentowi lewostronnemu. Najprostsza taka operacja była już stosowana wielokrotnie, odbywa się ona przy użyciu operatora = (równa się). Napisanie a = 5 oznacza, że zmiennej a ma być przypisana wartość 5. Oprócz prostego operatora = w Javie występuje szereg operatorów złożonych, tzn. takich, w których przypisaniu towarzyszy dodatkowa operacja arytmetyczna lub bitowa. Przykładowo istnieje operator +=, który oznacza: przypisz argumentowi z lewej strony wartość wynikającą z dodawania argumentu znajdującego się z lewej strony i argumentu znajdującego się z prawej strony operatora. Choć brzmi to z początku nieco zawile, w rzeczywistości jest bardzo proste i znacznie upraszcza niektóre konstrukcje programistyczne. Przykładowy zapis: a += b tłumaczy się po prostu jako: a = a + b Zatem wykonanie kodu widocznego na listingu 2.16 spowoduje przypisanie zmiennej a wartości 15, następnie dodanie do niej wartości 10 oraz wyświetlenie ostatecznego stanu zmiennej (a = 25) na ekranie. Listing 2.16. class Main { public static void main(String args[]) { int a = 15; a += 10; System.out.println("Zmienna a ma wartość = " + a); } } W Javie występuje cała grupa tych operatorów (są one zebrane w tabeli 2.9). Schematycznie można przedstawić ich znaczenie następująco: arg1 op= arg2 co oznacza działanie: arg1 = arg1 op arg2 czyli a += b oznacza a = a + b, a = b oznacza a = a b, a %= b oznacza a = a % b itd. Operatory porównywania (relacyjne) Operatory porównywania służą oczywiście do porównywania argumentów. Wynikiem ich działania jest wartość logiczna true lub false, czyli prawda lub fałsz. Operatory te są zebrane w tabeli 2.10. Wynikiem operacji argument1 == argument2 będzie true, jeżeli argumenty są sobie równe, oraz false, jeżeli są różne. Czyli 4 == 5 ma wartość false (fałsz), a 2 == 2 ma wartość true (prawda). Podobnie 2 < 3 ma wartość true (2 jest mniejsze od 3), ale 4 < 1 ma wartość false (4 jest większe, a nie mniejsze od 1). Rozdział 2. Instrukcje języka 45 Tabela 2.9. Operatory przypisania i ich znaczenie w Javie Argument 1 Operator Argument 2 Znaczenie x = y x=y x += y x=x+y x -= y x=x–y x *= y x=xy x /= y x=x/y x %= y x=x%y x <<= y x = x << y x >>= y x = x >> y x >>>= y x = x >>> y x &= y x=x&y x |= y x=x|y x ^= y x=x^y Tabela 2.10. Operatory porównywania w Javie Operator Opis == Wynikiem jest true, jeśli argumenty są sobie równe. != Wynikiem jest true, jeśli argumenty są różne. > Wynikiem jest true, jeśli argument prawostronny jest mniejszy od lewostronnego. < Wynikiem jest true, jeśli argument prawostronny jest większy od lewostronnego. >= Wynikiem jest true, jeśli argument prawostronny jest mniejszy od lewostronnego lub jest mu równy. <= Wynikiem jest true, jeśli argument prawostronny jest większy od lewostronnego lub jest mu równy. Priorytety operatorów Sama znajomość operatorów to jednak nie wszystko. Trzeba jeszcze wiedzieć, jaki mają one priorytet, czyli jaka jest kolejność ich wykonywania. Wiadomo z matematyki, że np. mnożenie jest silniejsze od dodawania, zatem najpierw mnożymy, potem dodajemy. W Javie jest podobnie — siła każdego operatora jest ściśle określona. Zostało to przedstawione w tabeli 2.119. Im wyższa pozycja w tabeli, tym wyższy priorytet operatora. Operatory znajdujące się na jednym poziomie (w jednym wierszu) mają ten sam priorytet. 9 Tabela nie przedstawia wszystkich operatorów występujących w Javie, ale jedynie omówione w bieżącej lekcji. 46 Java. Praktyczny kurs Tabela 2.11. Priorytety operatorów w Javie Grupa operatorów Symbole Inkrementacja przyrostkowa ++, -- Inkrementacja przedrostkowa, negacja ++, --, ~, ! Mnożenie, dzielenie *, /, % Przesunięcia bitowe <<, >>, >>> Porównywania 1 <, >, <=, >= Porównywania 2 ==, != Bitowe AND & Bitowe XOR ^ Bitowe OR | Logiczne AND && Logiczne OR || Warunkowe ? Przypisania =, +=, -=, *=, /=, %=, >>=, <<=, >>>=, &=, ^=, |= Ćwiczenia do samodzielnego wykonania Ćwiczenie 6.1. Zadeklaruj trzy zmienne typu int: a, b i c. Przypisz im dowolne wartości całkowite. Wykonaj działanie a % b % c. Wynik tego działania przypisz czwartej zmiennej o nazwie wynik, jej wartość wyświetl na ekranie. Spróbuj tak dobrać wartości zmiennych, aby wynikiem nie było 0. Ćwiczenie 6.2. Zadeklaruj zmienną typu int o dowolnej nazwie. Przypisz jej wartość 256. Wykonaj trzy działania: przesunięcie bitowe w prawo o dwa miejsca, przesunięcie bitowe w lewo o dwa miejsca oraz przesunięcie bitowe w prawo o dwa miejsca z wypełnieniem zerami. Wynik wszystkich trzech działań wyświetl na ekranie. Zaobserwuj zachodzące prawidłowości. Ćwiczenie 6.3. Zadeklaruj dwie zmienne typu int. Przypisz im dowolne wartości oraz wykonaj na nich działania sumy bitowej oraz iloczynu bitowego. Wyświetl wyniki działań na ekranie. Ćwiczenie 6.4. Zadeklaruj zmienną typu int o dowolnej nazwie i przypisz jej dowolną wartość całkowitą. Dwukrotnie wykonaj na niej operację XOR, wykorzystując jako drugi argument również dowolną liczbę całkowitą. Użyj operatora przypisania ^=. Zaobserwuj otrzymany wynik i zastanów się, czemu ma on właśnie taką postać. Rozdział 2. Instrukcje języka 47 Ćwiczenie 6.5. Umieść w programie zmienną a o wartości 55 i zmienną b o wartości 88 oraz instrukcję wykonującą następujące działanie: a += (b - 3 ^ 4 << 2 | 8 - b >>> 8 * 2) - (a & a - 1); Wynik powyższej operacji wyświetl na ekranie. Spróbuj przewidzieć, jaka wartość zostanie wyświetlona, a następnie uruchom program i sprawdź swoje przypuszczenia. Instrukcje sterujące Lekcja 7. Instrukcja warunkowa if…else Instrukcje warunkowe służą, jak sama nazwa wskazuje, do sprawdzania warunków. Dzięki temu w zależności od tego, czy dany warunek jest prawdziwy, czy nie, można wykonać różne działania. W Javie instrukcja warunkowa ma ogólną postać: if (warunek) { //instrukcje do wykonania, kiedy warunek jest prawdziwy } else { //instrukcje do wykonania, kiedy warunek jest fałszywy } Zatem po słowie kluczowym if w nawiasie okrągłym umieszcza się warunek do sprawdzenia, a za nim w nawiasie klamrowym blok instrukcji do wykonania, kiedy warunek jest prawdziwy. Dalej następuje słowo kluczowe else, a za nim, również w nawiasie klamrowym10, blok instrukcji, które zostaną wykonane, kiedy warunek będzie fałszywy. Blok else jest opcjonalny, zatem prawidłowa jest również konstrukcja: if (warunek){ //instrukcje do wykonania, kiedy warunek jest prawdziwy } Zobaczmy, jak to wygląda w praktyce: sprawdzimy, czy zmienna typu int jest większa od 0, i wyświetlimy odpowiedni komunikat na ekranie. Kod realizujący takie zadanie jest widoczny na listingu 2.17. Listing 2.17. class Main { public static void main(String args[]) { int liczba = 15; if (liczba > 0) { System.out.println("Zmienna liczba jest większa od zera."); } else { 10 Nawiasy klamrowe można pominąć, jeśli w bloku ma być wykonana tylko jedna instrukcja. 48 Java. Praktyczny kurs System.out.println("Zmienna liczba nie jest większa od zera."); } } } Na początku deklarujemy zmienną liczba typu int i przypisujemy jej wartość 15. Następnie za pomocą instrukcji if sprawdzamy, czy jest ona większa od 0. Wykorzystujemy w tym celu operator porównywania < (lekcja 6.). Ponieważ zmienna liczba otrzymała stałą wartość równą 15, która na pewno jest większa od 0, zostanie oczywiście wyświetlony napis: Zmienna liczba jest większa od zera. Jeśli przypiszemy jej wartość ujemną lub równą 0, to zostanie wykonany blok else. Ponieważ w nawiasach klamrowych występujących po if i po else mogą znaleźć się dowolne instrukcje, można tam również umieścić kolejne instrukcje if…else. Innymi słowy, instrukcje te można zagnieżdżać. Schematycznie wygląda to następująco: if (warunek1) { if(warunek2) { } else { } } else { if (warunek3) { } else { } } Spróbujmy wykorzystać taką konstrukcję do wykonania bardziej skomplikowanego przykładu. Napiszemy program rozwiązujący klasyczne równanie kwadratowe. Jak wiadomo ze szkoły, ma ono postać: A x 2 B x C 0 , gdzie A, B i C to parametry równania. Równanie ma rozwiązanie w zbiorze liczb rzeczywistych, jeśli parametr (delta) równy B 2 4 A C jest większy lub równy 0. Jeśli jest równa 0, mamy B jedno rozwiązanie równe , jeśli jest większa od 0, mamy dwa rozwiązania: 2 A B B i x2 . Taka liczba warunków sprawia, że to zadanie świetnie x1 2 A 2 A nadaje się do przećwiczenia działania instrukcji if…else. Jedyną niedogodnością programu będzie to, że parametry A, B i C będą musiały być wprowadzone bezpośrednio w kodzie programu, ponieważ nie zostały jeszcze przedstawione sposoby na wczytywanie danych z klawiatury (będzie to omówione dopiero w rozdziale 6.). Cały program jest zaprezentowany na listingu 2.18. Listing 2.18. class Main { public static void main (String args[]) { //deklaracja zmiennych int A = 1, B = 1, C = -2; //wyświetlenie parametrów równania Rozdział 2. Instrukcje języka 49 System.out.println ("Parametry równania:\n"); System.out.println ("A: " + A + " B: " + B + " C: " + C + "\n"); //sprawdzenie, czy jest to równanie kwadratowe //A jest równe zero, równanie nie jest kwadratowe if (A == 0) { System.out.println ("To nie jest równanie kwadratowe: A = 0!"); } //A jest różne od zera, równanie jest kwadratowe else { //obliczenie delty double delta = B * B - 4 * A * C; //jeśli delta mniejsza od zera if (delta < 0) { System.out.println ("Delta < 0."); System.out.println ("To równanie nie ma rozwiązania w zbiorze liczb rzeczywistych"); } //jeśli delta większa lub równa zero else{ //deklaracja zmiennej pomocniczej double wynik; //jeśli delta równa zero if (delta == 0){ //obliczenie wyniku wynik = -B / (2 * A); System.out.println ("Rozwiązanie: x = " + wynik); } //jeśli delta większa od zera else{ //obliczenie wyników wynik = (-B + Math.sqrt(delta)) / (2 * A); System.out.print ("Rozwiązanie: x1 = " + wynik); wynik = (-B - Math.sqrt(delta)) / (2 * A); System.out.println (", x2 = " + wynik); } } } } } Zaczynamy od zadeklarowania i zainicjowania trzech zmiennych, A, B i C, odzwierciedlających parametry równania. Następnie wyświetlamy je na ekranie. Za pomocą instrukcji if sprawdzamy, czy zmienna A jest równa 0. Jeśli tak, oznacza to, że równanie nie jest kwadratowe. Jeśli jednak A jest różne od 0, możemy przystąpić do obliczenia delty. Wynik obliczeń przypisujemy zmiennej o nazwie delta. Zmienna ta jest typu double, to znaczy może przechowywać liczby zmiennoprzecinkowe. To konieczne, jako że delta nie musi być liczbą całkowitą. Kolejny krok to sprawdzenie, czy delta jest mniejsza od 0. Jeśli jest, oznacza to, że równanie nie ma rozwiązań w zbiorze liczb rzeczywistych, wyświetlamy więc stosowny komunikat na ekranie. Gdy jednak delta nie jest mniejsza od 0, przystępujemy do sprawdzenia kolejnych warunków. Jeżeli delta jest równa 0, można od razu obli- 50 Java. Praktyczny kurs czyć rozwiązanie równania ze wzoru -B / (2 * A). Wynik obliczeń przypisujemy zmiennej pomocniczej o nazwie wynik i wyświetlamy komunikat z rozwiązaniem na ekranie. W przypadku gdy delta jest większa od 0, są dwa pierwiastki (rozwiązania) równania. Obliczane są w liniach: wynik = (-B + Math.sqrt(delta)) / (2 * A); oraz: wynik = (-B - Math.sqrt(delta)) / (2 * A); Rezultat obliczeń wyświetlamy rzecz jasna na ekranie, tak jak na rysunku 2.17. Nieznana nam do tej pory instrukcja Math.sqrt(delta) powoduje obliczenie pierwiastka kwadratowego (drugiego stopnia) z wartości zawartej w zmiennej delta. Rysunek 2.17. Program obliczający rozwiązania równania kwadratowego Zagnieżdżanie instrukcji if sprawdza się dobrze w tak prostym przykładzie jak powyższy, jednak z każdym kolejnym poziomem staje się coraz bardziej nieczytelne. Nadmiernemu zagnieżdżaniu można zapobiec przez zastosowanie nieco zmodyfikowanej instrukcji w postaci if…else if. Załóżmy, że mamy znaną już z listingu 2.18 konstrukcję w postaci: if(warunek1){ instrukcje1 } else{ if(warunek2){ instrukcje2 } else{ if(warunek3){ instrukcje3 } else{ instrukcje4 } } } Innymi słowy, trzeba sprawdzić po kolei warunki warunek1, warunek2 i warunek3 i w zależności od tego, które są prawdziwe, wykonać instrukcje instrukcje1, instrukcje2, instrukcje3 lub instrukcje4. Zatem instrukcje1 są wykonywane, kiedy warunek1 Rozdział 2. Instrukcje języka 51 jest prawdziwy; instrukcje2 — kiedy warunek1 jest fałszywy, a warunek2 prawdziwy; instrukcje3 — kiedy prawdziwy jest warunek3, natomiast fałszywe są warunek1 i warunek2; instrukcje4 — kiedy wszystkie warunki są fałszywe. Jest to zobrazowane w tabeli 2.12. Tabela 2.12. Wykonanie instrukcji w zależności od stanu warunków warunek1 warunek2 warunek3 Wykonaj instrukcje Prawdziwy Bez znaczenia Bez znaczenia instrukcje1 Fałszywy Prawdziwy Bez znaczenia instrukcje2 Fałszywy Fałszywy Prawdziwy instrukcje3 Fałszywy Fałszywy Fałszywy instrukcje4 Taką konstrukcję można zamienić na identyczną znaczeniowo (semantycznie), ale prostszą w zapisie instrukcję if…else if w postaci: if(warunek1){ //instrukcje1 } else if (warunek2){ instrukcje2 } else if(warunek3){ instrukcje3 } else{ instrukcje4 } Taki zapis tłumaczy się następująco: jeśli prawdziwy jest warunek1, wykonaj instrukcje1; w przeciwnym razie, jeżeli prawdziwy jest warunek2, wykonaj instrukcje2; w przeciwnym razie, jeśli prawdziwy jest warunek3, wykonaj instrukcje3; w przeciwnym razie wykonaj instrukcje4. Taka konstrukcja pozwala uprościć nieco przykładowy kod z listingu 2.18 obliczający pierwiastki równania kwadratowego. Zamiast sprawdzać, czy delta jest mniejsza, większa, czy równa 0, za pomocą zagnieżdżonej instrukcji if, łatwiej skorzystać z instrukcji if…else if. Zostało to zobrazowane na listingu 2.19. Listing 2.19. class Main { public static void main (String args[]) { //deklaracja zmiennych int A = 1, B = 1, C = -2; //wyświetlenie parametrów równania System.out.println ("Parametry równania:\n"); System.out.println ("A: " + A + " B: " + B + " C: " + C + "\n"); //sprawdzenie, czy jest to równanie kwadratowe //A jest równe zero, równanie nie jest kwadratowe if (A == 0) { 52 Java. Praktyczny kurs System.out.println ("To nie jest równanie kwadratowe: A = 0!"); } //A jest różne od zera, równanie jest kwadratowe else{ //obliczenie delty double delta = B * B - 4 * A * C; //jeśli delta mniejsza od zera if (delta < 0) { System.out.println ("Delta < 0."); System.out.println ("To równanie nie ma rozwiązania w zbiorze liczb rzeczywistych."); } //jeśli delta równa zero else if(delta == 0) { //obliczenie wyniku double wynik = - B / (2 * A); System.out.println ("Rozwiązanie: x = " + wynik); } //jeśli delta większa od zera else if(delta > 0) { double wynik; //obliczenie wyników wynik = (- B + Math.sqrt(delta)) / (2 * A); System.out.print ("Rozwiązanie: x1 = " + wynik); wynik = (- B - Math.sqrt(delta)) / (2 * A); System.out.println (", x2 = " + wynik); } } } } Warto zauważyć, że można również zrezygnować z badania ostatniego warunku. Obecnie mamy następującą konstrukcję: if(delta < 0){ //instrukcje } else if(delta == 0){ //instrukcje } else if(delta > 0){ //instrukcje } Przy takim zapisie, jeżeli dwa pierwsze warunki są fałszywe, ostatni musi być prawdziwy. Przecież jeśli delta (która jest zmienną typu double) nie jest mniejsza od 0 (warunek pierwszy) i nie jest równa 0 (warunek drugi), to z pewnością jest większa od 0. Można więc pominąć sprawdzanie warunku trzeciego. Równie dobrze można użyć zatem fragmentu w postaci: if(delta < 0){ //instrukcje } else if(delta == 0){ //instrukcje Rozdział 2. Instrukcje języka 53 } else{ //instrukcje } Ćwiczenia do samodzielnego wykonania Ćwiczenie 7.1. Zadeklaruj dwie zmienne typu int: a i b. Przypisz im dowolne wartości całkowite. Użyj instrukcji if do sprawdzenia, czy dzielenie modulo a przez b daje w wyniku 0. Wyświetl stosowny komunikat na ekranie. Ćwiczenie 7.2. Napisz program, którego zadaniem będzie stwierdzenie, czy równanie kwadratowe ma rozwiązanie w zbiorze liczb rzeczywistych. Ćwiczenie 7.3. Napisz program obliczający pole prostokąta o długości i wysokości podanej w postaci dwóch zmiennych. Program powinien reagować odpowiednio w sytuacji, gdy dane będą niepoprawne (jedna lub obie wartości będą ujemne lub równe zero) — nie należy wtedy obliczać pola, ale wyświetlić stosowny komunikat. Ćwiczenie 7.4. Napisz program podający informację, czy kwadrat o danej długości boku może być wpisany w okrąg o danym promieniu. Ćwiczenie 7.5. Napisz program podający informację, czy z trzech odcinków o zadanych długościach da się zbudować trójkąt prostokątny. Lekcja 8. Instrukcja switch i operator warunkowy W lekcji 7. została przedstawiona instrukcja warunkowa if w kilku różnych postaciach. W praktyce wystarczyłaby ona do obsługi wszelkich zadań programistycznych związanych ze sprawdzaniem warunków. Okazuje się jednak, że czasami wygodniejsze są inne konstrukcje warunkowe i właśnie im jest poświęcona lekcja 8. Omówione tu zostaną dokładnie instrukcja switch, nazywana instrukcją wyboru, oraz tak zwany operator warunkowy. Czytelnicy znający C bądź C++ mogą tę lekcję pominąć, gdyż działanie tych instrukcji jest im znane, w Javie mają one bowiem taką samą postać jak w wymienionych językach. Z kolei osoby dopiero rozpoczynające naukę czy też znające np. Pascala powinny wnikliwie zapoznać się z przedstawionym materiałem. 54 Java. Praktyczny kurs Instrukcja switch Instrukcja switch pozwala w wygodny i przejrzysty sposób sprawdzić ciąg warunków i wykonywać różny kod w zależności od tego, czy są one prawdziwe, czy fałszywe. W najprostszej postaci może być ona odpowiednikiem ciągu if…else if, w którym jako warunek jest wykorzystywane porównywanie zmiennej z wybraną liczbą. Daje ona programiście dodatkowe możliwości, jak choćby wykonania tego samego kodu dla kilku warunków. Taki oto przykładowy zapis: if(liczba == 1){ instrukcje1 } else if (liczba == 2){ instrukcje2 } else if(liczba == 3){ instrukcje3 } else{ instrukcje4 } można przedstawić za pomocą instrukcji switch, która będzie wyglądać następująco: switch(liczba){ case 1 : instrukcje1; break; case 2 : instrukcje2; break; case 3 : instrukcje3; break; default : instrukcje4; } W rzeczywistości w nawiasie okrągłym po switch nie musi występować nazwa zmiennej, ale może się tam znaleźć dowolne wyrażenie, którego wynikiem będzie wartość arytmetyczna (czyli wartość typu byte, short, int albo long), lub też, począwszy od Java 7, ciąg znaków (wartość typu String). W postaci ogólnej cała konstrukcja wygląda zatem następująco: switch(wyrażenie){ case wartość1 : instrukcje1; break; case wartość2 : instrukcje2; break; case wartość3 : instrukcje3; break; default : instrukcje4; Rozdział 2. Instrukcje języka 55 } Należy ją rozumieć jako: sprawdź wartość wyrażenia wyrażenie; jeśli jest to wartość1, wykonaj instrukcje1 i przerwij wykonywanie bloku switch (instrukcja break); jeżeli jest to wartość2, wykonaj instrukcje2 i przerwij wykonywanie bloku switch; jeśli jest to wartość3, wykonaj instrukcje3 i przerwij wykonywanie bloku switch; jeżeli w żadnym z powyższych przypadków dopasowanie nie nastąpiło, wykonaj instrukcje4 i zakończ blok switch. Oczywiście w praktyce liczba bloków case nie jest ograniczona i może ich być dowolnie wiele. Zobaczmy, jak to działa na konkretnym przykładzie, który został zaprezentowany na listingu 2.20. Listing 2.20. class Main { public static void main (String args[]) { int liczba = 25; switch(liczba){ case 25 : System.out.println("liczba = 25"); break; case 15 : System.out.println("liczba = 15"); break; default : System.out.println("Zmienna liczba nie jest równa ani 15, ani 25."); } } } Na początku deklarujemy zmienną o nazwie liczba i typie int, czyli taką, która może przechowywać liczby całkowite, i przypisujemy jej wartość 25. Następnie używamy instrukcji switch do sprawdzenia stanu zmiennej. W tym wypadku wartością wyrażenia będącego parametrem instrukcji switch jest oczywiście wartość zapisana w zmiennej liczba. Nic nie stoi jednak na przeszkodzie, aby parametr ten był wyliczany dynamicznie w samej instrukcji. Jest to widoczne w przykładzie na listingu 2.21. Listing 2.21. class Main { public static void main (String args[]) { int liczba1 = 2; int liczba2 = 1; switch(liczba1 * 5 / (liczba2 + 1)){ case 5 : System.out.println("Wartość wyrażenia to 5."); break; case 15 : System.out.println("Wartość wyrażenia to 15."); break; default : System.out.println("Wartość wyrażenia nie jest równa ani 5, ani 15."); } 56 Java. Praktyczny kurs } } Zatem instrukcja switch najpierw oblicza wartość wyrażenia występującego w nawiasie okrągłym (jeśli jest to zmienna, w to miejsce jest podstawiana jej wartość), a następnie próbuje ją dopasować do jednej z wartości występujących po słowach case. Jeśli zgodność zostanie stwierdzona, zostaną wykonane instrukcje występujące w danym bloku case. Jeżeli nie uda się dopasować wartości wyrażenia do żadnej z wartości występujących po słowach case, zostaje wykonany blok default. Blok default nie jest jednak obligatoryjny i jeśli nie jest w programie potrzebny, można go pominąć. Szczególną uwagę należy jednak zwrócić na instrukcję break, która przerywa wykonywanie danego bloku case i tym samym powoduje opuszczenie instrukcji switch. Jej przypadkowe pominięcie może doprowadzić do nieoczekiwanych wyników i błędów w programie. Aby przekonać się, w jaki sposób działa instrukcja switch bez instrukcji break, wystarczy zmodyfikować listing 2.20, usuwając z niego wszystkie słowa break. Powstanie wtedy kod widoczny na listingu 2.22. Listing 2.22. class Main { public static void main (String args[]) { int liczba = 25; switch(liczba){ case 25: System.out.println("liczba = 25"); case 15: System.out.println("liczba = 15"); default: System.out.println("Zmienna liczba nie jest równa ani 15, ani 25."); } } } Wynik jego działania może zadziwić — jest widoczny na rysunku 2.18. Ten program raczej nie jest w stanie podać, jaką tak naprawdę wartość ma zmienna liczba, prawda? Jak zatem działa przedstawiony kod? Otóż jeśli w którymś z bloków (przypadków) case zostanie wykryta zgodność z wyrażeniem występującym za switch, zostaną wykonane wszystkie dalsze instrukcje, aż do napotkania instrukcji break (a dokładniej: dowolnej instrukcji powodującej przerwanie działania bloku switch) lub dotarcia do końca instrukcji switch. Rozdział 2. Instrukcje języka 57 Rysunek 2.18. Wynik braku instrukcji break w bloku switch W kodzie z listingu 2.22 zgodność jest stwierdzona już w pierwszym przypadku (case 25), zostaje zatem wykonana instrukcja System.out.println("liczba = 25"). Ponieważ jednak nie występuje po niej break, w dalszej kolejności zostaje wykonana instrukcja System.out.println("liczba = 15") (nie ma znaczenia, że należy ona do przypadku case 15!). Po tej instrukcji również nie ma break, zatem zostanie wykonana instrukcja występująca po default, czyli System.out.println("Zmienna liczba nie jest równa ani 15, ani 25."). Ktoś może spytać: czemu w takim razie break nie jest obligatoryjny, tak jak np. w języku C#11? Chodzi o to, że takie właśnie zachowanie bloku switch dla wprawnego programisty może być ułatwieniem przy pisaniu kodu (bo pozwala na wykonanie wielu bloków case dla jednej wartości wyrażenia). Właściwość tę można sprawnie wykorzystać przy tworzeniu aplikacji. Początkujący powinni jednak zapamiętać, że po każdym przypadku case należy umieścić instrukcję break, co pozwoli uniknąć niespodziewanych problemów. Operator warunkowy Operator warunkowy ma postać: warunek ? wartość1 : wartość2 Oznacza ona: jeżeli warunek jest prawdziwy, podstaw za wartość wyrażenia wartość1, w przeciwnym wypadku podstaw wartość2. Sprawdźmy w praktyce, jak może wyglądać jego wykorzystanie — zobrazowano to w kodzie widocznym na listingu 2.23. Listing 2.23. class Main { public static void main (String args[]){ int liczba1 = 10; int liczba2; liczba2 = liczba1 < 0 ? -1 : 1; System.out.println(liczba2); } } 11 W języku C# każdy przypadek case musi kończyć się instrukcją przekazującą sterowanie poza blok switch. Najczęściej jest to instrukcja break, ale można ją również zastąpić np. przez instrukcję goto. 58 Java. Praktyczny kurs Najważniejsza jest tu oczywiście linia liczba2 = liczba1 < 0 ? -1 : 1;. Po lewej stronie operatora przypisania = znajduje się zmienna (liczba2), natomiast po stronie prawej wyrażenie warunkowe, czyli linia ta oznacza: przypisz zmiennej liczba2 wartość wyrażenia warunkowego. Jaka jest ta wartość? Trzeba przeanalizować samo wyrażenie: liczba1 < 0 ? -1 : 1. Oznacza ono, zgodnie z tym, co zostało napisane w poprzednim akapicie: jeżeli wartość zmiennej liczba1 jest mniejsza od 0, przypisz wyrażeniu wartość –1, w przeciwnym razie (zmienna liczba1 większa lub równa 0) przypisz wartość 1. Ponieważ zmiennej liczba1 zostało przypisane wcześniej 10, wartością całego wyrażenia będzie 1 i ta właśnie wartość zostanie przypisana zmiennej liczba2. Łatwo zauważyć, że operator warunkowy wykonuje podobne zadanie co instrukcja if…else. Kod z listingu 2.23 można zatem w prosty sposób przerobić tak, by ten sam efekt został osiągnięty za pomocą instrukcji warunkowej (ćwiczenie 8.3). Ćwiczenia do samodzielnego wykonania Ćwiczenie 8.1. Napisz instrukcję switch zawierającą 10 bloków case sprawdzających kolejne wartości całkowite od 0 do 9. Pamiętaj o instrukcjach break. Ćwiczenie 8.2. Zadeklaruj zmienną typu boolean. Wykorzystaj wyrażenie warunkowe do sprawdzenia, czy wynikiem dowolnego dzielenia modulo jest wartość 0. Jeśli tak, przypisz tej zmiennej wartość true, w przeciwnym wypadku przypisz wartość false. Ćwiczenie 8.3. Zmień kod z listingu 2.23 tak, aby badanie stanu zmiennej liczba1 było wykonywane za pomocą instrukcji warunkowej if…else. Ćwiczenie 8.4. Zadeklaruj zmienną przechowującą liczby całkowite i przypisz jej dowolną wartość początkową. Napisz instrukcję, która w przypadku gdy wartość zmiennej jest mniejsza od zera, zamieni tę wartość na dodatnią (zachowa się jak wartość bezwzględna w matematyce). Użyj operatora warunkowego. Ćwiczenie 8.5. Napisz instrukcję switch badającą wartość pewnej zmiennej typu całkowitoliczbowego i wyświetlającą komunikaty w następujących sytuacjach: a) zmienna ma wartość 1, 4, 8; b) zmienna ma wartość 2, 3, 7; c) wszystkie inne przypadki. Postaraj się całość zapisać możliwie krótko. Rozdział 2. Instrukcje języka 59 Lekcja 9. Pętle Pętle są konstrukcjami programistycznymi, które pozwalają na wykonywanie powtarzających się czynności. Przykładowo aby wyświetlić na ekranie dziesięć razy dowolny napis, najłatwiej skorzystać właśnie z odpowiedniej pętli. Oczywiście można też dziesięć razy napisać w kodzie programu System.out.println("napis"), jednak będzie to z pewnością niezbyt wygodne. W tej lekcji zostaną przedstawione wszystkie występujące w Javie postacie pętli, czyli pętle for, while oraz do…while, a także wprowadzona w Java 5.0 (1.5) pętla typu foreach. Omówione zostaną także różnice występujące między nimi oraz przykłady wykorzystania różnych typów pętli. Pętla for Pętla for ma ogólną postać: for (wyrażenie początkowe; wyrażenie warunkowe; wyrażenie modyfikujące){ instrukcje do wykonania } wyrażenie początkowe jest stosowane do zainicjalizowania zmiennej używanej jako licznik liczby wykonań pętli, wyrażenie warunkowe określa warunek, jaki musi być spełniony, aby dokonać kolejnego przejścia w pętli, natomiast wyrażenie modyfikujące jest zwykle używane do modyfikacji zmiennej będącej licznikiem. Najłatwiej wyjaśnić to wszystko na praktycznym przykładzie. Znajduje się on na listingu 2.24. Listing 2.24. class Main { public static void main (String args[]) { for(int i = 0; i < 10; i++){ System.out.println("Pętle w Javie"); } } } Taką konstrukcję należy rozumieć następująco: zadeklaruj zmienną i i przypisz jej wartość 0 (int i = 0), następnie tak długo, jak długo wartość i jest mniejsza od 10 (i < 10), wykonuj instrukcję System.out.println("Pętle w Javie") oraz zwiększaj wartość i o 1 (i++). Dzięki temu na ekranie pojawi się dziesięć razy napis Pętle w Javie (rysunek 2.19). Zmienna i jest nazywana zmienną iteracyjną, czyli kontrolującą kolejne przebiegi (iteracje) pętli. Jeśli chcemy zobaczyć, jak zmienia się stan zmiennej i w trakcie kolejnych przebiegów, możemy zmodyfikować instrukcję wyświetlającą napis, tak aby podawała i tę informację. Wystarczy drobna poprawka w postaci: for(int i = 0; i < 10; i++){ System.out.println("[i = " + i + "] Pętle w Javie"); } 60 Java. Praktyczny kurs Rysunek 2.19. Wynik działania prostej pętli typu for Po jej wprowadzeniu w każdej linii będzie wyświetlana również wartość i, tak jak zostało to przedstawione na rysunku 2.20. Rysunek 2.20. Niewielka modyfikacja kodu pozwala podejrzeć stan zmiennej iteracyjnej Wyrażenie początkowe to w powyższym przykładzie int i = 0, wyrażenie warunkowe to i < 10, a wyrażenie modyfikujące to i++. Okazuje się, że mamy dużą dowolność umiejscawiania tych wyrażeń. Na przykład wyrażenie modyfikujące, które najczęściej jest wykorzystywane do modyfikacji zmiennej iteracyjnej, można umieścić wewnątrz samej pętli, to znaczy zastosować konstrukcję o następującej schematycznej postaci: for (wyrażenie początkowe; wyrażenie warunkowe;){ instrukcje do wykonania wyrażenie modyfikujące } Zmieńmy zatem program z listingu 2.24 tak, aby wyrażenie modyfikujące znalazło się wewnątrz pętli. Zostało to zobrazowane na listingu 2.25. Listing 2.25. class Main { public static void main (String args[]) { for(int i = 0; i < 10;){ System.out.println("Pętle w Javie"); Rozdział 2. Instrukcje języka 61 i++; } } } Program ten jest funkcjonalnym odpowiednikiem poprzedniego przykładu. Szczególną uwagę należy zwrócić natomiast na znak średnika, występujący po wyrażeniu warunkowym. Mimo że wyrażenie modyfikujące znalazło się teraz wewnątrz bloku pętli, ten średnik jest niezbędny. Jeśli go zabraknie, kompilacja z pewnością się nie uda. Skoro udało nam się usunąć wyrażenie modyfikujące z nawiasu okrągłego pętli, spróbujmy dokonać takiego zabiegu również z wyrażeniem początkowym. Jest to prosty zabieg techniczny. Schematycznie taka konstrukcja wygląda następująco: wyrażenie początkowe; for (; wyrażenie warunkowe;){ instrukcje do wykonania wyrażenie modyfikujące; } Jak może to wyglądać w praktyce, zobrazowano na listingu 2.26. Całe wyrażenie początkowe zostało przeniesione po prostu przed pętlę. To jest nadal w pełni funkcjonalny odpowiednik programu z listingu 2.24. Ponownie należy zwrócić uwagę na umiejscowienie średników pętli for. Oba są niezbędne do prawidłowego działania kodu. Listing 2.26. class Main { public static void main (String args[]) { int i = 0; for(; i < 10;){ System.out.println("Pętle w Javie"); i++; } } } Kolejną ciekawą możliwością jest połączenie wyrażenia warunkowego i modyfikującego. Pozostawimy wyrażenie początkowe przed pętlą, natomiast wyrażenie modyfikujące ponownie wprowadzimy do konstrukcji pętli, łącząc je jednak z wyrażeniem warunkowym. Taka konstrukcja jest przedstawiona na listingu 2.27. Listing 2.27. class Main { public static void main (String args[]) { int i = 0; for(; i++ < 10;){ System.out.println("Pętle w Javie"); } } } 62 Java. Praktyczny kurs Istnieje również możliwość przeniesienia wyrażenia warunkowego do wnętrza pętli, jednak wymaga to zastosowania instrukcji break, która pojawi się w lekcji 10. Warto też zwrócić uwagę, że przedstawiony wyżej kod nie jest w pełni funkcjonalnym odpowiednikiem pętli z listingów 2.24 – 2.26, choć w pierwszej chwili wyniki działania wydają się identyczne. Odkrycie odpowiedzi na pytanie, dlaczego tak jest, oraz wprowadzenie odpowiednich poprawek (tak aby program z listingu 2.24 był dokładnym odpowiednikiem wcześniejszych) pozostanie jednak ćwiczeniem do samodzielnego wykonania (wskazówka znajduje się w przykładach dotyczących pętli while). Pętla while Pętla typu while służy, podobnie jak pętla for, do wykonywania powtarzających się czynności. Pętlę for zwykle wykorzystuje się, kiedy liczba powtarzanych operacji jest znana (na przykład zapisana w jakiejś zmiennej), natomiast while, kiedy nie jest z góry znana (na przykład wynika z działania jakiejś funkcji). Jest to jednak podział umowny: oba typy pętli można zapisać w taki sposób, aby były swoimi funkcjonalnymi odpowiednikami. Ogólna postać pętli while wygląda następująco: while (wyrażenie warunkowe){ instrukcje; } Instrukcje są wykonywane dopóty, dopóki wyrażenie warunkowe jest prawdziwe. Zobaczmy zatem, jak za pomocą pętli while wyświetlić na ekranie dziesięć razy dowolny napis. To zadanie jest wykonywane przez kod widoczny na listingu 2.28. Pętlę taką rozumiemy następująco: dopóki i jest mniejsze od 10 (i < 10), wyświetlaj napis na ekranie (System.out.println("Pętle w Javie");), za każdym razem zwiększając i o 1 (i++). Listing 2.28. class Main { public static void main (String args[]) { int i = 0; while(i < 10){ System.out.println("Pętle w Javie"); i++; } } } Nic nie stoi na przeszkodzie, aby tak jak w przypadku pętli for, wyrażenie warunkowe było jednocześnie wyrażeniem modyfikującym. Pętla taka wyglądałaby wtedy jak na listingu 2.29. Listing 2.29. class Main { public static void main (String args[]) { int i = 0; while(i++ < 10){ System.out.println("Pętle w Javie"); Rozdział 2. Instrukcje języka 63 } } } Należy jednak przy tym zwrócić uwagę, że choć programy z listingów 2.28 i 2.29 wykonują to samo zadanie, nie są to w pełni funkcjonalne odpowiedniki. Można to zauważyć po dodaniu instrukcji wyświetlającej stan zmiennej i w obu wersjach kodu. Wystarczy zmodyfikować instrukcję System.out.println("Pętle w Javie") tak samo jak w przypadku pętli for: System.out.println("[i = " + i + "] Pętle w Javie"). Wynik działania kodu, kiedy zmienna i jest modyfikowana wewnątrz pętli (tak jak na listingu 2.28), będzie taki sam jak na rysunku 2.20, natomiast wynik jego działania, kiedy zmienna ta jest modyfikowana w wyrażeniu warunkowym (tak jak na listingu 2.29), przedstawiony jest na rysunku 2.21. Rysunek 2.21. Stan zmiennej i, kiedy jest modyfikowana w wyrażeniu warunkowym Widać wyraźnie, że w pierwszym przypadku wartości zmiennej zmieniają się od 0 do 9, natomiast w drugiej sytuacji od 1 do 10. Nie ma to znaczenia, kiedy wyświetla się jedynie serię napisów, jednak gdyby wykorzystywać zmienną i do jakichś celów, np. dostępu do komórek tablicy (podrozdział „Tablice”), ta drobna z pozoru różnica spowodowałaby poważne konsekwencje w działaniu programu. Dobrym ćwiczeniem do samodzielnego wykonania będzie natomiast poprawienie programu z listingu 2.28 tak, aby działał dokładnie tak samo jak ten z listingu 2.29 (podpunkt „Ćwiczenia do samodzielnego wykonania”). Pętla do…while Odmianą pętli while jest pętla do…while, której schematyczna postać wygląda następująco: do{ instrukcje; } while(warunek); 64 Java. Praktyczny kurs Konstrukcję tę należy rozumieć następująco: wykonuj instrukcje, dopóki warunek jest prawdziwy12. Zobaczmy zatem, jak wygląda znane nam już dobrze zadanie wyświetlenia dziesięciu napisów, jeśli do jego realizacji zostanie wykorzystana pętla do…while. Zobrazowano to w kodzie przedstawionym na listingu 2.30. Listing 2.30. class Main { public static void main (String args[]) { int i = 0; do{ System.out.println("[i = " + i + "] Pętle w Javie"); } while(i++ < 9); } } Zwróćmy uwagę na to, jak w tej chwili wygląda warunek. Od razu widać podstawową różnicę w stosunku do pętli while. Otóż w pętli while najpierw jest sprawdzany warunek, a dopiero potem są wykonywane instrukcje. W przypadku pętli do…while jest dokładnie odwrotnie — najpierw są wykonywane instrukcje, a dopiero potem jest sprawdzany warunek. Dlatego też tym razem trzeba sprawdzić, czy i jest mniejsze od 9. Gdyby pozostawić starą postać wyrażenia warunkowego: i++ < 10, napis zostałby wyświetlony jedenaście razy. Takiemu zachowaniu można zapobiec, wprowadzając wyrażenie modyfikujące zmienną i do wnętrza pętli; sama pętla miałaby wtedy postać: int i = 0; do{ System.out.println("[i = " + i + "] Pętle w Javie"); i++; } while(i < 10); Warunek teraz pozostaje w starej postaci i otrzymujemy odpowiednik pętli while. Ten sam efekt można też uzyskać bez przesuwania wyrażenia modyfikującego do wnętrza pętli, wystarczy użyć przedrostkowej wersji operatora ++. Niech to jednak pozostanie zadaniem do samodzielnego wykonania. Ta cecha (czyli wykonywanie instrukcji przed sprawdzeniem warunku) pętli while jest bardzo ważna, oznacza bowiem, że pętla tego typu jest wykonywana zawsze co najmniej raz, nawet jeśli warunek jest fałszywy. Można się o tym przekonać w bardzo prosty sposób — wprowadzając fałszywy warunek i obserwując zachowanie programu, np.: int i = 0; do{ System.out.println("Pętle w Javie"); i++; 12 Osoby znające język programowania Pascal z pewnością zauważą w tej konstrukcji podobieństwo do pętli repeat…until. Rozdział 2. Instrukcje języka 65 } while(i < 0); Warunek w tej postaci jest ewidentnie fałszywy, jako że zmienna i już w trakcie inicjacji jest równa 0 (nie może być więc jednocześnie mniejsza od 0!). Mimo to po wykonaniu powyższego kodu na ekranie pojawi się jeden napis Pętle w Javie. Jest to najlepszy dowód na to, że warunek jest sprawdzany nie przed przebiegiem pętli, ale dopiero po każdym przebiegu. Pętla typu foreach Począwszy od Javy w wersji 5.0 (1.5), dostępny jest nowy rodzaj pętli. To pętla nazywana pętlą typu foreach lub rozszerzoną pętlą for (ang. enhanced for). Pozwala ona na automatyczną iterację po kolekcji obiektów lub też po tablicy13. Jej działanie zostanie pokazane właśnie w tym drugim przypadku (osoby nieznające pojęcia tablic powinny najpierw zapoznać się z materiałem zawartym w lekcji 11.). Jeśli bowiem mamy tablicę tab zawierającą wartości pewnego typu, to do przejrzenia wszystkich jej elementów możemy użyć konstrukcji o postaci: for(typ nazwa: tab){ //instrukcje } W takim przypadku w kolejnych przebiegach pętli pod nazwa będzie podstawiana wartość kolejnej komórki. Pętla będzie działała tak długo, aż zostaną przejrzane wszystkie elementy tablicy (lub kolekcji) typu typ bądź też zostanie przerwana za pomocą jednej z instrukcji pozwalających na taką operację (lekcja 10.). Przykład użycia takiej pętli został przedstawiony na listingu 2.31. Listing 2.31. class Main { public static void main (String args[]) { int tab[] = {1, 2, 3, 4, 5, 4, 3, 2, 1}; for(int val: tab){ System.out.println(val); } } } Ćwiczenia do samodzielnego wykonania Ćwiczenie 9.1. Wykorzystując pętlę for, napisz program, który wyświetli liczby całkowite od 1 do 10 podzielne przez 2. 13 Ściślej rzecz ujmując: po obiekcie udostępniającym tzw. iterator. 66 Java. Praktyczny kurs Ćwiczenie 9.2. Nie zmieniając żadnej instrukcji wewnątrz pętli, zmodyfikuj kod z listingu 2.28 w taki sposób, aby był funkcjonalnym odpowiednikiem kodu z listingu 2.29. Ćwiczenie 9.3. Wykorzystując pętlę while, napisz program, który wyświetli liczby całkowite od 1 do 20 niepodzielne przez 3. Ćwiczenie 9.4. Zmodyfikuj kod z listingu 2.30 w taki sposób, aby w wyrażeniu warunkowym pętli do…while zamiast operatora < wykorzystać <=. Ćwiczenie 9.5. Napisz program, który wyświetli na ekranie liczby od 1 do 20 i zaznaczy przy każdej z nich, czy jest to liczba parzysta, czy nieparzysta. Zrób to: a) wykorzystując pętlę for, b) wykorzystując pętlę while, c) wykorzystując pętlę do…while. Lekcja 10. Instrukcje break i continue W lekcji 9. zostały omówione cztery rodzaje pętli, czyli konstrukcji programistycznych pozwalających na łatwe wykonywanie powtarzających się czynności. Były to pętle for, while, do…while i foreach. Teraz zostaną przedstawione dwie dodatkowe współpracujące z pętlami instrukcje: break i continue. Pierwsza z nich powoduje przerwanie wykonywania pętli i opuszczenie jej bloku, natomiast druga — przerwanie bieżącej iteracji i przejście do kolejnej. W lekcji znajdą się informacje, jak stosować te instrukcje w przypadku prostych pętli pojedynczych, a także w przypadku pętli dwu- lub wielokrotnie zagnieżdżonych. Omówiony zostanie również temat etykiet, które dodatkowo zmieniają zachowanie instrukcji break i continue, a tym samym umożliwiają tworzenie bardziej zaawansowanych konstrukcji pętli. Instrukcja break Instrukcja break pojawiła się już w lekcji 8. przy omawianiu instrukcji switch. Znaczenie break w języku programowania jest zgodne z nazwą, w języku angielskim break znaczy „przerywać”. Dokładnie tak zachowywała się ta konstrukcja w przypadku instrukcji switch, tak też zachowuje się w przypadku pętli — po prostu przerywa ich wykonanie. Dzięki temu można np. tak zmodyfikować pętlę for, aby wyrażenie warunkowe znalazło się wewnątrz niej. Kod realizujący takie zadanie został przedstawiony na listingu 2.32. Rozdział 2. Instrukcje języka 67 Listing 2.32. class Main { public static void main (String args[]) { for(int i = 0; ; i++){ System.out.println("Pętle w Javie " + i); if(i == 9){ break; } } } } Ponownie szczególną uwagę należy zwrócić na wyrażenia znajdujące się w nawiasie okrągłym pętli. Mimo że usunięte zostało wyrażenie warunkowe, umieszczony po nim średnik musiał pozostać na swoim miejscu, inaczej nie udałaby się kompilacja programu. Sama pętla działa w taki sposób, że w każdym przebiegu wykonywana jest instrukcja warunkowa if sprawdzająca, czy zmienna i osiągnęła wartość 9, czyli badająca warunek i == 9. Jeśli ten warunek będzie prawdziwy, będzie to oznaczało, że na ekranie zostało wyświetlonych 10 napisów, zostanie więc wykonana instrukcja break, która przerwie działanie pętli. Na tym etapie można też pokazać, jak pozbyć się wszystkich wyrażeń z nawiasu okrągłego pętli. Wyrażenie początkowe przenosi się przed pętlę, a modyfikujące i warunkowe do jej wnętrza. Zostało to przedstawione na listingu 2.33. Tego typu konstrukcja niegdyś była tylko ciekawostką programistyczną, raczej niestosowaną w praktycznych rozwiązaniach, obecnie jednak coraz częściej widuje się ją w różnych projektach programistycznych, gdzie pełni rolę zamiennika dla pętli while. Listing 2.33. class Main { public static void main (String args[]) { int i = 0; for(; ;){ System.out.println("Pętle w Javie " + i); if(i++ >= 9){ break; } } } } Należy pamiętać, że instrukcja break przerywa działanie pętli, w której się znajduje. Jeśli zatem mamy zagnieżdżone pętle for, a instrukcja break występuje w pętli wewnętrznej, zostanie przerwana jedynie pętla wewnętrzna. Pętla zewnętrzna nadal będzie działać. Spójrzmy na kod znajdujący się na listingu 2.34: to właśnie dwie zagnieżdżone pętle for. 68 Java. Praktyczny kurs Listing 2.34. class Main { public static void main (String args[]) { for(int i = 0; i < 3; i++){ for(int j = 0; j < 3; j++){ System.out.println(i + " " + j); } } } } Wynikiem działania takiego programu będzie ciąg liczb widoczny na rysunku 2.22. Pierwszy pionowy ciąg liczb określa stan zmiennej i, natomiast drugi — stan zmiennej j. Ta konstrukcja działa w ten sposób, że w każdym przebiegu pętli zewnętrznej są wykonywane trzy przebiegi pętli wewnętrznej (a zatem instrukcja wyświetlająca stan zmiennych wykonywana jest w sumie dziewięć razy). Stąd też ciągi liczb, które pojawiają się na ekranie. Rysunek 2.22. Wynik działania dwóch zagnieżdżonych pętli typu for W pętli wewnętrznej umieśćmy teraz instrukcję warunkową if(i == 2) break;, tak aby cała konstrukcja wyglądała następująco: for(int i = 0; i < 3; i++){ for(int j = 0; j < 3; j++){ if(i == 2) break; System.out.println(i + " " + j); } } Zgodnie z tym, co zostało napisane powyżej, za każdym razem, kiedy i osiągnie wartość 2, przerywana będzie pętla wewnętrzna, a sterowanie będzie przekazywane do pętli zewnętrznej (zostało to zobrazowane na rysunku 2.23). Tym samym po uruchomieniu programu znikną ciągi liczb wyświetlane, kiedy i było równe 2 (rysunek 2.24). Instrukcja break powoduje bowiem przejście do kolejnej iteracji zewnętrznej pętli. Zastosowanie instrukcji break nie ogranicza się oczywiście jedynie do pętli typu for. Może być ona również stosowana w połączeniu z pozostałymi rodzajami pętli, podobnie jak instrukcja continue, która zostanie przedstawiona już za chwilę. Rozdział 2. Instrukcje języka 69 Rysunek 2.23. Instrukcja break powoduje przerwanie wykonywania pętli wewnętrznej Rysunek 2.24. Zastosowanie instrukcji warunkowej w połączeniu z break Instrukcja continue O ile instrukcja break powodowała przerwanie wykonywania pętli oraz jej opuszczenie, o tyle instrukcja continue powoduje przejście do kolejnej iteracji. A zatem jeśli wewnątrz pętli znajdzie się instrukcja continue, bieżąca iteracja (przebieg) zostanie przerwana oraz rozpocznie się kolejna (chyba że bieżąca iteracja była ostatnią). Zobaczmy, jak to działa, na konkretnym przykładzie. Na listingu 2.35 jest widoczna pętla for, która wyświetla liczby całkowite z zakresu 1 – 20 podzielne przez 2 (por. ćwiczenia do samodzielnego wykonania z lekcji 9.). Listing 2.35. class Main { public static void main (String args[]) { for(int i = 1; i <= 20; i++){ if(i % 2 != 0) continue; System.out.println(i); } } } Wynik działania tego programu jest widoczny na rysunku 2.25. Sama pętla jest skonstruowana w dobrze nam znany sposób. W środku znajduje się instrukcja warunkowa sprawdzająca warunek i % 2 != 0, czyli badająca, czy reszta z dzielenia i przez 2 jest różna od 0. Jeśli warunek jest prawdziwy (reszta jest różna od 0), oznacza to, że wartość zawarta w i nie jest podzielna przez 2, wykonywana jest zatem instrukcja continue. Jak wiadomo, zgodnie z tym, co zostało napisane powyżej, powoduje ona rozpoczęcie kolejnej iteracji pętli, czyli zwiększenie wartości zmiennej i o 1 i przejście na początek pętli (do pierwszej instrukcji). Tym samym jeśli wartość i jest niepodzielna przez 2, nie zostanie wykonana znajdująca się za warunkiem instrukcja System.out.println(i), zatem dana wartość nie pojawi się na ekranie, a to właśnie było naszym celem. 70 Java. Praktyczny kurs Rysunek 2.25. Wynik działania programu wyświetlającego liczby z zakresu 1 – 20 podzielne przez 2 Rzecz jasna to zadanie można wykonać bez użycia instrukcji continue (podpunkt „Ćwiczenia do samodzielnego wykonania”), ale bardzo dobrze ilustruje ono istotę jej działania. Schemat działania instrukcji continue dla pętli for pokazano na rysunku 2.26. Rysunek 2.26. Schemat działania instrukcji continue dla pętli typu for Instrukcja continue w przypadku pętli zagnieżdżonych działa tak jak instrukcja break, to znaczy jej działanie dotyczy tylko pętli, w której się znajduje. A zatem jeśli jest umieszczona w pętli wewnętrznej, powoduje przejście do kolejnej iteracji pętli wewnętrznej, a jeśli znajduje się w pętli zewnętrznej — do kolejnej iteracji pętli zewnętrznej. Schemat tego działania został przedstawiony na rysunku 2.27. Rysunek 2.27. Sposób działania instrukcji continue w przypadku zagnieżdżonych pętli for Rozdział 2. Instrukcje języka 71 Etykiety Instrukcje break i continue oprócz tego, że można je wykorzystać w postaci omówionej powyżej, występują również w połączeniu z etykietami. Postacie te nazywa się czasami etykietowanym break (ang. labeled break) oraz etykietowanym continue (ang. labeled continue). Etykieta to wyznaczone miejsce w kodzie programu. Ustala się ją poprzez podanie jej nazwy zakończonej znakiem dwukropka, np.: etykieta1: Teoretycznie taką etykietę można umieścić w dowolnym miejscu między instrukcjami, choć aby mogła zostać użyta, wolno wstawić ją jedynie przed instrukcją rozpoczynającą pętlę. Inaczej mówiąc, przykładowy fragment kodu: int liczba = 0; etykieta1: if(liczba == 0){ System.out.println("tekst"); } jest formalnie poprawny i uda się go skompilować, jednak taka etykieta nie będzie miała praktycznego znaczenia. Zupełnie inaczej będzie, jeśli umieścimy ją tuż przed instrukcją iteracji (pętlą). Widać to na listingu 2.36 — jest to modyfikacja przykładu z zagnieżdżonymi pętlami for, który był omawiany podczas objaśniania działania zwykłej instrukcji break. Listing 2.36. class Main { public static void main (String args[]) { etykieta1: for(int i = 0; i < 3; i++){ for(int j = 0; j < 3; j++){ if(i == 1) break etykieta1; System.out.println(i + " " + j); } } } } Do kodu została dodana etykieta o nazwie etykieta1 umieszczona prawidłowo tuż przed pętlami for. W wewnętrznej pętli for znajduje się instrukcja break etykieta1, która oznacza: przerwij działanie wszystkich pętli aż do etykiety etykieta1. Innymi słowy, przerwij działanie pętli i idź do etykiety etykieta1 (dokładniej: do bloku oznaczonego tą etykietą). Instrukcja ta zostanie wykonana, kiedy zmienna i osiągnie wartość 1, zatem po uruchomieniu programu na ekranie pojawią się tym razem jedynie trzy rzędy liczb (rysunek 2.28). Powtórzmy jeszcze raz: break etykieta powoduje przerwanie wszystkich pętli aż do miejsca oznaczonego etykietą, czyli wszystkich pętli znajdujących się za etykietą. To znaczy, że jeżeli będziemy mieli potrójnie zagnieżdżone pętle w postaci: 72 Java. Praktyczny kurs Rysunek 2.28. Efekt działania etykietowanej instrukcji break for(int i = 0; i < 3; i++){ etykieta1: for(int j = 0; j < 3; j++){ for(int k = 0; k < 3; k++){ break etykieta1; } } } to po wykonaniu break zostanie przerwane działanie dwóch pętli wewnętrznych (gdzie zmiennymi iteracyjnymi są j oraz k), a sterowanie zostanie przekazane pętli zewnętrznej (gdzie zmienną iteracyjną jest i). Ogólny schemat działania etykietowanej instrukcji break jest widoczny na rysunku 2.29. Widać wyraźnie, że po napotkaniu break następuje wyjście z pętli i przejście do wykonania kolejnej instrukcji, umieszczonej za pętlą. Rysunek 2.29. Schemat działania etykietowanej instrukcji break w przypadku pętli for Etykietowana instrukcja continue działa nieco inaczej. Również powoduje przerwanie bieżącej iteracji (przebiegu) pętli i przejście do miejsca oznaczonego etykietą. Potem jednak następuje ponowne wejście do pętli znajdującej się tuż za etykietą. Zostało to zilustrowane na rysunku 2.30. Rysunek 2.30. Schemat działania etykietowanej instrukcji continue w przypadku pętli for Rozdział 2. Instrukcje języka 73 Dlatego też jeśli zmodyfikujemy program z listingu 2.36 tak, aby zamiast instrukcji break etykieta1 występowała instrukcja continue etykieta1, jak jest to widoczne na listingu 2.37, wynik jego działania będzie taki jak na rysunku 2.31. Jest to najlepszy dowód, że inaczej niż etykietowane break, etykietowane continue powoduje ponowne wejście do pętli. W przeciwnym wypadku w pierwszej kolumnie nie pojawiłyby się cyfry 2 (por. rysunek 2.28). Listing 2.37. class Main { public static void main (String args[]) { etykieta1: for(int i = 0; i < 3; i++){ for(int j = 0; j < 3; j++){ if(i == 1) continue etykieta1; System.out.println(i + " " + j); } } } } Rysunek 2.31. Wynik działania programu z listingu 2.37 Ćwiczenia do samodzielnego wykonania Ćwiczenie 10.1. Napisz program, który wyświetli na ekranie nieparzyste liczby z zakresu 1 – 20. Wykorzystaj pętlę for i instrukcję continue. Ćwiczenie 10.2. Napisz program, który wyświetli na ekranie nieparzyste liczby z zakresu 1 – 20. Wykorzystaj pętlę while i instrukcję continue. Ćwiczenie 10.3. Napisz program, który wyświetli na ekranie liczby z zakresu 1 – 30 niepodzielne przez 3. Wykonaj trzy warianty ćwiczenia: 74 Java. Praktyczny kurs a) bez używania instrukcji continue, b) z użyciem instrukcji continue, c) z użyciem instrukcji break po wykryciu podzielności liczby przez 3 (osoby początkujące mogą potraktować tę wersję jako zadanie „z gwiazdką”). Ćwiczenie 10.4. Napisz program, który wyświetli na ekranie liczby z zakresu 1 – 100 podzielne przez 4, ale niepodzielne przez 8 i przez 10. Wykorzystaj instrukcję continue. Ćwiczenie 10.5. Zmodyfikuj program znajdujący się na listingu 2.35 tak, aby wynik jego działania pozostał bez zmian, ale nie było potrzeby używania instrukcji continue. Tablice Tablica to stosunkowo prosta struktura danych pozwalająca na przechowanie uporządkowanego zbioru elementów danego typu. Obrazowo zostało to przedstawione na rysunku 2.32. Jak widać, struktura ta składa się z ponumerowanych kolejno komórek (numeracja zaczyna się od 0). Każda taka komórka może przechowywać pewną porcję danych. To, jakiego rodzaju będą to dane, jest określone przez typ tablicy. Jeśli zostanie zadeklarowana tablica typu całkowitoliczbowego (int), będzie ona mogła zawierać liczby całkowite. Jeżeli będzie to natomiast typ znakowy (char), poszczególne komórki będą mogły zawierać różne znaki. Rysunek 2.32. Schematyczna struktura tablicy Lekcja 11. Podstawowe operacje na tablicach Tablice to struktury danych występujące w większości popularnych języków programowania. Nie mogło ich zatem zabraknąć również w Javie. W tej lekcji zostaną przedstawione podstawowe typy tablic jednowymiarowych, sposoby ich deklarowania oraz używania. Omówiona będzie również bardzo ważna dla tej struktury właściwość length. Nie pominiemy też kwestii sposobu numeracji komórek każdej tablicy, która, jak to zostało zaprezentowane na rysunku 2.32, zawsze zaczyna się od 0. Wiedzą o tym osoby programujące w C i C++, może być to jednak nowością dla programujących w Pascalu. Rozdział 2. Instrukcje języka 75 Proste tablice w Javie Tablice w Javie są obiektami (więcej o obiektach w rozdziale 3.). Aby móc skorzystać z tablicy, trzeba najpierw zadeklarować zmienną tablicową, a następnie utworzyć samą tablicę (obiekt tablicy). Schemat deklaracji wygląda następująco: typ_tablicy nazwa_tablicy[]; lub też (co ma identyczne znaczenie): typ_tablicy[] nazwa_tablicy; Jest ona zatem bardzo podobna do deklaracji zwykłej zmiennej typu prostego (takiego jak int, char, short itp.), wyróżnikiem jest natomiast nawias kwadratowy. Taka deklaracja to jednak nie wszystko, powstała dopiero zmienna o nazwie nazwa_tablicy, dzięki której będzie można odwoływać się do tablicy, ale samej tablicy jeszcze wcale nie ma! Trzeba ją dopiero utworzyć, korzystając z operatora new w postaci: new typ_tablicy[liczba_elementów]; Można jednocześnie zadeklarować i utworzyć tablicę, korzystając z konstrukcji: typ_tablicy nazwa_tablicy[] = new typ_tablicy[liczba_elementów]; lub: typ_tablicy[] nazwa_tablicy = new typ_tablicy[liczba_elementów]; bądź też rozbić te czynności na dwie instrukcje. Schemat postępowania wygląda wtedy następująco: typ_tablicy nazwa_tablicy[]; /*tutaj mogą znaleźć się inne instrukcje*/ nazwa_tablicy = new typ_tablicy[liczba_elementów]; Jak widać, pomiędzy deklaracją a utworzeniem tablicy można umieścić również inne instrukcje. W przypadku prostych programów najczęściej wykorzystuje się jednak sposób pierwszy, to znaczy jednoczesną deklarację zmiennej tablicowej i utworzenie tablicy. Od razu warto zobaczyć, jak to wygląda w praktyce. Zadeklarujemy tablicę liczb całkowitych (typu int) o nazwie tab i wielkości jednego elementu. Temu elementowi przypiszemy dowolną wartość, a następnie wyświetlimy ją na ekranie. Kod realizujący to zadanie jest widoczny na listingu 2.38. Listing 2.38. class Main { public static void main (String args[]) { int tab[] = new int[1]; tab[0] = 10; System.out.println("Pierwszy element tablicy ma wartość: " + tab[0]); } } 76 Java. Praktyczny kurs W pierwszym kroku nastąpiła deklaracja zmiennej tablicowej tab i przypisanie jej nowo utworzonej tablicy typu int o rozmiarze 1 (int tab[] = new int[1]). Oznacza to, że tablica ta ma tylko jedną komórkę i może przechowywać naraz tylko jedną liczbę całkowitą. W drugim kroku jedynemu elementowi tej tablicy przypisano wartość 10. Zwróćmy uwagę na sposób odwołania się do tego elementu: tab[0] = 10. Ponieważ w Javie (podobnie jak w C i C++) elementy tablic są numerowane od 0 (por. rysunek 2.32), pierwszy z nich ma indeks 0! To bardzo ważne: pierwszy element to indeks 0, drugi to indeks 1, trzeci to indeks 2 itd. Jeśli zatem chcemy odwołać się do pierwszego elementu tablicy tab, napiszemy tab[0] (indeks żądanego elementu umieszcza się w nawiasie kwadratowym za nazwą tablicy). W trzecim kroku zawartość wskazanego elementu jest po prostu wyświetlana na ekranie przy użyciu dobrze nam znanej instrukcji System.out. println. Sprawdźmy teraz, co się stanie, jeśli się pomylimy i spróbujemy odwołać się do nieistniejącego elementu tablicy. Przykładowo: jeśli zadeklarujemy tablicę 10-elementową, ale zapomnimy, że tablice są indeksowane od 0, i spróbujemy odwołać się do elementu o indeksie 10 (element o takim indeksie oczywiście nie istnieje). Taki scenariusz został zrealizowany we fragmencie kodu przedstawionym na listingu 2.39. Listing 2.39. class Main { public static void main (String args[]) { int tab[] = new int[10]; tab[10] = 5; System.out.println("Dziesiąty element tablicy ma wartość: " + tab[10]); } } Taki program da się bez problemu skompilować, jednak próba jego uruchomienia przyniesie rezultat widoczny na rysunku 2.33. Choć wygląda to bardzo groźnie, tak naprawdę nie stało się nic strasznego. Wykonywanie programu zostało rzecz jasna przerwane, jednak najważniejsze jest to, że próba nieprawidłowego odwołania się do tablicy została wykryta przez środowisko uruchomieniowe (maszynę wirtualną Javy) i samo odwołanie, które mogłoby naruszyć stabilność systemu, nie nastąpiło. Zamiast tego został wygenerowany tak zwany wyjątek (wyjątkami zajmiemy się w rozdziale 4.) o nazwie ArrayIndexOutOfBoundsException (indeks tablicy poza zakresem) i program zakończył działanie. Rysunek 2.33. Próba odwołania się do nieistniejącego elementu tablicy Rozdział 2. Instrukcje języka 77 To bardzo ważna cecha Javy. Analogicznie działający program w C czy C++ (tzn. odwołujący się do nieistniejących elementów tablic) potrafi poczynić spore spustoszenie w systemie. Jego działanie jest też zupełnie nieprzewidywalne. Mimo wystąpienia błędu może on dalej działać poprawnie (to bardzo optymistyczne założenie), może działać, ale dawać nieprawidłowe wyniki albo może nastąpić zamknięcie programu przez system operacyjny, a w ekstremalnym przypadku także naruszenie stabilności samego systemu operacyjnego14. W Javie sprawa jest prosta — jeśli nastąpi przekroczenie zakresu tablicy, system wygeneruje wyjątek. Ważną sprawą jest inicjalizacja tablicy, czyli przypisanie jej komórkom wartości początkowych. W przypadku niewielkich tablic takiego przypisania można dokonać, ujmując żądane wartości w nawias klamrowy. Nie trzeba wtedy korzystać z operatora new. Środowisko Javy utworzy tablicę samodzielnie i zapisze w jej kolejnych komórkach podane wartości. Schematycznie deklaracja taka wygląda następująco: typ_tablicy nazwa_tablicy[] = {wartość1, wartość2, ... , wartośćn} Przykładowo aby zadeklarować 6-elementową tablicę liczb całkowitych typu int i przypisać jej kolejnym komórkom wartości od 1 do 6, należy zastosować konstrukcję: int tablica[] = {1, 2, 3, 4, 5, 6} O tym, że taka konstrukcja jest prawidłowa, można przekonać się, uruchamiając kod widoczny na listingu 2.40, gdzie ta tablica została zadeklarowana. Do wyświetlenia jej zawartości na ekranie została użyta pętla typu for (por. lekcja 9.) i instrukcja System.out. println. Wynik działania tego programu jest widoczny na rysunku 2.34. Listing 2.40. class Main { public static void main (String args[]) { int tablica[] = {1, 2, 3, 4, 5, 6}; for(int i = 0; i < 6; i++){ System.out.println("tab[" + i + "] = " + tablica[i]); } } } Rysunek 2.34. Wynik działania pętli for użytej do wyświetlenia zawartości tablicy 14 Nie powinno się to jednak zdarzyć w żadnym z nowoczesnych systemów operacyjnych. 78 Java. Praktyczny kurs Właściwość length Kiedy rozmiar tablicy jest większy, zamiast stosować przedstawione wyżej przypisanie w nawiasie klamrowym, lepiej do wypełnienia jej danymi wykorzystać zwyczajną pętlę. A zatem jeśli mamy np. 20-elementową tablicę liczb typu int i chcemy zapisać w każdej z jej komórek liczbę 15, możemy wykorzystać w tym celu konstrukcję: for(int i = 0; i < 20, i++){ tab[i] = 15; } Gdy jednak pisze się w ten sposób, łatwo o pomyłkę. Może się np. zdarzyć, że programista zmieni rozmiar tablicy, ale zapomni zmodyfikować pętlę. Nierzadko zdarza się też sytuacja, kiedy rozmiar tablicy nie jest z góry znany i zostaje ustalony dopiero w trakcie działania programu. Na szczęście ten problem został rozwiązany w bardzo prosty sposób. Dzięki temu, że tablice w Javie są obiektami, każda z nich ma przechowującą jej rozmiar właściwość length, która może być tylko odczytywana. A zatem przy użyciu konstrukcji w postaci: nazwa_tablicy.length otrzymuje się rozmiar dowolnej tablicy. Należy oczywiście pamiętać o numerowaniu poszczególnych komórek tablicy od 0. Aby odwołać się do ostatniego elementu tablicy, należy w związku z tym odwołać się do indeksu o wartości length – 1. W praktyce program wypełniający prostą tablicę liczb typu short kolejnymi wartościami całkowitymi oraz wyświetlający jej zawartość na ekranie będzie wyglądał tak jak na listingu 2.41. Listing 2.41. class Main { public static void main (String args[]) { short tablica[] = new short[10]; for(short i = 0; i < tablica.length; i++){ tablica[i] = i; } for(short i = 0; i < tablica.length; i++){ System.out.println("tablica[" + i + "] = " + tablica[i]); } } } Ćwiczenia do samodzielnego wykonania Ćwiczenie 11.1. Napisz program, w którym zostanie utworzona 10-elementowa tablica liczb typu int. Za pomocą pętli for zapisz w kolejnych komórkach liczby od 101 do 110. Zawartość tablicy wyświetl na ekranie. Rozdział 2. Instrukcje języka 79 Ćwiczenie 11.2. Napisz program, w którym zostanie utworzona 10-elementowa tablica liczb typu int. Użyj pętli for do wypełnienia jej danymi w taki sposób, aby w kolejnych komórkach znalazły się liczby od 10 do 100 (czyli 10, 20, 30 itd.). Zawartość tablicy wyświetl na ekranie. Ćwiczenie 11.3. Napisz program, w którym zostanie utworzona 20-elementowa tablica typu boolean. Komórkom o indeksie parzystym przypisz wartość true, a o indeksie nieparzystym — false (zero możesz uznać za wartość parzystą). Zawartość tablicy wyświetl na ekranie. Ćwiczenie 11.4. Spróbuj rozwiązać ćwiczenie 11.3 w taki sposób, aby zapisywanie danych w tablicy odbywało się za pomocą jednej instrukcji przypisania (korzystającej z operatora =). Ćwiczenie 11.5. Napisz program, w którym zostanie utworzona 100-elementowa tablica liczb typu int. Komórkom o indeksach 0, 10, 20, … , 90 przypisz wartość 0, komórkom 1, 11, 21, … , 91 wartość 1, komórkom 2, 12, 22, … , 92 wartość 2 itd. Ćwiczenie 11.6. Utwórz 26-elementową tablicę typu char. Zapisz w kolejnych komórkach małe litery alfabetu od a do z. Ćwiczenie 11.7. Zmodyfikuj program z listingu 2.41 w taki sposób, aby do wypełnienia tablicy danymi została wykorzystana pętla typu while. Lekcja 12. Tablice wielowymiarowe Tablice jednowymiarowe prezentowane w lekcji 11. to nie jedyny rodzaj tablic, jakie można tworzyć w Javie. Istnieją również tablice wielowymiarowe, którymi zajmiemy się właśnie w tej lekcji. Przedstawiony zostanie sposób ich deklarowania i tworzenia oraz odwoływania się do poszczególnych elementów. Omówione zostaną przy tym zarówno tablice o regularnym, jak i nieregularnym układzie komórek. Okaże się też, jak ważna w przypadku tego rodzaju struktur jest znana nam z poprzedniej lekcji właściwość length. 80 Java. Praktyczny kurs Tablice dwuwymiarowe W lekcji 11. zostały przedstawione podstawowe tablice jednowymiarowe, to znaczy takie, które są wektorami elementów, czyli strukturami zaprezentowanymi na rysunku 2.32. Tablice nie muszą być jednak jednowymiarowe, wymiarów może być więcej, np. dwa. Wtedy taka struktura wygląda tak jak na rysunku 2.35. Widać wyraźnie, że do wyznaczenia konkretnej komórki trzeba tym razem podać dwie liczby określające rząd oraz kolumnę. Rysunek 2.35. Przykład tablicy dwuwymiarowej Taką tablicę trzeba zadeklarować oraz utworzyć. Odbywa się to tak samo jak w przypadku tablic jednowymiarowych. Jeśli schematyczna deklaracja tablicy jednowymiarowej wyglądała: typ nazwa_tablicy[] to w przypadku tablicy dwuwymiarowej będzie ona następująca: typ nazwa_tablicy[][] Jak widać, dodaje się po prostu kolejny nawias kwadratowy15. Kiedy mamy już zmienną tablicową, możemy utworzyć i jednocześnie zainicjować samą tablicę. Tu również konstrukcja jest analogiczna do konstrukcji tablic jednowymiarowych: typ_tablicy nazwa_tablicy[][] = {{wartość1, wartość2, ... , wartośćN}, {wartość1, wartość2, ... , wartośćN }} Ten zapis można rozbić na kilka wierszy w celu zwiększenia czytelności, np.: typ_tablicy nazwa_tablicy[][] = { {wartość1, wartość2, ... , wartośćN}, {wartość1, wartość2, ... , wartośćN} } Aby zatem utworzyć tablicę taką jak na rysunku 2.35, wypełnić ją kolejnymi liczbami całkowitymi i wyświetlić jej zawartość na ekranie, należy wykorzystać program widoczny na listingu 2.42. Tablica ta będzie wtedy wyglądać tak jak na rysunku 2.36. 15 Możliwa jest oczywiście również deklaracja w postaci typ[][] nazwa_tablicy. Rozdział 2. Instrukcje języka 81 Rysunek 2.36. Schematyczna postać tablicy dwuwymiarowej po wypełnieniu danymi Listing 2.42. class Main { public static void main (String args[]) { int tab[][] = { {1, 2, 3, 4}, {5, 6, 7, 8} }; for(int i = 0; i < 2; i++){ for(int j = 0; j < 4; j++){ System.out.print("tab[" + i + "," + j + "] = "); System.out.println(tab[i][j]); } } } } Do wyświetlenia zawartości tablicy na ekranie zostały użyte dwie pętle typu for. Pętla zewnętrzna ze zmienną iteracyjną i przebiega kolejne wiersze tablicy, a wewnętrzna ze zmienną iteracyjną j — kolejne komórki w danym wierszu. Aby odczytać zawartość konkretnej komórki, stosuje się odwołanie: tab[i][j] Wynik działania programu z listingu 2.42 jest widoczny na rysunku 2.37. Rysunek 2.37. Wynik działania programu wypełniającego tablicę dwuwymiarową Drugi sposób utworzenia tablicy dwuwymiarowej to wykorzystanie operatora new przedstawionego w lekcji 11. Tym razem będzie on miał następującą postać: 82 Java. Praktyczny kurs new typ_tablicy[liczba_elementów][liczba_elementów]; Można jednocześnie zadeklarować i utworzyć tablicę, korzystając z konstrukcji: typ_tablicy nazwa_tablicy[][] = new typ_tablicy[liczba_elementów][liczba_elementów]; lub też rozbić te czynności na dwie instrukcje. Schemat postępowania wygląda wtedy następująco: typ_tablicy nazwa_tablicy[][]; /*tutaj mogą się znaleźć inne instrukcje*/ nazwa_tablicy = new typ_tablicy[liczba_elementów][liczba_elementów]; Jak widać, oba sposoby są analogiczne do metod tworzenia tablic jednowymiarowych. Oczywiście również nawiasy kwadratowe mogą znaleźć się przed nazwą, a za typem tablicy: typ_tablicy[][] nazwa_tablicy Jeśli zatem ma powstać tablica liczb typu int, o dwóch wierszach i czterech komórkach w każdym z nich (czyli dokładnie taka jak w przykładzie z listingu 2.42), należy zastosować instrukcję: int tab[][] = new int[2][4]; Taką tablicę można wypełnić danymi przy użyciu zagnieżdżonych pętli for, tak jak zostało to zaprezentowane na listingu 2.43. Listing 2.43. class Main { public static void main (String args[]) { int tab[][] = new int[2][4]; int licznik = 1; for(int i = 0; i < 2; i++){ for(int j = 0; j < 4; j++){ tab[i][j] = licznik++; } } for(int i = 0; i < 2; i++){ for(int j = 0; j < 4; j++){ System.out.print("tab[" + i + "," + j + "] = "); System.out.println(tab[i][j]); } } } } Za wypełnienie tablicy danymi odpowiadają dwie pierwsze zagnieżdżone pętle for. Kolejnym komórkom jest przypisywany stan zmiennej licznik. Zmienna ta ma wartość początkową równą 1 i w każdej iteracji jej wartość jest zwiększana o 1. Stosuje się w tym celu operator ++ (lekcja 6.). Po wypełnieniu danymi zawartość tablicy jest wyświetlana na ekranie za pomocą dwóch kolejnych zagnieżdżonych pętli for, dokładnie tak jak w przypadku programu z listingu 2.42. Rozdział 2. Instrukcje języka 83 Właściwość length Zaprezentowany na listingu 2.43 sposób wypełniania tablicy (oraz wyświetlania jej zawartości na ekranie) ma jeden mankament. W przypadku obu pętli zakładamy, że rozmiar tablicy jest znany. Jak sobie poradzić w sytuacji, kiedy rozmiar pozostaje nieznany? W przypadku tablic jednowymiarowych mamy do dyspozycji właściwość length. Czy istnieje ona również w tablicach wielowymiarowych? Jeśli tak, to jakie ma znaczenie? Odpowiedź na pierwsze pytanie brzmi: oczywiście tak, tablica wielowymiarowa również ma właściwość length. Aby sprawnie się nią posługiwać, trzeba jednak dobrze zrozumieć, czym tak naprawdę jest tablica wielowymiarowa. Można to odkryć po dokładnym przyjrzeniu się jej deklaracji. Da się zauważyć, że jeśli struktura: int[] oznacza tablicę liczb typu int, to: int[][] oznacza tablicę tablic typu int, innymi słowy: tablicę składającą się z innych tablic typu int. Zatem pełna deklaracja: int[][] tablica = new int[n][m] określa w rzeczywistości n-elementową tablicę, której elementami są m-elementowe tablice liczb typu int. Odwołanie: tablica.length da więc w wyniku liczbę tablic m-elementowych (czyli n), natomiast wywołanie: tablica[indeks].length da wielkość tablicy znajdującej się pod indeksem indeks (w każdym przypadku będzie to rzecz jasna m). Prześledźmy to na przykładzie konkretnej deklaracji: int[][] tablica = new int[2][4] W tym przypadku zachodzą następujące zależności: tablica.length = 2; tablica[0].length = 4; tablica[1].length = 4. Budowę tej tablicy zobrazowano na rysunku 2.38. Aby lepiej zrozumieć te zależności, wykonajmy jeszcze pełny praktyczny przykład. Zmodyfikujemy kod z listingu 2.43, tak aby pętle zajmujące się zarówno wypełnianiem tablicy, jak i wyświetlaniem jej zawartości na ekranie nie były zależne od jej rozmiaru i pracowały, nawet jeśli wielkość tablicy ulegnie modyfikacji. Zadanie to można zrealizować w sposób przedstawiony na listingu 2.44. 84 Java. Praktyczny kurs Rysunek 2.38. Budowa dwuwymiarowej tablicy o postaci int[2][4] Listing 2.44. class Main { public static void main (String args[]) { int tab[][] = new int[2][4]; int licznik = 1; for(int i = 0; i < tab.length; i++){ for(int j = 0; j < tab[i].length; j++){ tab[i][j] = licznik++; } } for(int i = 0; i < tab.length; i++){ for(int j = 0; j < tab[i].length; j++){ System.out.print("tab[" + i + "," + j + "] = "); System.out.println(tab[i][j]); } } } } Tablice nieregularne Tablice wielowymiarowe wcale nie muszą mieć regularnie prostokątnych kształtów, tak jak prezentowane wcześniej w tym punkcie. Prostokątnych, to znaczy takich, w których w każdym wierszu znajduje się taka sama liczba komórek (tak jak zaprezentowano to na rysunkach 2.35 i 2.36). Nic nie stoi na przeszkodzie, aby stworzyć strukturę trójkątną (rysunek 2.39a) lub też całkiem nieregularną (rysunek 2.39b). Przy tworzeniu takich struktur czeka nas jednak więcej pracy niż w przypadku tablic regularnych, gdyż prawdopodobnie każdy wiersz trzeba będzie tworzyć oddzielnie16. Jak tworzyć tego typu struktury? Wiadomo już, że tablice wielowymiarowe to tak naprawdę tablice tablic jednowymiarowych (tablica dwuwymiarowa to tablica jednowymiarowa zawierająca szereg tablic jednowymiarowych, tablica trójwymiarowa to tablica 16 Zależy to od kształtu tablicy, który chce się uzyskać. Rozdział 2. Instrukcje języka 85 Rysunek 2.39. Przykłady nieregularnych tablic wielowymiarowych jednowymiarowa zawierająca w sobie tablice dwuwymiarowe itd.). Spróbujmy zatem stworzyć strukturę widoczną na rysunku 2.39b. Zaczniemy od samej deklaracji — nie przysporzy ona z pewnością żadnego kłopotu, była prezentowana już kilkakrotnie: int[][] tab Ta deklaracja tworzy zmienną tablicową o nazwie tab, której można przypisywać tablice dwuwymiarowe przechowujące liczby typu int. Skoro struktura ma wyglądać jak na rysunku 2.39b, należy teraz utworzyć 4-elementową tablicę, która będzie mogła przechowywać tablice jednowymiarowe liczb typu int. Trzeba więc użyć operatora new w postaci: new int[4][] Deklaracja i jednoczesna inicjalizacja zmiennej tab będzie wyglądać następująco: int[][] tab = new int[4][]; Teraz kolejnym elementom: tab[0], tab[1], tab[2] i tab[3], trzeba przypisać nowo utworzone tablice jednowymiarowe liczb typu int, tak aby w komórce tab[0] znalazła się tablica 4-elementowa, w tab[1] — tablica 2-elementowa, w tab[2] — tablica 1-elementowa, w tab[3] — tablica 3-elementowa (rysunek 2.40). Należy zatem wykonać ciąg instrukcji: tab[0] tab[1] tab[2] tab[3] = = = = new new new new int[4]; int[2]; int[1]; int[3]; To wszystko — tablica jest gotowa. Można zacząć wypełniać ją danymi. Załóżmy, że kolejne komórki mają zawierać liczby od 1 do 10, to znaczy w pierwszym wierszu tablicy znajdą się liczby 1, 2, 3, 4, w drugim — 5, 6, w trzecim — 7, a w czwartym — 8, 9, 10, tak jak zostało to zobrazowane na rysunku 2.41. Zatem do wypełnienia tablicy można użyć zagnieżdżonej pętli for, analogicznie do przypadku tablicy regularnej w przykładzie z listingu 2.44. Podobnie zagnieżdżonych pętli for, choć w nieco zmienionej postaci, da się użyć do wyświetlenia zawartości tablicy na ekranie. Pełny kod programu realizującego postawione zadania jest widoczny na listingu 2.45. 86 Java. Praktyczny kurs Rysunek 2.40. Tworzenie nieregularnej tablicy dwuwymiarowej Rysunek 2.41. Nieregularna tablica wypełniona danymi Listing 2.45. class Main { public static void main (String args[]) { int[][] tab = new int[4][]; tab[0] = new int[4]; tab[1] = new int[2]; tab[2] = new int[1]; tab[3] = new int[3]; int licznik = 1; for(int i = 0; i < tab.length; i++){ for(int j = 0; j < tab[i].length; j++){ tab[i][j] = licznik++; } } Rozdział 2. Instrukcje języka 87 for(int i = 0; i < tab.length; i++){ System.out.print("tab[" + i + "] = "); for(int j = 0; j < tab[i].length; j++){ System.out.print(tab[i][j] + " "); } System.out.println(""); } } } W pierwszej części kodu powstała dwuwymiarowa tablica o strukturze takiej jak na rysunkach 2.39b i 2.41 (wszystkie użyte konstrukcje zostały wyjaśnione w poprzednich akapitach). Jest ona wypełniana danymi tak, aby uzyskać w poszczególnych komórkach wartości widoczne na rysunku 2.41. W tym celu użyte zostały zagnieżdżone pętle for i zmienna licznik. To taki sam sposób jak zastosowany w programie z listingu 2.44. Pętla zewnętrzna, ze zmienną iteracyjną i, odpowiada za przebieg po kolejnych wierszach tablicy, a wewnętrzna, ze zmienną iteracyjną j, za przebieg po kolejnych komórkach w każdym wierszu. Do wyświetlenia danych również są używane dwie zagnieżdżone pętle for, ale inaczej niż w przykładzie z listingu 2.44, dane dotyczące jednego wiersza tablicy zostają wyświetlone w jednej linii ekranu, co tworzy obraz widoczny na rysunku 2.42. Jest to możliwe dzięki użyciu instrukcji System.out.print zamiast System.out.println. W pętli zewnętrznej jest umieszczona instrukcja System.out.print("tab[" + i + "] = ") wyświetlająca numer aktualnie przetwarzanego wiersza tabeli, natomiast w pętli wewnętrznej instrukcja System.out.print(tab[i][j] + " ") wyświetlająca zawartość komórek w danym wierszu. Rysunek 2.42. Wyświetlenie danych z tablicy nieregularnej Spróbujmy teraz utworzyć tablicę, której struktura została przedstawiona na rysunku 2.39A. Po wyjaśnieniach z ostatniego przykładu nie powinno to przysporzyć najmniejszych problemów. Wypełnijmy ją danymi tak samo jak w poprzednim przypadku — umieszczając w komórkach kolejne wartości od 1 do 10. Deklaracja i inicjalizacja będą wyglądać następująco: int[][] tab = new int[4][]; Kolejne wiersze tablicy można utworzyć za pomocą serii instrukcji: tab[0] tab[1] tab[2] tab[3] = = = = new new new new int[4]; int[3]; int[2]; int[1]; 88 Java. Praktyczny kurs Wypełnienie takiej tablicy danymi (zgodnie z podanymi zasadami) oraz wyświetlenie tych danych na ekranie będzie się odbywać podobnie jak w przypadku kodu z listingu 2.45. Zatem pełny program będzie wyglądał tak, jak przedstawiono na listingu 2.46. Listing 2.46. class Main { public static void main (String args[]) { int[][] tab = new int[4][]; tab[0] = new int[4]; tab[1] = new int[3]; tab[2] = new int[2]; tab[3] = new int[1]; int licznik = 1; for(int i = 0; i < tab.length; i++){ for(int j = 0; j < tab[i].length; j++){ tab[i][j] = licznik++; } } for(int i = 0; i < tab.length; i++){ System.out.print("tab[" + i + "] = "); for(int j = 0; j < tab[i].length; j++){ System.out.print(tab[i][j] + " "); } System.out.println(""); } } } Warto w tym miejscu zwrócić uwagę na jedną rzecz: tablica taka jak na rysunku 2.39a, mimo że nazwana nieregularną, powinna być raczej nazywana nieprostokątną, gdyż w rzeczywistości jej trójkątny kształt można traktować jako regularny. Jeśli tak, nie trzeba jej tworzyć ręcznie za pomocą serii (w naszym wypadku czterech) instrukcji. Zamiast pisać: tab[0] tab[1] tab[2] tab[3] = = = = new new new new int[4]; int[3]; int[2]; int[1]; można również wykorzystać odpowiednio skonstruowaną pętlę typu for. Skoro każdy kolejny wiersz ma o jedną komórkę mniej, powinna być to pętla zliczająca od 4 do 1, czyli powinna wyglądać następująco: for(int i = 0; i < 4; i++){ tab[i] = new int[4 - i]; } Zmienna i zmienia się tu w zakresie 0 – 3, zatem tab[i] przyjmuje kolejne wartości tab[0], tab[1], tab[2], tab[3], natomiast wyrażenie new int[4 – i] wartości: new int[4], new int[2], new int[3], new int[1]. Tym samym otrzymamy dokładny odpowiednik czterech ręcznie napisanych instrukcji. Rozdział 2. Instrukcje języka 89 Ćwiczenia do samodzielnego wykonania Ćwiczenie 12.1. Zmodyfikuj kod z listingu 2.45 tak, aby w kolejnych komórkach tablicy znalazły się liczby malejące od 10 do 1. Ćwiczenie 12.2. Zmodyfikuj program z listingu 2.46 tak, aby do wypełnienia tablicy danymi były wykorzystywane pętle typu while. Ćwiczenie 12.3. Utwórz tablicę liczb typu int zaprezentowaną na rysunku 2.43. Wypełnij kolejne komórki wartościami malejącymi od 10 do 1. Do utworzenia tablicy i wypełnienia jej danymi wykorzystaj pętle typu for. Rysunek 2.43. Odwrócona tablica trójkątna (ilustracja do ćwiczenia 12.3) Ćwiczenie 12.4. Utwórz trójwymiarową tablicę dla wartości typu int (będzie to struktura, którą można sobie wyobrazić jako prostopadłościan składający się z sześcianów; każdy sześcian będzie pojedynczą komórką). Powinna umożliwiać przechowywanie trzydziestu wartości. Poszczególne komórki wypełnij liczbami od 30 do 59. Zawartość wyświetl na ekranie. Zastanów się, ile wersji tego typu tablicy można utworzyć. Ćwiczenie 12.5. Utwórz tablicę dwuwymiarową, w której liczba komórek w kolejnych rzędach będzie równa dziesięciu kolejnym wartościom ciągu Fibonacciego, poczynając od elementu o wartości 1 (1, 1, 2, 3, 5 itd.). Wartość każdej komórki powinna być jej numerem w danym wierszu w kolejności malejącej (czyli dla wiersza o długości trzech komórek kolejne wartości to 3, 2, 1). Zwartość tablicy wyświetl na ekranie. 90 Java. Praktyczny kurs Ćwiczenie 12.6. Zmodyfikuj kod z ćwiczenia 12.5 w taki sposób, aby w kolejnych rzędach znajdowała się liczba komórek wynikająca z kolejnych wartości ciągu Fibonacciego, ale tylko tych nieparzystych (czyli 1, 1, 3, 5, 13, 21 itd.). Rozdział 3. Programowanie obiektowe. Część I Każdy program w Javie składa się z jednej lub wielu klas. W przykładach przedstawionych dotychczas była to tylko jedna klasa o nazwie Main. Przypomnijmy sobie nasz pierwszy program wyświetlający na ekranie napis. Jego kod wyglądał następująco: class Main { public static void main(String args[]){ System.out.println("To jest napis"); } } Założyliśmy wtedy, że szkielet kolejnych aplikacji ilustrujących różne struktury języka programowania ma właśnie tak wyglądać. Teraz nadszedł czas, aby wyjaśnić, dlaczego właśnie tak. Wszystko stanie się jasne po przeczytaniu tego rozdziału. Podstawy Pierwsza część rozdziału 3. składa się z trzech lekcji, w których są omówione podstawy programowania obiektowego w Javie. Lekcja 13. opisuje budowę klas oraz tworzenie obiektów, ponadto zostały w niej przedstawione pola i metody, sposoby ich deklaracji oraz wywoływania. Lekcja 14. jest poświęcona argumentom metod oraz technice przeciążania metod, została w niej również przybliżona wykorzystywana już wcześniej metoda main. W ostatniej lekcji, 15., został zaprezentowany temat konstruktorów, czyli specjalnych metod wywoływanych podczas tworzenia obiektów. Lekcja 13. Klasy, pola i metody Lekcja 13. rozpoczyna rozdział przedstawiający podstawy programowania obiektowego w Javie. Podstawowe pojęcia z tego zakresu pojawiały się już w prezentowanym wcześniej materiale, teraz zostaną jednak wyjaśnione dokładniej na praktycznych przykładach. 92 Java. Praktyczny kurs Zajmiemy się tworzeniem klas, ich strukturą i deklaracjami, omówiony zostanie też związek między klasą i obiektem. W tej lekcji zostaną również przedstawione składowe klasy, czyli pola i metody, nie pominiemy także kwestii domyślnych wartości pól. Okaże się też, jakie relacje występują między zadeklarowaną na stosie zmienną obiektową (inaczej referencyjną, odnośnikową) a utworzonym na stercie obiektem. Pierwsza klasa Jak wiadomo z rozdziału 1., klasy są opisami obiektów, czyli bytów programistycznych, które mogą przechowywać dane oraz wykonywać polecone przez programistę zadania. Każdy obiekt jest instancją, czyli wystąpieniem jakiejś klasy. W związku z tym klasa określa także typ danego obiektu. Przypomnijmy, że typ określa rodzaj wartości, jakie może przyjmować dany byt programistyczny, na przykład zmienna. Jeśli np. mamy zmienną typu int, to może ona przyjmować jedynie wartości typu int, czyli liczby z zakresu od –2 147 483 648 do 2 147 483 647. Z obiektami jest podobnie: zmienna obiektowa hipotetycznej klasy Punkt może przechowywać obiekty klasy (typu) Punkt1. Klasa to zatem nic innego jak definicja nowego typu danych. Ponieważ dla osób nieobeznanych z programowaniem obiektowym to wszystko może brzmieć zawile, zobaczmy od razu, jak to wygląda w praktyce. Załóżmy, że pisany przez nas program wymaga przechowywania danych odnośnie do punktów na płaszczyźnie. Każdy taki punkt jest charakteryzowany przez dwie wartości: współrzędną x oraz współrzędną y. Stwórzmy więc klasę opisującą obiekty tego typu. Schematyczny szkielet takiej klasy wygląda następująco: class nazwa_klasy { //treść klasy } W treści klasy definiuje się pola i metody. Pola służą do przechowywania danych, metody do wykonywania różnych operacji (oba te elementy są nazywane składowymi klasy). W przypadku klasy, która ma przechowywać dane dotyczące współrzędnych x i y, wystarczą więc dwa pola typu int2. Pozostaje jeszcze wybór nazwy dla takiej klasy. Występują tu takie same ograniczenia jak w przypadku nazewnictwa zmiennych (lekcja 4.), czyli nazwa klasy może składać się jedynie z liter (zarówno małych, jak i dużych), cyfr oraz znaku podkreślenia, ale nie może zaczynać się od cyfry. Nie zaleca się również stosowania w nazwach klas polskich znaków diakrytycznych, szczególnie że nazwa klasy musi3 być zgodna z nazwą pliku, w którym dana klasa została zapisana (lekcja 1.). Utworzoną klasę nazwiemy zatem, jakżeby inaczej, Punkt i będzie ona miała postać widoczną na listingu 3.1. Kod ten należy zapisać w pliku o nazwie Punkt.java. 1 Jak zobaczymy w dalszej części książki, takiej zmiennej można również przypisać obiekty klas potomnych lub nadrzędnych w stosunku do klasy Punkt. 2 Załóżmy, że chodzi np. o punkty ekranowe (piksele) i wystarczy, jeśli będą mogły przyjmować tylko współrzędne całkowite. 3 Jak zostanie to wyjaśnione w dalszej części książki, to stwierdzenie dotyczy tylko klas publicznych. Rozdział 3. Programowanie obiektowe. Część I 93 Tu ponownie trzeba zwrócić uwagę na to, że każdą klasę zapisuje się w oddzielnym pliku o nazwie zgodnej z jej nazwą oraz rozszerzeniu java. A zatem klasę Punkt zapisuje się w pliku Punkt.java, klasę Main w pliku Main.java, a klasę Cos w pliku Cos.java. W dalszej części książki na jednym listingu będzie znajdowało się nawet kilka klas, ale należy je również zapisywać w oddzielnych plikach w tym samym katalogu, chyba że zostanie wyraźnie napisane, by zrobić inaczej. Dokładne wyjaśnienie tej kwestii znajdzie się w lekcji 17. Listing 3.1. class Punkt { int x; int y; } Jak widać, klasa zawiera tylko dwa pola o nazwach x i y, które za pomocą wartości całkowitych opisują współrzędne położenia punktu. Po zdefiniowaniu klasy Punkt można zadeklarować zmienną typu Punkt. Robi się to w dokładnie taki sam sposób, w jaki deklarowane były zmienne typów prostych (np. short, int, char), pisząc: typ_zmiennej nazwa_zmiennej; Ponieważ typem zmiennej jest nazwa klasy, deklaracja ta przyjmie postać: Punkt przykladowyPunkt; W ten sposób powstała zmienna odnośnikowa (referencyjna, obiektowa), która domyślnie jest pusta, tzn. nie zawiera żadnych danych. Dokładniej rzecz ujmując, po deklaracji zmienna taka zawiera wartość specjalną null, która określa, że zmienna nie zawiera odniesienia do żadnego obiektu. Trzeba więc samodzielnie utworzyć obiekt klasy Punkt i powiązać go z tą zmienną4. Ta pierwsza czynność wykonywana jest za pomocą operatora new w postaci: new nazwa_klasy(); Zatem cała konstrukcja schematycznie będzie wyglądać następująco: nazwa_klasy nazwa_zmiennej = new nazwa_klasy(): w przypadku klasy Punkt: Punkt przykladowyPunkt = new Punkt(); Rzecz jasna, podobnie jak w przypadku zmiennych typów prostych (lekcja 4.), również tutaj można oddzielić deklarację zmiennej od jej inicjalizacji, w związku z czym równie poprawna jest konstrukcja w postaci: Punkt przykladowyPunkt; przykladowyPunkt = new Punkt(); 4 Osoby programujące w C++ powinny zwrócić na to uwagę, gdyż w tym języku już sama deklaracja zmiennej typu klasowego powoduje wywołanie domyślnego konstruktora i utworzenie obiektu. 94 Java. Praktyczny kurs Należy sobie uzmysłowić, że po wykonaniu tych instrukcji w pamięci powstają dwie różne struktury. Pierwszą z nich jest powstała na tak zwanym stosie (ang. stack) zmienna referencyjna przykladowyPunkt, drugą jest powstały na tak zwanej stercie (ang. heap) obiekt klasy (typu) Punkt. Zmienna przykladowyPunkt zawiera odniesienie do przypisanego jej obiektu klasy Punkt i tylko poprzez nią można się do tego obiektu odwoływać. Zostało to schematycznie zobrazowane na rysunku 3.1. Rysunek 3.1. Zależność między zmienną odnośnikową a wskazywanym przez nią obiektem Aby odwołać się do danego pola klasy, należy skorzystać z operatora . (kropka), czyli użyć konstrukcji: nazwa_zmiennej_obiektowej.nazwa_pola_obiektu Przypisanie wartości 100 polu x obiektu klasy Punkt reprezentowanego przez zmienną przykladowyPunkt będzie zatem wyglądało następująco: przykladowyPunkt.x = 100; Napiszmy przykładowy program wykorzystujący klasy Main i Punkt, w którym utworzymy zmienną odnośnikową klasy Punkt oraz obiekt klasy Punkt, przypiszemy polom x i y tego obiektu wartości 100 oraz wyświetlimy te wartości na ekranie. Klasa Punkt pozostanie bez zmian, tak jak na listingu 3.1, natomiast Main będzie miała postać przedstawioną na listingu 3.2. Listing 3.2. class Main { public static void main (String args[]) { Punkt punkt = new Punkt(); punkt.x = 100; punkt.y = 100; System.out.println("punkt.x = " + punkt.x); System.out.println("punkt.y = " + punkt.y); } } Na początku deklarujemy zmienną klasy Punkt o nazwie punkt (zauważmy, że jest to możliwe dzięki temu, że w Javie małe i duże litery są rozróżniane) i przypisujemy jej nowo utworzony obiekt tej klasy. Jest to zatem jednoczesna deklaracja i inicjalizacja. Rozdział 3. Programowanie obiektowe. Część I 95 Od tej chwili zmienna punkt wskazuje na obiekt klasy Punkt, można się zatem posługiwać nią tak jak samym obiektem. Pisząc punkt.x = 100, przypisujemy wartość 100 polu x, a pisząc punkt.y = 100, przypisujemy wartość 100 polu y. W ostatnich dwóch liniach korzystamy z instrukcji System.out.println, aby wyświetlić wartość obu pól na ekranie. Wystarczy teraz skompilować i uruchomić program. Efekt jest widoczny na rysunku 3.2. Rysunek 3.2. Wynik działania klasy Main z listingu 3.2 Nie trzeba oddzielnie kompilować klas Main i Punkt. Ponieważ w klasie Main jest wykorzystywana klasa Punkt, wydanie polecenia javac Main.java spowoduje kompilację zarówno pliku Main.java, jak i Punkt.java. Obie klasy muszą natomiast znajdować się w tym samym katalogu. Metody klas Klasy oprócz pól przechowujących dane zawierają także metody, które wykonują zapisane przez programistę operacje. Definiuje się je w ciele (czyli wewnątrz) klasy pomiędzy znakami nawiasu klamrowego. Każda metoda może przyjmować argumenty (nazywane także parametrami; oba terminy często stosowane są zamiennie) oraz zwracać wynik. Schematyczna deklaracja metody wygląda następująco: typ_wyniku nazwa_metody(argumenty_metody) { //instrukcje metody } Po umieszczeniu w ciele klasy ta deklaracja będzie natomiast wyglądała tak: class nazwa_klasy { typ_wyniku nazwa_metody(argumenty_metody) { //instrukcje metody } } Jeśli metoda nie zwraca żadnego wyniku, jako typ wyniku należy zastosować słowo void, jeśli natomiast nie przyjmuje żadnych argumentów, pomiędzy znakami nawiasu okrągłego nie należy nic wpisywać. Aby zobaczyć, jak to wygląda w praktyce, dodajmy do klasy Punkt prostą metodę, której zadaniem będzie wyświetlenie wartości współrzędnych x i y na ekranie. Nadamy jej nazwę wyswietlWspolrzedne. Powinna zatem wyglądać następująco: void wyswietlWspolrzedne() { System.out.println("współrzędna x = " + x); System.out.println("współrzędna y = " + y); } 96 Java. Praktyczny kurs Słowo void oznacza, że metoda nie zwraca żadnego wyniku, a brak argumentów pomiędzy znakami nawiasu okrągłego mówi, że metoda ta nie przyjmuje żadnych argumentów. Wewnątrz metody znajdują się dwie dobrze nam znane instrukcje, które wyświetlają na ekranie współrzędne punktu. Jeśli umieścimy powyższy kod wewnątrz klasy Punkt, będzie ona miała postać widoczną na listingu 3.3. Listing 3.3. class Punkt { int x; int y; void wyswietlWspolrzedne() { System.out.println("współrzędna x = " + x); System.out.println("współrzędna y = " + y); } } Po utworzeniu obiektu danej klasy można wywołać metodę (czyli wykonać zawarte w niej instrukcje) w sposób identyczny z tym, w jaki odwołujemy się do pól klasy, tzn. korzystając z operatora . (kropka). Jeśli zatem przykładowa zmienna punkt zawiera referencję do obiektu klasy Punkt, prawidłowym wywołaniem metody wyswietlWspolrzedne będzie: punkt.wyswietlWspolrzedne(); Ogólnie wywołanie metody wygląda następująco: nazwa_zmiennej.nazwa_metody(argumenty_metody); Oczywiście jeśli dana metoda nie ma argumentów, po prostu się je pomija. Wykorzystajmy teraz klasę Main do przetestowania nowej konstrukcji. Zmodyfikujemy program z listingu 3.2 tak, aby korzystał z metody wyswietlWspolrzedne. Odpowiedni kod jest zaprezentowany na listingu 3.4. Wynik jego działania łatwo przewidzieć (rysunek 3.3). Listing 3.4. class Main { public static void main (String args[]) { Punkt punkt = new Punkt(); punkt.x = 100; punkt.y = 100; punkt.wyswietlWspolrzedne(); } } Rysunek 3.3. Wynik działania metody wyswietlWspolrzedne klasy Punkt Rozdział 3. Programowanie obiektowe. Część I 97 Zobaczmy teraz, w jaki sposób napisać metody, które będą potrafiły zwracać wyniki. Typ wyniku należy podać przed nazwą metody, zatem jeśli ma ona zwracać liczbę typu int, deklaracja powinna wyglądać następująco: int nazwa_metody() { //instrukcje metody } Sam wynik zwraca się natomiast przez zastosowanie instrukcji return. Najlepiej zobaczyć to na praktycznym przykładzie. Do klasy Punkt dodamy zatem dwie metody — jedna będzie podawała wartość współrzędnej x, druga y. Nazwiemy je odpowiednio pobierzX i pobierzY. Wygląd metody pobierzX będzie następujący: int pobierzX() { return x; } Przed nazwą metody znajduje się określenie typu zwracanego przez nią wyniku — skoro jest to int, oznacza to, że metoda ta musi zwrócić jako wynik liczbę całkowitą z przedziału określonego przez typ int (tabela 1.1 z rozdziału 1.). Wynik jest zwracany dzięki instrukcji return. Zapis return x oznacza zwrócenie przez metodę wartości zapisanej w polu x. Jak łatwo się domyślić, metoda pobierzY będzie wyglądała analogicznie, z tym że będzie w niej zwracana wartość zapisana w polu y. Pełny kod klasy Punkt po dodaniu tych dwóch metod będzie wyglądał tak, jak to zostało przedstawione na listingu 3.5. Na listingu 3.6 widać z kolei kod przykładowej klasy Main wykorzystującej nowe metody klasy Punkt. Listing 3.5. class Punkt { int x; int y; int pobierzX() { return x; } int pobierzY() { return y; } void wyswietlWspolrzedne() { System.out.println("współrzędna x = " + x); System.out.println("współrzędna y = " + y); } } Listing 3.6. class Main { public static void main (String args[]) { Punkt punkt = new Punkt(); punkt.x = 100; punkt.y = 100; 98 Java. Praktyczny kurs System.out.println("współrzędna x = " + punkt.pobierzX()); System.out.println("współrzędna y = " + punkt.pobierzY()); } } To, że metoda zwraca jakąś wartość, jest równoznaczne z tym, że ta wartość jest podstawiana w miejscu wywołania metody. A zatem w tym przypadku w miejscu wystąpienia wywołania: punkt.pobierzX() zostanie podstawiona wartość 100 (pole x ma wartość 100). Tak samo będzie też przy wywołaniu: punkt.pobierzY() (pole y również ma wartość 100). Wartości domyślne pól Na listingu 3.6 znajduje się klasa Main, w której tworzony jest obiekt klasy Punkt. Obu polom tego obiektu zostaje przypisana wartość 100. Pytanie, które warto sobie zadać, brzmi: jakie wartości miały te pola po utworzeniu obiektu, ale przed przypisaniem do nich wartości? Otóż w Javie wartość, jaką przyjmuje niezainicjowane pole obiektu, jest ściśle określona, zatem prawidłowa odpowiedź na to pytanie to: oba pola przyjmą wartość 0. Można się o tym przekonać, kompilując i uruchamiając program przedstawiony na listingu 3.7. Wynik jego działania jest widoczny na rysunku 3.4. Wartości domyślne dla typów danych występujących w Javie zostały z kolei zaprezentowane w tabeli 3.1. Listing 3.7. class Main { public static void main (String args[]) { Punkt punkt = new Punkt(); System.out.println("punkt.x = " + punkt.x); System.out.println("punkt.y = " + punkt.y); } } Rysunek 3.4. Wartości domyślne niezainicjowanych pól klasy Punkt Rozdział 3. Programowanie obiektowe. Część I 99 Tabela 3.1. Domyślne wartości dla poszczególnych typów danych Typ Wartość domyślna byte 0 short 0 int 0 long 0 float 0.0 double 0.0 char \0 boolean false odnośnikowy null Ćwiczenia do samodzielnego wykonania Ćwiczenie 13.1. Napisz przykładową klasę LiczbaCalkowita, która będzie przechowywała wartość całkowitą. Klasa ta powinna zawierać metodę wyswietlLiczbe, która będzie wyświetlała na ekranie przechowywaną wartość, oraz metodę pobierzLiczbę zwracającą przechowywaną wartość. Ćwiczenie 13.2. Napisz kod przykładowej klasy Prostokat zawierającej cztery pola przechowujące współrzędne czterech rogów prostokąta. Wykorzystaj obiekty klasy Punkt. Ćwiczenie 13.3. Do utworzonej w ćwiczeniu 13.2 klasy Prostokat dopisz metody zwracające wartości poszczególnych współrzędnych oraz metodę wyświetlającą wszystkie współrzędne. Ćwiczenie 13.4. Napisz przykładową klasę Main testującą zachowanie klas LiczbaCalkowita (z ćwiczenia 13.1) oraz Prostokat (z ćwiczenia 13.3). Ćwiczenie 13.5. Napisz klasę Prostokat przechowującą jedynie współrzędne lewego górnego i prawego dolnego rogu (wystarczają one do jednoznacznego wyznaczenia prostokąta na płaszczyźnie). Dodaj metody podające współrzędne każdego rogu. 100 Java. Praktyczny kurs Ćwiczenie 13.6. Do klasy Prostokat z ćwiczenia 13.2 lub 13.3 dopisz metodę sprawdzającą, czy wprowadzone współrzędne rzeczywiście definiują prostokąt (cztery punkty na płaszczyźnie dają dowolny czworokąt, który nie musi być prostokątem). Lekcja 14. Argumenty i przeciążanie metod O tym, że metody mogą mieć argumenty, wiadomo z lekcji 13. Czas się dowiedzieć, jak się nimi posługiwać. Właśnie temu tematowi została poświęcona cała lekcja 14. Znajdują się w niej informacje o sposobach przekazywania metodom argumentów typów prostych oraz referencyjnych, a także o przeciążaniu metod, czyli technice umożliwiającej umieszczenie w jednej klasie kilku metod o tej samej nazwie. Nieco miejsca poświęcono także metodzie main, od której zaczyna się wykonywanie aplikacji. Opisany zostanie również sposób, w jaki przekazuje się aplikacji parametry z wiersza poleceń. Argumenty metod Dzięki lekcji 13. wiadomo, że każda metoda może mieć argumenty. Argumenty metody to inaczej dane, które można jej przekazać. Metoda może mieć dowolną liczbę argumentów umieszczonych w nawiasie okrągłym za jej nazwą. Poszczególne argumenty oddziela się znakiem przecinka. Schematycznie wygląda to następująco: typ_wyniku nazwa_metody(typ_parametru_1 nazwa_parametru_1, typ_parametru_2 nazwa_parametru_2, ... , typ_parametru_n nazwa_parametru_n) { /*treść metody*/ } Przykładowo w klasie Punkt przydałyby się metody umożliwiające ustawianie współrzędnych. Jest tu możliwych kilka wariantów — zacznijmy od najprostszych: napiszemy dwie metody, ustawX i ustawY. Pierwsza będzie odpowiedzialna za przypisanie przekazanej jej wartości polu x, a druga — polu y. Zgodnie z podanym powyżej schematem pierwsza z nich powinna wyglądać następująco: void ustawX(int wspX) { x = wspX; } natomiast druga: void ustawY(int wspY) { y = wspY; } Metody te nie zwracają żadnych wyników, co sygnalizuje słowo void, przyjmują natomiast jeden argument typu int. W ciele każdej z metod następuje z kolei przypisanie wartości przekazanej w parametrze odpowiedniemu polu: x w przypadku metody ustawX oraz y w przypadku metody ustawY. W podobny sposób można napisać metodę, która będzie jednocześnie ustawiała pola x i y obiektów klasy Punkt. Oczywiście będzie przyjmowała dwa argumenty. Jak wiadomo, w deklaracji należy oddzielić je przecinkiem, zatem będzie ona wyglądała następująco: Rozdział 3. Programowanie obiektowe. Część I 101 void ustawXY(int wspX, int wspY) { x = wspX; y = wspY; } Taka metoda ustawXY nie zwraca żadnego wyniku, ale przyjmuje dwa argumenty: wspX, wspY, oba typu int. We wnętrzu tej metody parametr wspX (dokładniej jego wartość) zostaje przypisany polu x, a wspY polu y. Jeśli teraz dodamy do klasy Punkt wszystkie trzy opisane powyżej metody, powstanie kod widoczny na listingu 3.8. Listing 3.8. class Punkt { int x; int y; int pobierzX() { return x; } int pobierzY() { return y; } void ustawX(int wspX) { x = wspX; } void ustawY(int wspY) { y = wspY; } void ustawXY(int wspX, int wspY) { x = wspX; y = wspY; } void wyswietlWspolrzedne() { System.out.println("współrzędna x = " + x); System.out.println("współrzędna y = " + y); } } Warto teraz dodatkowo napisać klasę Main, która przetestuje nowe metody. Dzięki temu będzie można sprawdzić, czy wszystkie trzy działają zgodnie z założeniami. Taka przykładowa klasa jest widoczna na listingu 3.9. Listing 3.9. class Main { public static void main (String args[]) { Punkt punkt = new Punkt(); Punkt nowyPunkt = new Punkt(); punkt.ustawX(100); punkt.ustawY(100); System.out.println("punkt:"); punkt.wyswietlWspolrzedne(); nowyPunkt.ustawXY(200, 200); 102 Java. Praktyczny kurs System.out.println("nowyPunkt:"); nowyPunkt.wyswietlWspolrzedne(); } } Na początku tworzymy dwa obiekty typu (klasy) Punkt, jeden z nich przypisujemy zmiennej o nazwie punkt, drugi — zmiennej o nazwie nowy_punkt. Tak naprawdę (jak już wiadomo z wcześniejszego opisu) obu zmiennym zostały przypisane referencje do utworzonych na stercie obiektów, jednak od tego miejsca książki niekiedy będzie stosowana również taka uproszczona, ale często spotykana terminologia, czyli utożsamianie zmiennej ze wskazywanym przez nią obiektem. Zatem jeśli zostanie napisane na przykład, że zmiennej jest przypisywany obiekt, w rzeczywistości będzie chodziło o to, że jest jej przypisywana referencja do tego obiektu. Następnie wykorzystujemy metody ustawX i ustawY do przypisania polom obiektu5 punkt wartości 100. W kolejnym kroku za pomocą metody wyswietlWspolrzedne wyświetlamy te wartości na ekranie. Dalej wykorzystujemy metodę ustawXY, aby przypisać polom obiektu nowy_punkt wartości 200, oraz wyświetlamy je na ekranie, również za pomocą metody wyswietlWspolrzedne. Po wykonaniu tego programu uzyskany efekt będzie taki jak na rysunku 3.5. Rysunek 3.5. Wykonanie programu z listingu 3.9 Obiekt jako argument Argumentem przekazanym metodzie może być również obiekt, nie musimy ograniczać się jedynie do typów prostych. Podobnie metoda może zwracać obiekt w wyniku swojego wykonania. W obu wymienionych sytuacjach postępowanie jest dokładnie takie samo jak w przypadku typów prostych. Przykładowo metoda ustawXY w klasie Punkt mogłaby przyjmować jako argument obiekt tej klasy (ściślej: referencję do obiektu), a nie dwie liczby typu int, tak jak zostało to zrobione w przedstawionych wcześniej przykładach (listing 3.8). Metoda taka wyglądałaby następująco: void ustawXY(Punkt punkt) { x = punkt.x; y = punkt.y; } 5 Dokładniej: polom obiektu wskazywanego przez zmienną punkt. Stosowana jest tu jednak już wspomniana uproszczona, bardziej potoczna i łatwiejsza w komunikacji terminologia. Rozdział 3. Programowanie obiektowe. Część I 103 Parametrem (argumentem) jest w tej chwili obiekt punkt klasy Punkt. W ciele metody następuje skopiowanie wartości pól z obiektu przekazanego jako argument do obiektu bieżącego, czyli przypisanie polu x wartości zapisanej w punkt.x, a polu y wartości zapisanej w punkt.y. Podobnie można dopisać do klasy Punkt metodę o nazwie pobierzWspolrzedne, która zwróci w wyniku nowy obiekt klasy Punkt o takich współrzędnych, jakie zostały zapisane w polach obiektu bieżącego. Metoda ta będzie miała postać: Punkt pobierzWspolrzedne() { Punkt punkt = new Punkt(); punkt.x = x; punkt.y = y; return punkt; } Jak widać, nie przyjmuje ona żadnych argumentów, nie ma przecież takiej potrzeby; z deklaracji wynika jednak, że zwraca obiekt klasy Punkt. W ciele metody najpierw tworzymy nowy obiekt klasy Punkt, przypisując go zmiennej referencyjnej o nazwie punkt, a następnie przypisujemy jego polom wartości pól x i y z obiektu bieżącego. Ostatecznie za pomocą instrukcji return powodujemy, że obiekt punkt staje się wartością zwracaną przez metodę. Klasa Punkt po wprowadzeniu takich modyfikacji będzie miała postać widoczną na listingu 3.10. Listing 3.10. class Punkt { int x; int y; int pobierzX() { return x; } int pobierzY() { return y; } void ustawX(int wspX) { x = wspX; } void ustawY(int wspY) { y = wspY; } void ustawXY(Punkt punkt) { x = punkt.x; y = punkt.y; } Punkt pobierzWspolrzedne() { Punkt punkt = new Punkt(); punkt.x = x; punkt.y = y; return punkt; } void wyswietlWspolrzedne() { System.out.println("współrzędna x = " + x); System.out.println("współrzędna y = " + y); } } 104 Java. Praktyczny kurs Aby lepiej uzmysłowić sobie sposób działania wymienionych metod, warto napisać teraz krótki program, który będzie je wykorzystywał. Powstanie oczywiście kolejna wersja klasy Main. Jest ona widoczna na listingu 3.11. Listing 3.11. class Main { public static void main (String args[]) { Punkt punkt = new Punkt(); Punkt drugiPunkt; punkt.ustawX(100); punkt.ustawY(200); System.out.println("Obiekt punkt ma współrzędne:"); punkt.wyswietlWspolrzedne(); System.out.print("\n"); drugiPunkt = punkt.pobierzWspolrzedne(); System.out.println("Obiekt drugiPunkt ma współrzędne:"); drugiPunkt.wyswietlWspolrzedne(); System.out.print("\n"); Punkt trzeciPunkt = new Punkt(); trzeciPunkt.ustawXY(drugiPunkt); System.out.println("Obiekt trzeciPunkt ma współrzędne:"); trzeciPunkt.wyswietlWspolrzedne(); } } Na początku deklarujemy zmienne punkt oraz drugiPunkt. Zmiennej punkt przypisujemy nowo utworzony obiekt klasy Punkt (rysunek 3.7a). Następnie wykorzystujemy znane nam już dobrze metody ustawX i ustawY do przypisania polom x i y wartości 100 oraz wyświetlamy te dane na ekranie, używając metody wyswietlWspolrzedne. W kolejnym kroku zmiennej drugiPunkt, która nie została wcześniej zainicjowana, przypisujemy obiekt zwrócony przez metodę pobierzXY wywołaną na rzecz obiektu punkt. A zatem zapis: drugiPunkt = punkt.pobierzWspolrzedne(); oznacza, że wywoływana jest metoda pobierzWspolrzedne obiektu punkt, a zwrócony przez nią wynik jest przypisywany zmiennej drugiPunkt. Jak wiadomo, wynikiem działania tej metody będzie obiekt (referencja do obiektu) klasy Punkt będący kopią obiektu punkt, czyli zawierający w polach x i y takie same wartości, jakie są zapisane w polach obiektu punkt. To znaczy, że po wykonaniu tej instrukcji zmienna drugiPunkt zawiera referencję do obiektu, w którym pola x i y mają wartość 100 (rysunek 3.7b). Obie wartości wyświetlamy na ekranie za pomocą metody wyswietlWspolrzedne. W trzeciej części programu tworzymy obiekt trzeciPunkt (Punkt trzeciPunkt = new Punkt();) i wywołujemy jego metodę ustawXY, aby wypełnić pola x i y danymi. Metoda Rozdział 3. Programowanie obiektowe. Część I 105 ta jako argument przyjmuje obiekt klasy Punkt, w tym przypadku obiekt drugiPunkt. Zatem po wykonaniu instrukcji wartości pól x i y obiektu trzeciPunkt będą takie same jak pól x i y obiektu drugiPunkt (rysunek 3.7c). Nic więc dziwnego, że wynik działania programu z listingu 3.11 jest taki jak zaprezentowany na rysunku 3.6. Z kolei na rysunku 3.7 przedstawiono schematyczne zależności pomiędzy zmiennymi i obiektami występującymi w klasie Main. Rysunek 3.6. Utworzenie trzech takich samych obiektów różnymi metodami Rysunek 3.7. Kolejne etapy powstawania zmiennych i obiektów w programie z listingu 3.11 W fazie pierwszej, na samym początku programu, są jedynie dwie zmienne: punkt i drugiPunkt. Tylko pierwszej z nich jest przypisany obiekt, druga jest po prostu pusta (zawiera wartość null). Zostało to przedstawione na rysunku 3.7a. W części drugiej przypisujemy zmiennej drugiPunkt odniesienie do obiektu, który jest kopią obiektu punkt (rysunek 3.7b), a w trzeciej tworzymy obiekt trzeciPunkt i wypełniamy go danymi pochodzącymi z obiektu drugiPunkt. Tym samym ostatecznie otrzymujemy trzy zmienne i trzy obiekty (rysunek 3.7c). 106 Java. Praktyczny kurs Metoda main Każdy program musi zawierać punkt startowy, czyli miejsce, od którego zacznie się jego wykonywanie. W Javie takim miejscem jest metoda o nazwie main i następującej deklaracji: public static void main(String[] args) { } Jeśli w danej klasie znajdzie się metoda w takiej postaci, od niej właśnie zacznie się wykonywanie kodu programu. Teraz powinno być już jasne, czemu w dwóch pierwszych rozdziałach przykładowe programy miały schematyczną konstrukcję: class Main { public static void main(String args[]) { //tutaj instrukcje do wykonania } } Przy użyciu tej konstrukcji była po prostu tworzona pomocnicza klasa o nazwie Main, a w niej metoda main, od której zaczynało się wykonywanie kodu aplikacji. Rzecz jasna klasa ta mogłaby mieć dowolną inną nazwę, np. KlasaUruchomieniowa lub podobną. Metoda main nie może zwracać żadnej wartości, czyli typem zwracanym musi być void. Przed słowem void występują jeszcze dwa terminy, public i static, jednak nie będą w tej chwili omawiane — przyjdzie na to czas w dalszej części książki. Warto natomiast w tym miejscu zwrócić uwagę na argument metody main, którym jest, jak już wiadomo z rozdziału 2., tablica typu String. Typ String opisuje obiekty, które są ciągami znaków, czyli napisami. Innymi słowy, args to tablica ciągów znaków. Tablica ta zawiera parametry wywołania programu, czyli parametry przekazane z wiersza poleceń. O tym, że tak jest w istocie, można się przekonać, uruchamiając program widoczny na listingu 3.12. Wykorzystuje on pętlę for do przejrzenia i wyświetlenia na ekranie zawartości wszystkich komórek tablicy args. Przykładowy wynik jego działania jest widoczny na rysunku 3.8. Listing 3.12. class Main { public static void main (String args[]) { System.out.println("Parametry wywołania:"); for(int i = 0; i < args.length; i++){ System.out.println(args[i]); } } } Metodę main można umieścić w dowolnej klasie, po prostu tam, gdzie jest to najwygodniejsze. Co więcej, nawet w ramach jednej aplikacji każda z klas wchodzących w jej skład może mieć swoją własną metodę main. O tym, która z tych metod zostanie wykonana, decyduje to, która klasa będzie klasą uruchomieniową. Klasa uruchomieniowa to ta klasa, której nazwę podaje się jako parametr dla maszyny wirtualnej. Czyli jeśli uruchamiamy aplikację, pisząc: java Main Rozdział 3. Programowanie obiektowe. Część I 107 Rysunek 3.8. Program wyświetlający parametry jego wywołania klasą uruchomieniową jest klasa Main, jeśli natomiast piszemy: java Punkt klasą uruchomieniową jest Punkt. W praktyce zazwyczaj stosuje się tylko jedną metodę main dla całej aplikacji. Powróćmy teraz do przykładów związanych z klasą Punkt. Skoro możliwe jest umieszczenie punktu startowego w dowolnej klasie, może to być także klasa Punkt. Dotychczas do przetestowania działania obiektów klasy Punkt pisaliśmy dodatkową klasę Main, jednak okazuje się, że nie jest to wcale potrzebne (zastosowanie dwóch klas jest jednak bardziej czytelne dla osób początkujących). Spójrzmy teraz ponownie na listingi 3.1 i 3.2 z lekcji 13. Na pierwszym z nich znajduje się podstawowa wersja klasy Punkt, która zawiera jedynie pola x i y. Drugi zawiera klasę Main, która testuje zachowanie obiektu klasy Punkt. Połączmy je zatem w taki sposób, aby metoda main znalazła się w klasie Punkt, bez zmieniania samej treści tej metody. Rozwiązanie tego zadania jest przedstawione na listingu 3.13. Listing 3.13. class Punkt { int x; int y; public static void main (String args[]) { Punkt punkt = new Punkt(); punkt.x = 100; punkt.y = 100; System.out.println("punkt.x = " + punkt.x); System.out.println("punkt.y = " + punkt.y); } } Co warte uwagi, nie dokonaliśmy tutaj żadnych poważnych zmian. Kod metody main pozostaje niezmieniony, a jedyną modyfikacją w klasie Punkt jest dodanie tej właśnie metody. Program będzie się zachowywał dokładnie tak samo jak ten z listingów 3.1 i 3.2 składający się z klas Main i Punkt. Oczywiście tym razem należy skompilować jedynie klasę Punkt oraz wykorzystać ją jako klasę uruchomieniową, stosując wywołanie maszyny wirtualnej w postaci: java Punkt tak, jak jest to przedstawione na rysunku 3.9. 108 Java. Praktyczny kurs Rysunek 3.9. Klasa Punkt jako klasa uruchomieniowa Przeciążanie metod W trakcie pracy nad kodem klasy Punkt powstały dwie metody o takiej samej nazwie, ale różnym kodzie. Chodzi oczywiście o metody ustawXY. Pierwsza wersja przyjmowała jako argumenty dwie liczby typu int, a druga miała tylko jeden argument, którym był obiekt klasy Punkt. Okazuje się, że takie dwie metody mogą współistnieć w klasie Punkt i z obu z nich można korzystać w kodzie programu. Ogólnie rzecz ujmując, w każdej klasie może istnieć dowolna liczba metod, które mają takie same nazwy, o ile tylko różnią się argumentami. Mogą one — ale nie muszą — różnić się również typem zwracanego wyniku. Taką technikę nazywa się przeciążaniem metod (ang. overloading). Skonstruujmy zatem taką klasę Punkt, w której znajdą się obie wersje metody ustawXY. Kod tej klasy jest przedstawiony na listingu 3.14. Listing 3.14. class Punkt { int x; int y; void ustawXY(int wspX, int wspY) { x = wspX; y = wspY; } void ustawXY(Punkt punkt) { x = punkt.x; y = punkt.y; } } Klasa ta zawiera w tej chwili dwie przeciążone metody o nazwie ustawXY. Jest to możliwe, ponieważ przyjmują one różne argumenty: pierwsza metoda — dwie liczby typu int, druga — jeden obiekt klasy Punkt. Obie metody realizują takie samo zadanie, tzn. ustawiają nowe wartości w polach x i y. Ich działanie można przetestować poprzez dopisanie w klasie Punkt metody main w przykładowej postaci przedstawionej na listingu 3.15. Listing 3.15. public static void main (String args[]) { Punkt punkt = new Punkt(); Punkt drugiPunkt = new Punkt(); punkt.ustawXY(100, 100); drugiPunkt.ustawXY(200,200); Rozdział 3. Programowanie obiektowe. Część I 109 System.out.println("Po pierwszym ustawieniu współrzędnych:"); System.out.println("x = " + punkt.x); System.out.println("y = " + punkt.y); System.out.println(""); punkt.ustawXY(drugiPunkt); System.out.println("Po drugim ustawieniu współrzędnych:"); System.out.println("x = " + punkt.x); System.out.println("y = " + punkt.y); } Działanie tej metody jest proste i nie wymaga wielu wyjaśnień. Na początku tworzymy dwa obiekty klasy Punkt i przypisujemy je zmiennym punkt oraz drugiPunkt. Następnie korzystamy z pierwszej wersji przeciążonej metody ustawXY, aby przypisać polom x i y pierwszego obiektu wartość 100, a polom x i y drugiego obiektu — 200. Dalej wyświetlamy zawartość obiektu punkt na ekranie. Potem wykorzystujemy drugą wersję metody ustawXY, aby zmienić zawartość pól obiektu punkt, tak by zawierały wartości zapisane w obiekcie drugiPunkt. Następnie ponownie wyświetlamy wartości pól obiektu punkt na ekranie. Ćwiczenia do samodzielnego wykonania Ćwiczenie 14.1. Napisz program, który wyświetli ponumerowaną listę parametrów przekazanych mu z wiersza poleceń — od argumentu ostatniego aż do pierwszego. Ćwiczenie 14.2. Napisz program, który połączy wszystkie przekazane mu argumenty w jeden ciąg znaków i wyświetli go na ekranie. Ćwiczenie 14.3. Do klasy Punkt z listingu 3.8 dopisz metody ustawX i ustawY, które jako argument będą przyjmowały obiekt klasy Punkt. Ćwiczenie 14.4. W klasie Punkt z listingu 3.8 zmień kod metod ustawX i ustawY, tak aby zwracały one poprzednią wartość zapisywanych pól. Zadaniem metody ustawX jest więc zmiana wartości pola x i zwrócenie jego poprzedniej wartości. Metoda ustawY ma wykonywać analogiczne czynności w stosunku do pola y. Ćwiczenie 14.5. Do klasy Punkt dopisz metodę ustawXY przyjmującą jako argument obiekt klasy Punkt. Polom x i y należy przypisać wartości pól x i y przekazanego obiektu. Metoda ma natomiast zwrócić obiekt klasy Punkt zawierający stare wartości x i y. 110 Java. Praktyczny kurs Lekcja 15. Konstruktory Lekcja 15. jest poświęcona konstruktorom, czyli specjalnym metodom wykonywanym podczas tworzenia obiektów. Będzie się można z niej dowiedzieć, jak powstaje konstruktor, jak umieścić go w klasie, a także czy może on przyjmować argumenty. Znajdą się tu również informacje o sposobach przeciążania konstruktorów oraz o wykorzystaniu słowa kluczowego this. Nie pominiemy techniki wywoływania konstruktora z innego konstruktora, która w pewnych sytuacjach upraszcza budowę kodu. Na zakończenie pojawi się specjalna metoda finalize wykonywana, kiedy obiekt jest usuwany z pamięci. Inicjalizacja obiektu Jak wiadomo z lekcji 13., po utworzeniu obiektu w pamięci wszystkie jego pola zawierają wartości domyślne. Wartości te dla poszczególnych typów danych zostały przedstawione w tabeli 3.1. Najczęściej jednak oczekuje się, by pola te zawierały jakieś konkretne wartości. Przykładowo można byłoby życzyć sobie, aby każdy obiekt powstałej w lekcji 13. (listing 3.1) klasy Punkt otrzymywał współrzędne x = 1 i y = 1. Oczywiście można po każdym utworzeniu obiektu przypisywać wartości tym polom, np.: Punkt punkt = new Punkt(); punkt.x = 1; punkt.y = 1; Można też dopisać do klasy Punkt dodatkową metodę, na przykład o nazwie init, initialize, inicjuj lub podobnej, w postaci: void init() { x = 1; y = 1; } i wywoływać ją po każdym utworzeniu obiektu. Widać jednak od razu, że żadna z tych koncepcji nie jest wygodna. Przede wszystkim wymagają one, aby programista zawsze pamiętał o ich stosowaniu, a jak pokazuje praktyka, jest to zwykle zbyt optymistyczne założenie. Na szczęście obiektowe języki programowania udostępniają dużo wygodniejszy mechanizm konstruktorów. Otóż konstruktor jest to specjalna metoda, która zostaje wywołana zawsze w trakcie tworzenia obiektu w pamięci. Nadaje się więc doskonale do jego zainicjowania. Metoda będąca konstruktorem nigdy nie zwraca żadnego wyniku i musi mieć nazwę zgodną z nazwą klasy — schematycznie wygląda to następująco: class nazwa_klasy { nazwa_klasy() { //kod konstruktora } } Przed definicją nie umieszcza się słowa void, tak jak miałoby to miejsce w przypadku zwykłej metody. To, co będzie robił konstruktor, czyli jakie wykona czynności, zależy już tylko od programisty. Rozdział 3. Programowanie obiektowe. Część I 111 Dopiszmy zatem do klasy Punkt z listingu 3.1 (czyli jej najprostszej wersji) konstruktor, który będzie przypisywał polom x i y każdego obiektu wartość 1. Wygląd takiej klasy został zaprezentowany na listingu 3.16. Listing 3.16. class Punkt { int x; int y; Punkt() { x = 1; y = 1; } } Jak widać, wszystko jest tu zgodne z podanym wyżej schematem. Konstruktor nie zwraca żadnej wartości i ma nazwę zgodną z nazwą klasy. Przed nazwą nie występuje słowo void. W jego wnętrzu następuje proste przypisanie wartości polom obiektu. O tym, że konstruktor działa, można się przekonać, pisząc dodatkową klasę Main i używając w niej obiektu nowej klasy Punkt (można by też dopisać metodę main do klasy Punkt). Taka przykładowa klasa Main jest widoczna na listingu 3.17. Listing 3.17. class Main { public static void main (String args[]) { Punkt punkt = new Punkt(); System.out.println("punkt.x = " + punkt.x); System.out.println("punkt.y = " + punkt.y); } } Klasa ta ma wyjątkowo prostą konstrukcję, jedyne jej zadania to utworzenie obiektu klasy Punkt i przypisanie odniesienia do niego zmiennej punkt oraz wyświetlenie zawartości jego pól na ekranie. Dzięki temu przekonamy się, że konstruktor rzeczywiście został wykonany, zobaczymy bowiem widok zaprezentowany na rysunku 3.10. Rysunek 3.10. Konstruktor klasy Punkt faktycznie został wykonany Argumenty konstruktorów Konstruktor nie musi być bezargumentowy, może również przyjmować argumenty, które zostaną wykorzystane, bezpośrednio lub pośrednio, np. do zainicjowania pól obiektu. Argumenty przekazuje się dokładnie tak samo jak w przypadku zwykłych metod (lekcja 14.), budowa takiego konstruktora byłaby więc następująca: 112 Java. Praktyczny kurs class nazwa_klasy { nazwa_klasy(typ1 argument1, typ2 argument2,..., typN argumentN) { } } Jeśli konstruktor przyjmuje argumenty, przy tworzeniu obiektu należy je podać, czyli zamiast stosowanej do tej pory konstrukcji: nazwa_klasy zmienna = new nazwa_klasy() trzeba wykorzystać wywołanie: nazwa_klasy zmienna = new nazwa_klasy(argumenty_konstruktora) W przypadku przykładowej klasy Punkt przydatny byłby np. konstruktor przyjmujący dwa argumenty, które oznaczałyby współrzędne punktu. Jego definicja, co nie jest z pewnością żadnym zaskoczeniem, będzie wyglądać następująco: Punkt(int wspX, int wspY) { x = wspX; y = wspY; } Kiedy zostanie umieszczony w klasie Punkt, przyjmie ona postać widoczną na listingu 3.18. Listing 3.18. class Punkt { int x; int y; Punkt(int wspX, int wspY) { x = wspX; y = wspY; } } Teraz podczas każdej próby utworzenia obiektu klasy Punkt będzie trzeba podawać jego współrzędne. Przykładowo: jeśli początkowa współrzędna x ma mieć wartość 100, a początkowa współrzędna y — 200, należy zastosować konstrukcję: Punkt punkt = new Punkt(100, 200); Przeciążanie konstruktorów Konstruktory, tak jak zwykłe metody, mogą być przeciążane, tzn. każda klasa może mieć kilka konstruktorów, o ile tylko różnią się one przyjmowanymi argumentami. Do tej pory powstały dwa konstruktory klasy Punkt: pierwszy bezargumentowy i drugi przyjmujący dwa argumenty typu int. Dopiszmy zatem jeszcze trzeci, który jako argument będzie przyjmował obiekt klasy Punkt. Jego postać będzie następująca: Punkt(Punkt punkt) { x = punkt.x; y = punkt.y; } Rozdział 3. Programowanie obiektowe. Część I 113 Zasada działania jest chyba jasna: polu x jest przypisywana wartość pola x obiektu przekazanego jako argument, natomiast polu y — wartość pola y tego obiektu. Można teraz zebrać wszystkie trzy napisane dotychczas konstruktory i umieścić je w klasie Punkt. Będzie ona wtedy miała postać przedstawioną na listingu 3.19. Listing 3.19. class Punkt { int x; int y; Punkt() { x = 1; y = 1; } Punkt(int wspX, int wspY) { x = wspX; y = wspY; } Punkt(Punkt punkt) { x = punkt.x; y = punkt.y; } } Taka budowa klasy Punkt pozwala na niezależne wywoływanie każdego z trzech konstruktorów, w zależności od tego, który z nich jest najbardziej odpowiedni w danej sytuacji. Warto teraz na konkretnym przykładzie przekonać się, że tak jest w istocie. Napiszmy więc klasę Main, w której powstaną trzy obiekty klasy Punkt, a każdy z nich będzie tworzony za pomocą innego konstruktora. Taka przykładowa klasa jest widoczna na listingu 3.20. Listing 3.20. class Main { public static void main (String args[]) { Punkt punkt1 = new Punkt(); System.out.println("punkt1:"); System.out.println("x = " + punkt1.x); System.out.println("y = " + punkt1.y); System.out.println(""); Punkt punkt2 = new Punkt(100, 100); System.out.println("punkt2:"); System.out.println("x = " + punkt2.x); System.out.println("y = " + punkt2.y); System.out.println(""); Punkt punkt3 = new Punkt(punkt1); System.out.println("punkt3:"); System.out.println("x = " + punkt3.x); System.out.println("y = " + punkt3.y); } } 114 Java. Praktyczny kurs Pierwszy obiekt — punkt1 — jest tworzony za pomocą konstruktora bezargumentowego, który przypisuje polom x i y wartość 1. Obiekt drugi — punkt2 — jest tworzony poprzez wywołanie drugiego z konstruktorów, który przyjmuje dwa argumenty odzwierciedlające wartości x i y. Oba pola otrzymują wartość 100. Konstruktor trzeci, zastosowany wobec obiektu punkt3, to nasza najnowsza konstrukcja. Jako argument przyjmuje on obiekt klasy Punkt, w omawianym przypadku — obiekt wskazywany przez punkt1. Ponieważ w tym obiekcie oba pola mają wartość 1, również pola obiektu punkt3 przyjmą wartość 1. W efekcie działania programu zobaczymy widok zaprezentowany na rysunku 3.11. Rysunek 3.11. Wykorzystanie trzech różnych konstruktorów klasy Punkt Słowo kluczowe this Słowo kluczowe this to nic innego niż odwołanie do obiektu bieżącego. Można je traktować jako referencję do aktualnego obiektu. Najłatwiej pokazać to na przykładzie. Załóżmy, że mamy konstruktor klasy Punkt taki jak na listingu 3.18, czyli przyjmujący dwa argumenty, którymi są liczby typu int. Nazwami tych parametrów były wspX i wspY. A co by się stało, gdyby nazwy tych parametrów brzmiały x i y, czyli gdyby deklaracja tego konstruktora wyglądała tak jak poniżej? Punkt(int x, int y) { } Co należy wpisać w jego treści, aby spełniał swoje zadanie? Gdyby postępować podobnie jak w przypadku klasy z listingu 3.18, powstałaby konstrukcja: Punkt(int x, int y) { x = x; y = x; } Oczywiście nie ma to najmniejszego sensu6. W jaki bowiem sposób kompilator ma rozróżnić, kiedy chodzi o argument konstruktora, a kiedy o pole klasy, jeśli ich nazwy są takie same? Sam sobie nie poradzi i tu właśnie z pomocą przychodzi słowo this. 6 Chociaż formalnie taki zapis jest w pełni poprawny. Rozdział 3. Programowanie obiektowe. Część I 115 Otóż jeśli chcemy zaznaczyć, że chodzi o składową klasy (pole, metodę), korzystamy z odwołania w postaci: this.nazwa_pola lub: this.nazwa_metody Wynika z tego, że poprawna postać opisanego konstruktora powinna wyglądać następująco: Punkt(int x, int y) { this.x = x; this.y = y; } Instrukcję this.x = x rozumiemy jako: przypisz polu x wartość przekazaną jako argument o nazwie x, a instrukcję this.y = y odpowiednio jako: przypisz polu y wartość przekazaną jako argument o nazwie y. Wywoływanie metod w konstruktorach Z wnętrza konstruktora (tak jak z wnętrza każdej innej metody) można wywoływać inne metody. W trakcie prac nad klasą Punkt pojawiły się m.in. metody ustawX, ustawY czy ustawXY. Doskonale nadają się one do ustalania wartości pól tej klasy. Skoro tak, można je wykorzystać w każdym z konstruktorów, zamiast bezpośrednio przypisywać wartości polom klasy. Najbardziej odpowiednia do tego celu będzie metoda ustawXY, która ustawia jednocześnie oba pola. Jeśli chcemy zmienić kod konstruktorów przedstawionych na listingu 3.19, dobrze będzie zastosować dwie wersje metody ustawXY — jedną przyjmującą jako argument dwie wartości typu int i drugą przyjmującą jako argument obiekt klasy Punkt. Same konstruktory wyglądałyby wówczas następująco: Punkt() { ustawXY(1, 1); } Punkt(int x, int y) { ustawXY(x, y); } Punkt(Punkt punkt) { ustawXY(punkt); } Pierwszy z nich ustawi współrzędne punktu na x = 1 i y = 1, drugi zrobi to zgodnie z przekazanymi wartościami w argumentach x i y, z kolei trzeci skopiuje wartości argumentów x i y z przekazanego obiektu punkt. Działanie tych konstruktorów będzie zatem takie samo jak tych z listingu 3.19, mimo że została zastosowana zupełnie inna konstrukcja kodu. Jeśli umieścimy je w klasie Punkt wraz z niezbędnymi wersjami metody ustawXY, otrzymamy kod klasy widoczny na listingu 3.21. Listing 3.21. class Punkt { int x; int y; 116 Java. Praktyczny kurs Punkt() { ustawXY(1, 1); } Punkt(int x, int y) { ustawXY(x, y); } Punkt(Punkt punkt) { ustawXY(punkt); } void ustawXY(int wspX, int wspY) { x = wspX; y = wspY; } void ustawXY(Punkt punkt) { x = punkt.x; y = punkt.y; } } Jest to w pełni funkcjonalny odpowiednik klasy z listingu 3.19. Można się o tym przekonać, uruchamiając program testowy zamieszczony na listingu 3.20. Wynik jego działania będzie dokładnie taki sam jak w poprzednim przypadku, czyli taki, jaki przedstawiono na rysunku 3.11. Przyjrzyjmy się teraz dokładniej drugiemu konstruktorowi, który jako argumenty przyjmuje dwie liczby typu int. Ma on postać: Punkt(int x, int y) { ustawXY(x, y); } Po cofnięciu się do opisu słowa kluczowego this można sobie przypomnieć, że w sytuacji, kiedy pola obiektu oraz argumenty metody mają takie same nazwy, występuje problem z rozróżnieniem, czy chodzi o pola, czy o argumenty. Czy w tym przypadku również istnieje taka wątpliwość i czy w związku z tym ten konstruktor jest na pewno poprawny? Oczywiście jest poprawny. Jeśli bowiem gdziekolwiek we wnętrzu metody odwołujemy się do nazwy argumentu, to niezależnie od tego, czy istnieje pole o takiej nazwie, czy nie, kompilator zawsze przyjmuje, że chodzi o argument. Można powiedzieć, że w ciele metody argument ma większą siłę niż nazwa pola. Tak więc w powyższym przykładzie, jeśli napiszemy x lub y, to na pewno chodzi o argumenty, a jeśli chcemy się odwołać do pól, musimy napisać this.x lub this.y. Dzięki temu sytuacja jest jednoznaczna i zawsze wiadomo, o jakie odwołanie chodzi. Z wnętrza konstruktora można również wywołać… inny konstruktor. Bywa to przydatne w sytuacji, kiedy w klasie znajduje się kilka przeciążonych konstruktorów, a zakres wykonywanego przez nie kodu się pokrywa — dokładniej, gdy kod jednego z konstruktorów jest podzbiorem kodu innego. Nie zawsze takie wywołanie jest możliwe i niezbędne, niemniej taka możliwość istnieje, trzeba więc wiedzieć, jak zrealizować zadanie tego typu. Przydatne okazuje się znane nam już słowo kluczowe this. Jeżeli za nim poda się listę argumentów umieszczonych w nawiasie okrągłym, czyli: this(argument1, argument2, ... , argumentn) Rozdział 3. Programowanie obiektowe. Część I 117 to zostanie wywołany konstruktor, którego argumenty pasują do wymienionych. Przykład kodu wykorzystującego taką technikę jest widoczny na listingu 3.22. Listing 3.22. class Dane { int liczba1; double liczba2; Dane(int liczba) { liczba1 = liczba; } Dane(double liczba) { liczba2 = liczba; } Dane(int liczba1, double liczba2) { this(liczba1); this.liczba2 = liczba2; } void wyswietlDane() { System.out.println("liczba1 = " + liczba1); System.out.println("liczba2 = " + liczba2); } } Jest to hipotetyczna klasa o nazwie Dane, która zawiera jedynie dwa pola: pierwsze typu int o nazwie liczba1 i drugie typu double o nazwie liczba2. Do dyspozycji są też trzy konstruktory. Pierwszy z nich przyjmuje jeden argument typu int i w jego ciele następuje przypisanie wartości tego argumentu polu liczba1. Drugi konstruktor również przyjmuje tylko jeden argument, ale typu double, służy zatem do zainicjowania pola liczba2. Najciekawszy z naszego punktu widzenia jest trzeci konstruktor. Przyjmuje on dwa argumenty — pierwszy typu int, drugi typu double. W jego wnętrzu nie następuje jednak bezpośrednie przypisanie przekazanych wartości obu polom. Zamiast tego najpierw wywołujemy pierwszy konstruktor, przekazując mu argument liczba1 (czyli wartość typu int), a dopiero w drugiej linii wykonujemy klasyczne przypisanie wartości argumentu liczba2 polu liczba2. Rzecz jasna, ponieważ argument ma taką samą nazwę jak pole, wykorzystujemy znaną nam już dobrze konstrukcję ze słowem kluczowym this. W tym miejscu pojawi się zapewne pytanie, czy nie można byłoby tu zastosować następującej postaci konstruktora: Dane(int liczba1, double liczba2) { this(liczba1); this(liczba2); } Wydaje się ona bardziej logiczna: skoro bowiem istnieje konstruktor przyjmujący jeden argument typu int oraz drugi przyjmujący jeden argument typu double, w konstruktorze dwuargumentowym należałoby wywołać je oba (tak jak na listingu powyżej). Niestety nie wolno nam tego zrobić. Zasada jest tutaj następująca: konstruktor można wywołać jawnie tylko w innym konstruktorze i musi on być pierwszą wykonywaną instrukcją. Oznacza to, że można wywołać tylko jeden konstruktor, a przed nim nie powinna się znaleźć żadna inna instrukcja. 118 Java. Praktyczny kurs Do przetestowania klasy Dane z listingu 3.22 można wykorzystać zawartą w niej metodę wyswietlDane. Taki przykładowy program jest widoczny na listingu 3.23. Wynik jego działania można obejrzeć na rysunku 3.12. Listing 3.23. class Main { public static void Dane dane1 = new Dane dane2 = new Dane dane3 = new main (String args[]) { Dane(100); Dane(99.9); Dane(200, 88.8); System.out.println("dane1:"); dane1.wyswietlDane(); System.out.println(""); System.out.println("dane2:"); dane2.wyswietlDane(); System.out.println(""); System.out.println("dane3:"); dane3.wyswietlDane(); } } Rysunek 3.12. Wynik działania programu z listingu 3.23 Metoda finalize Osoby, które programowały w językach obiektowych, takich jak np. C++ czy Object Pascal, zwykle zastanawiają się, jak w Javie wygląda destruktor i kiedy zwalniać pamięć zarezerwowaną dla obiektów. Skoro bowiem operator new pozwala na utworzenie obiektu, a tym samym na zarezerwowanie dla niego pamięci operacyjnej, logicznym założeniem jest, że po jego wykorzystaniu pamięć tę należy zwolnić. Ponieważ jednak takie podejście, tzn. zrzucenie na barki programistów konieczności zwalniania przydzielonej obiektom pamięci, powodowało powstawanie wielu błędów, w Javie zastosowano inne rozwiązanie. Otóż za zwalnianie pamięci odpowiada maszyna wirtualna, a programista praktycznie nie ma nad tym procesem kontroli7. 7 Wywołując metodę System.gc(), można jednak wymusić zainicjowanie procesu odzyskiwania pamięci. Rozdział 3. Programowanie obiektowe. Część I 119 Zajmuje się tym tak zwany odśmiecacz (ang. garbage collector), który czuwa nad optymalnym wykorzystaniem pamięci i uruchamia proces jej odzyskiwania w momencie, kiedy wolna ilość oddana do dyspozycji programu zbytnio się zmniejszy. Jest to wyjątkowo wygodne podejście dla programisty, zwalnia go bowiem z obowiązku zarządzania pamięcią. Zwiększa jednak nieco narzuty czasowe związane z wykonaniem programu, bo sam proces odśmiecania musi zająć czas procesora. Niemniej dzisiejsze maszyny wirtualne są na tyle dopracowane, że w większości przypadków nie ma najmniejszej potrzeby zaprzątania sobie głowy tym problemem. Należy jednak zdawać sobie sprawę, że Java jest w stanie automatycznie zarządzać wykorzystywaniem pamięci, ale tylko tej, która jest alokowana standardowo, czyli za pomocą operatora new. W nielicznych przypadkach, np. gdyby stworzony obiekt wykorzystywał mechanizmy alokacji pamięci specyficzne dla danej platformy systemowej czy też odwoływał się do modułów napisanych w innych językach programowania, o posprzątanie systemu i zwolnienie zarezerwowanej pamięci trzeba byłoby zadbać samemu. Java udostępnia w tym celu metodę finalize, która jest wykonywana zawsze, kiedy obiekt jest niszczony, usuwany z pamięci. Wystarczy więc, że klasa będzie zawierała taką metodę, a przy niszczeniu obiektu zostanie ona wykonana. Wewnątrz tej metody można wykonać dowolne instrukcje sprzątające. Jej deklaracja wygląda następująco: public void finalize() { //tutaj treść metody } Koniecznie jednak należy sobie uświadomić jedną rzecz: nie ma żadnej gwarancji, że ta metoda zostanie wykonana w trakcie działania programu! Przypomnijmy: proces odzyskiwania pamięci, a więc niszczenia nieużywanych już obiektów, zaczyna się wtedy, kiedy garbage collector „uzna” to za stosowne. Czyli wtedy, kiedy przyjmie, że ilość wolnej pamięci dostępnej dla programu zbytnio się zmniejszyła. Może się więc okazać, że pamięć zostanie zwolniona dopiero po zakończeniu pracy aplikacji. Dlatego jeśli niezbędne jest wykonanie dodatkowych czynności porządkowych w jakimś określonym miejscu podczas działania programu, lepiej napisać dodatkową metodę sprzątającą i jawnie ją wywołać. Żeby się przekonać, że metoda finalize jest faktycznie wykonywana, możemy napisać prostą klasę. Niech nosi nazwę Test. Została przedstawiona razem z testową klasą Main na listingu 3.24 (kod można zapisać w dwóch osobnych plikach Test.java i Main.java lub też tylko w jednym — Main.java). Listing 3.24. class Test { public void finalize() { System.out.println("Niszczenie obiektu."); } } class Main { public static void main (String args[]) { Test test = null; for(int i = 0; i < 10; i++){ 120 Java. Praktyczny kurs test = new Test(); } System.gc(); for(int i = 0; i < 100000; i++){ for(int j = 0; j < 1000; j++){ } } } } Klasa Test nie zawiera żadnych pól, a jej jedyną metodą jest finalize. Deklaracja tej metody musi być właśnie taka, tzn. public void finalize (znaczenie słowa public zostanie podane w dalszej części książki, w lekcji 17.). W jej ciele znajduje się tylko jedna instrukcja powodująca wyświetlenie na ekranie napisu Niszczenie obiektu. W klasie Main z kolei deklarujemy zmienną klasy Test o nazwie test i początkowo przypisujemy jej wartość pustą — null. Następnie w pętli dziesięć razy tworzymy nowy obiekt klasy Test i przypisujemy referencję do niego zmiennej test. Należy zauważyć, że każde takie przypisanie powoduje utracenie poprzedniej wartości zmiennej test (czyli poprzedniej referencji), a tym samym oznaczenie wcześniej przypisanego obiektu jako nieużywanego. To z kolei oznacza, że po wykonaniu całej pętli zmienna test będzie zawierała referencję do ostatnio przypisanego, dziesiątego obiektu, natomiast wszystkie poprzednie obiekty zostaną utracone i będą mogły zostać poddane procesowi odśmiecania. Aby zobaczyć efekty tego odśmiecania, wywołujemy na końcu instrukcję System.gc(), która powoduje uruchomienie odśmiecacza8. Za tą instrukcją znajdują się dwie zagnieżdżone pętle for, które opóźniają zakończenie wykonywania programu. Ostatecznie na ekranie pojawi się obraz widoczny na rysunku 3.13. Jest na nim dziewięć napisów Niszczenie obiektu odpowiadających dziewięciu utraconym obiektom. Ostatni, dziesiąty obiekt został zniszczony przez maszynę wirtualną już po zakończeniu pracy programu, dlatego też na ekranie nie mógł się pojawić dziesiąty napis. Warto samodzielnie sprawdzić, co się stanie, jeśli pętle opóźniające zostaną usunięte, i jak będzie się wtedy zachowywała aplikacja przy kolejnych uruchomieniach (uwaga: będzie to zależało od użytej maszyny wirtualnej i wersji Javy). Ćwiczenia do samodzielnego wykonania Ćwiczenie 15.1. Napisz klasę, której zadaniem będzie przechowywanie liczb typu int. Dołącz jednoargumentowy konstruktor przyjmujący argument typu int. Polu klasy nadaj nazwę liczba, tak samo nazwij argument konstruktora. 8 Dokładniej, należy tę instrukcję traktować jako sugestię dla maszyny wirtualnej, aby uruchomiła proces odzyskiwania pamięci. Nie ma stuprocentowej gwarancji, że zostanie on uruchomiony dokładnie w chwili wykonania tej instrukcji, niemniej odbędzie się to w najbliższym możliwym momencie, chyba że aplikacja wcześniej zakończy działanie. Rozdział 3. Programowanie obiektowe. Część I 121 Rysunek 3.13. Metoda finalize jest wykonywana podczas niszczenia obiektu Ćwiczenie 15.2. Do klasy powstałej w ćwiczeniu 15.1 dopisz przeciążony konstruktor bezargumentowy ustawiający jej pole na wartość –1. Ćwiczenie 15.3. Napisz klasę zawierającą dwa pola: pierwsze typu double i drugie typu char. Dopisz cztery przeciążone konstruktory: pierwszy przyjmujący jeden argument typu double, drugi przyjmujący jeden argument typu char, trzeci przyjmujący dwa argumenty — pierwszy typu double, drugi typu char — i czwarty przyjmujący również dwa argumenty — pierwszy typu char, drugi typu double. Ćwiczenie 15.4. Zmień kod klasy powstałej w ćwiczeniu 15.3 tak, aby w konstruktorach dwuargumentowych były wykorzystywane konstruktory jednoargumentowe. Ćwiczenie 15.5. Zmodyfikuj program z listingu 3.24 w taki sposób, aby przed wywołaniem odśmiecacza w programie nie było odwołania do żadnego obiektu na stercie (liczba obiektów powinna pozostać niezmienna). Ilu napisów o niszczeniu obiektu możesz się spodziewać po uruchomieniu programu? Sprawdź również zachowanie aplikacji po usunięciu znajdujących się na jej końcu pętli opóźniających. Dziedziczenie Dziedziczenie to jeden z fundamentów programowania obiektowego. Umożliwia sprawne i łatwe wykorzystywanie raz napisanego kodu czy budowanie hierarchii klas przejmujących swoje właściwości. Ten podrozdział zawiera cztery lekcje przybliżające temat dziedziczenia. W lekcji 16. zaprezentowane są podstawy, czyli sposoby tworzenia klas potomnych oraz zachowania konstruktorów klasy bazowej i potomnej. W lekcji 17. 122 Java. Praktyczny kurs zostały omówione specyfikatory dostępu pozwalające na ustalanie praw dostępu do składowych klas, a także temat tworzenia i wykorzystywania pakietów. W lekcji 18. przedstawiono techniki przesłaniania pól i metod w klasach potomnych oraz składowe statyczne. W lekcji ostatniej, 19., zostały opisane klasy finalne oraz składowe finalne klas. Lekcja 16. Klasy potomne Lekcja 16. jest poświęcona podstawom dziedziczenia, czyli budowania nowych klas na bazie już istniejących. Każda taka nowa klasa przejmuje zachowanie i właściwości klasy bazowej. Zostanie tu przedstawione, jak tworzy się klasy potomne, jakie podstawowe zależności występują między klasą bazową a potomną oraz jak zachowują się konstruktory w przypadku dziedziczenia. Dziedziczenie Na początku lekcji 13. powstała klasa Punkt, która przechowywała informację o współrzędnych punktu na płaszczyźnie. W trakcie dalszych ćwiczeń została rozbudowana o dodatkowe metody, które pozwalały na ustawianie i pobieranie tych współrzędnych. Zastanówmy się teraz, co należałoby zrobić, gdyby potrzebne było określenie położenia punktu nie w dwóch, ale w trzech wymiarach, czyli gdyby do współrzędnych x i y trzeba było dodać współrzędną z. Pomysłem, który od razu przychodzi do głowy, jest napisanie dodatkowej klasy, np. o nazwie Punkt3D, w postaci: class int int int } Punkt3D { x; y; z; Do tej klasy należałoby dalej dopisać pełny zestaw metod, które znajdowały się w klasie Punkt, takich jak pobierzX, pobierzY, ustawX, ustawY itd., oraz dodatkowe metody operujące na współrzędnej z. Zauważmy jednak, że w takiej sytuacji w dużej części po prostu powtarza się już raz napisany kod. Czym bowiem będzie się różniła metoda ustawX klasy Punkt od metody ustawX klasy Punkt3D? Oczywiście niczym. Po prostu Punkt3D jest pewnego rodzaju rozszerzeniem klasy Punkt. Rozszerza ją o dodatkowe możliwości (pola, metody), pozostawiając stare właściwości bez zmian. Zamiast więc pisać całkiem od nowa klasę Punkt3D, lepiej spowodować, aby przejęła ona wszystkie możliwości klasy Punkt, wprowadzając dodatkowo swoje własne pola i metody. Jest to tak zwane dziedziczenie, czyli jeden z fundamentów programowania obiektowego. Powiemy, że klasa Punkt3D dziedziczy po klasie Punkt9, czyli przejmuje jej pola i metody oraz dodaje swoje własne. Wtedy klasę Punkt nazwiemy klasą bazową (nadrzędną, nadklasą, ang. superclass), a klasę Punkt3D — klasą potomną (pochodną, podrzędną, podklasą, ang. subclass). W Javie dziedziczenie jest wyrażane za pomocą słowa extends, a cała definicja schematycznie wygląda następująco: 9 Często spotyka się również potoczną formę „dziedziczyć z”, np. „Klasa Punkt3D dziedziczy z klasy Punkt”. Rozdział 3. Programowanie obiektowe. Część I 123 class klasa_potomna extends klasa_bazowa { //wnętrze klasy } Taki zapis oznacza, że klasa potomna dziedziczy po klasie bazowej. Zobaczmy, jak taka deklaracja będzie wyglądała w praktyce dla wspomnianych klas Punkt i Punkt3D. Jest to bardzo proste: class Punkt3D extends Punkt { int z; } Taki zapis oznacza, że klasa Punkt3D przejęła wszystkie właściwości klasy Punkt, a dodatkowo otrzymała pole typu int o nazwie z. Przekonajmy się, że tak jest w istocie. Niech klasy Punkt i Punkt3D wyglądają tak jak na listingu 3.25 (najlepiej zapisać je w dwóch plikach: Punkt.java i Punkt3D.java). Listing 3.25. class Punkt { int x; int y; int pobierzX() { return x; } int pobierzY() { return y; } void ustawX(int wspX) { x = wspX; } void ustawY(int wspY) { y = wspY; } void ustawXY(int wspX, int wspY) { x = wspX; y = wspY; } void ustawXY(Punkt punkt) { x = punkt.x; y = punkt.y; } void wyswietlWspolrzedne() { System.out.println("współrzędna x = " + x); System.out.println("współrzędna y = " + y); } } class Punkt3D extends Punkt { int z; } Klasa Punkt ma tu postać znaną nam z wcześniej prezentowanych przykładów. Zawiera dwa pola, x i y, oraz sześć metod: pobierzX i pobierzY (zwracające współrzędne x i y), 124 Java. Praktyczny kurs ustawX, ustawY i ustawXY (ustawiające współrzędne) oraz wyswietlWspolrzedne (wyświetlającą wartości pól x i y na ekranie). Ponieważ klasa Punkt3D dziedziczy po klasie Punkt, również zawiera wymienione pola i metody oraz dodatkowo pole o nazwie z. Jeśli napiszemy teraz jeszcze klasę Main widoczną na listingu 3.26, testującą obiekt klasy Punkt3D, przekonamy się, że na takim obiekcie zadziałają wszystkie metody, które znajdowały się w klasie Punkt. Listing 3.26. class Main { public static void main (String args[]) { Punkt3D punkt = new Punkt3D(); System.out.println("x = " + punkt.x); System.out.println("y = " + punkt.y); System.out.println("z = " + punkt.z); System.out.println(""); punkt.ustawX(100); punkt.ustawY(200); System.out.println("x = " + punkt.x); System.out.println("y = " + punkt.y); System.out.println("z = " + punkt.z); System.out.println(""); punkt.ustawXY(300, 400); System.out.println("x = " + punkt.x); System.out.println("y = " + punkt.y); System.out.println("z = " + punkt.z); } } Na początku definiujemy zmienną klasy Punkt3D o nazwie punkt i przypisujemy jej nowo utworzony obiekt typu Punkt3D. Wykorzystujemy oczywiście dobrze nam znany operator new. Następnie wyświetlamy na ekranie wartości wszystkich pól tego obiektu. Wiadomo, że są to trzy pola, x, y, z, oraz że powinny one otrzymać wartości domyślne równe 0 (tabela 3.1). Następnie wykorzystujemy metody ustawX oraz ustawY, aby przypisać polom x i y wartości 100 oraz 200. W kolejnym kroku ponownie wyświetlamy zawartość wszystkich pól na ekranie. W dalszej części kodu wykorzystujemy metodę ustawXY do przypisania polu x wartości 300, a polu y wartości 400 i jeszcze raz wyświetlamy zawartość wszystkich pól na ekranie. Otrzymamy tym samym widok zaprezentowany na rysunku 3.14. Jest to też najlepszy dowód, że klasa Punkt3D rzeczywiście odziedziczyła wszystkie pola i metody zawarte w klasie Punkt. Klasa Punkt3D nie jest jednak w takiej postaci w pełni funkcjonalna, należałoby przecież dopisać metody operujące na nowym polu z. Na pewno przydatne będą ustawZ, pobierzZ oraz ustawXYZ. Oczywiście metoda ustawZ będzie przyjmowała jeden argument typu int i przypisywała jego wartość polu z, metoda pobierzZ będzie zwracała wartość Rozdział 3. Programowanie obiektowe. Część I 125 Rysunek 3.14. Klasa Punkt3D przejęła pola i metody klasy Punkt pola z, natomiast ustawXYZ będzie przyjmowała trzy argumenty typu int i przypisywała je polom x, y i z. Łatwo się domyślić, że metody te będą wyglądały tak, jak zaprezentowano na listingu 3.27. Można się również zastanowić nad dopisaniem metod analogicznych do ustawXY, czyli metod ustawXZ oraz ustawYZ, to jednak będzie dobrym ćwiczeniem do samodzielnego wykonania (podpunkt „Ćwiczenia do samodzielnego wykonania”). Listing 3.27. class Punkt3D extends Punkt { int z; void ustawZ(int wspZ) { z = wspZ; } int pobierzZ() { return z; } void ustawXYZ(int wspX, int wspY, int wspZ) { x = wspX; y = wspY; z = wspZ; } } Konstruktory klasy bazowej i potomnej Ostatnia wersja klasy Punkt3D, widoczna na listingu 3.27, ma już metody operujące na polu z, tzn. ustawiające oraz pobierające jego wartość, brakuje jej jednak konstruktorów. Przypomnijmy, że we wcześniejszych wersjach klasy Punkt zostały napisane aż trzy konstruktory: bezargumentowy, ustawiający wartość wszystkich pól na 1; dwuargumentowy, przyjmujący dwie wartości typu int; jednoargumentowy, przyjmujący obiekt klasy Punkt. 126 Java. Praktyczny kurs Co się stanie, jeśli dopiszemy je do kodu z listingu 3.25 i użyjemy razem z programem z listingu 3.26? Odpowiedni konstruktor z klasy Punkt zostanie wtedy wykonany (dzieje się tak dlatego, że obiekt klasy Punkt3D, można powiedzieć, zawiera w sobie obiekt klasy nadrzędnej Punkt). Niestety, co nie powinno być zaskoczeniem, żaden z konstruktorów klasy Punkt nie zajmuje się polem z, którego w klasie Punkt po prostu nie ma. Jeśli więc po opisanej zmianie skompilujemy i uruchomimy program testowy z listingu 3.26, przekonamy się, że tak jest w istocie. Widać to na rysunku 3.15. Pola x i y zostały zainicjowane wartościami 1, natomiast w polu z pozostała wartość domyślna 0. Rysunek 3.15. Efekt działania konstruktorów odziedziczonych po klasie Punkt Konstruktory dla klasy Punkt3D trzeba więc napisać samodzielnie. Nie jest to skomplikowane zadanie, konstruktory te są zaprezentowane na listingu 3.28. Listing 3.28. class Punkt3D extends Punkt { int z; Punkt3D() { x = 1; y = 1; z = 1; } Punkt3D(int wspX, int wspY, int wspZ) { x = wspX; y = wspY; z = wspZ; } Punkt3D(Punkt3D punkt) { x = punkt.x; y = punkt.y; z = punkt.z; } /* …pozostałe metody klasy Punkt3D… */ } Jak widać, pierwszy konstruktor nie przyjmuje żadnych argumentów i przypisuje wszystkim polom wartość 1. Drugi konstruktor przyjmuje trzy argumenty: wspX, wspY oraz wspZ, wszystkie typu int, i przypisuje otrzymane wartości polom x, y i z. Trzeci konstruktor otrzymuje jako argument obiekt klasy Punkt3D i kopiuje z niego wartości pól. Oczywiście pozostałe metody klasy Punkt3D pozostają bez zmian, nie zostały one uwzględnione na listingu, aby nie zaciemniać obrazu oraz nie marnować miejsca na stronie (są natomiast uwzględnione w listingach dostępnych na serwerze FTP). Rozdział 3. Programowanie obiektowe. Część I 127 Przyglądając się dokładnie napisanym przed chwilą konstruktorom, nie da się nie zauważyć, że ich kod w znacznej części dubluje się z kodem konstruktorów klasy Punkt. Dokładniej, są to te same instrukcje, uzupełnione dodatkowo o instrukcje operujące na wartościach pola z. Przecież konstruktory: Punkt3D(int wspX, int wspY, int wspZ) { x = wspX; y = wspY; z = wspZ; } oraz: Punkt(int wspX, int wspY) { x = wspX; y = wspY; } są prawie identyczne! Jedyna różnica to dodatkowy argument i dodatkowa instrukcja przypisująca jego wartość polu z. Czy nie lepiej byłoby zatem wykorzystać konstruktor klasy Punkt w klasie Punkt3D lub ogólniej — konstruktor klasy bazowej w konstruktorze klasy potomnej? Oczywiście, że tak. Nie można jednak wywołać konstruktora tak jak zwyczajnej metody, do tego celu służy specjalna konstrukcja. Dokładniej, w konstruktorze klasy potomnej należy wywołać metodę super() w postaci: class klasa_potomna extends klasa_bazowa { klasa_potomna(argumenty) { super(); /* …dalszy kod konstruktora… */ } } Ważne jest, aby metoda super(), czyli wywołanie konstruktora klasy bazowej, była pierwszą instrukcją konstruktora klasy potomnej. Próba umieszczenia tej instrukcji w innym miejscu spowoduje wygenerowanie błędu kompilacji. Jest on widoczny na rysunku 3.16. Rysunek 3.16. Metoda super musi być pierwszą instrukcją w konstruktorze klasy potomnej Oczywiście jeśli metodzie super zostaną przekazane argumenty, zostanie wywołany konstruktor klasy bazowej, który tym argumentom odpowiada. Do praktycznego zobrazowania takiej konstrukcji doskonale nadaje się klasa Punkt3D. Spójrzmy na listing 3.29. 128 Java. Praktyczny kurs Listing 3.29. class Punkt3D extends Punkt { int z; Punkt3D() { super(); z = 1; } Punkt3D(int wspX, int wspY, int wspZ) { super(wspX, wspY); z = wspZ; } Punkt3D(Punkt3D punkt) { super(punkt); z = punkt.z; } /* …pozostałe metody klasy Punkt3D… */ } W pierwszym konstruktorze wywołujemy bezargumentową metodę super, która powoduje wywołanie bezargumentowego konstruktora klasy bazowej. Taki konstruktor (bezargumentowy) istnieje w klasie Punkt, konstrukcja ta nie budzi więc żadnych wątpliwości. W konstruktorze drugim wywołujemy metodę super, przekazując jej dwa parametry typu int. Ponieważ w klasie Punkt istnieje konstruktor dwuargumentowy przyjmujący dwie wartości typu int, i ta konstrukcja jest jasna. Trzeci konstruktor przyjmuje jeden argument typu (klasy) Punkt3D, w klasie Punkt istnieje konstruktor przyjmujący jeden argument klasy… no właśnie, w klasie Punkt przecież wcale nie ma konstruktora, który przyjmowałby argument tego typu! Jest co prawda konstruktor: Punkt(Punkt punkt) { //instrukcje konstruktora } ale przecież przyjmuje on argument typu Punkt, a nie Punkt3D. Tymczasem klasa z listingu 3.29 kompiluje się bez żadnych problemów! Jak to możliwe? Przecież nie zgadzają się typy argumentów! Otóż okazuje się, że jeśli oczekiwany jest argument klasy X, a podany zostanie argument klasy Y, która jest klasą potomną dla X, błędu nie będzie. W takiej sytuacji nastąpi tak zwane rzutowanie typu obiektu, które zostanie omówione dokładniej dopiero w rozdziale 5. W tej chwili warto zapamiętać zasadę: w miejscu, gdzie powinien być zastosowany obiekt pewnej klasy X, można zastosować również obiekt klasy potomnej dla X10. 10 Istnieją jednak sytuacje, kiedy nie będzie to możliwe. Zajmiemy się dokładniej tym tematem w dalszej części książki. Rozdział 3. Programowanie obiektowe. Część I 129 Ćwiczenia do samodzielnego wykonania Ćwiczenie 16.1. Zmodyfikuj kod klasy Punkt z listingu 3.25 w taki sposób, aby nazwy argumentów w metodach ustawX, ustawY oraz ustawXY miały takie same nazwy jak nazwy pól, czyli x i y. Zatem nagłówki tych metod powinny wyglądać następująco: void ustawX(int x) void ustawY(int y) void ustawXY(int x, int y) Ćwiczenie 16.2. Dopisz do klasy Punkt3D zaprezentowanej na listingu 3.27 metodę ustawXZ oraz ustawYZ. Ćwiczenie 16.3. Napisz przykładową klasę Main wykorzystującą wszystkie trzy konstruktory klasy Punkt3D z listingu 3.28 (lub 3.29). Ćwiczenie 16.4. Zmodyfikuj kod z listingu 3.28 w taki sposób, aby w żadnym z konstruktorów nie występowało bezpośrednie przypisanie wartości do pól klasy. Zamiast tego w każdym konstruktorze wywołaj metodę ustawXYZ. Ćwiczenie 16.5. Napisz kod klasy KolorowyPunkt będącej rozszerzeniem klasy Punkt o informację o kolorze. Kolor ma być określany dodatkowym polem o nazwie kolor i typie int (lub innym). Dopisz metody ustawKolor i pobierzKolor, a także odpowiednie konstruktory. Ćwiczenie 16.6. Dopisz do klasy Punkt3D z listingu 3.29 konstruktor, który jako argument będzie przyjmował obiekt klasy Punkt. Wykorzystaj w tym konstruktorze metodę super. Lekcja 17. Specyfikatory dostępu i pakiety Specyfikatory dostępu pełnią ważną rolę w Javie, pozwalają bowiem na określenie praw dostępu do składowych klas, a także do samych klas. Występują one w czterech rodzajach, które zostaną przedstawione właśnie w tej lekcji. W jej drugiej części będzie natomiast mowa o pakietach, które można traktować jako biblioteki klas. Okaże się, co to znaczy, że klasa jest publiczna lub pakietowa, oraz jak tworzyć własne pakiety. 130 Java. Praktyczny kurs Publiczne, prywatne czy chronione? W Javie przed każdym polem i metodą może, a często wręcz powinien pojawiać się tak zwany specyfikator dostępu (używa się również terminu „modyfikator dostępu”, ang. access modifier). Dostęp do każdego pola i każdej metody może być: publiczny, prywatny, chroniony, pakietowy. Domyślnie, jeżeli przed składową klasy nie występuje żadne określenie, dostęp jest pakietowy, co oznacza, że dostęp do tej składowej mają wszystkie klasy wchodzące w skład danego pakietu (pakiety zostaną omówione w dalszej części lekcji). Dostęp publiczny jest określany słowem public, prywatny — słowem private, a chroniony — słowem protected. Dostęp publiczny — public Jeżeli dana składowa (pole lub metoda) klasy jest publiczna, oznacza to, że mają do niej dostęp wszystkie inne klasy, czyli dostęp ten nie jest w żaden sposób ograniczony. Weźmy np. pierwotną wersję klasy Punkt z listingu 3.1 (lekcja 13.). Gdyby pola x i y tej klasy miały być publiczne, musiałaby ona wyglądać tak jak na listingu 3.30. Listing 3.30. class Punkt { public int x; public int y; } Specyfikator dostępu należy zatem umieścić przed nazwą typu, co schematycznie wygląda następująco: specyfikator_dostępu nazwa_typu nazwa_pola Podobnie jest z metodami — specyfikator dostępu powinien być pierwszym elementem deklaracji, czyli ogólnie należy napisać: specyfikator_dostępu typ_zwracany nazwa_metody(argumenty) Przykładowo gdyby do klasy Punkt z listingu 3.30 dopisać publiczne metody pobierzX i pobierzY oraz ustawX i ustawY, miałaby ona postać przedstawioną na listingu 3.31. Listing 3.31. class Punkt { public int x; public int y; public int pobierzX() { Rozdział 3. Programowanie obiektowe. Część I 131 return x; } public int pobierzY() { return y; } public void ustawX(int wspX) { x = wspX; } public void ustawY(int wspY) { y = wspY; } } Dostęp prywatny — private Składowe oznaczone słowem private to takie, które są dostępne jedynie z wnętrza danej klasy. To znaczy, że wszystkie metody danej klasy mogą je dowolnie odczytywać i zapisywać, natomiast dostęp z zewnątrz jest zabroniony zarówno dla zapisu, jak i odczytu. Jeżeli zatem w klasie Punkt z listingu 3.30 oba pola zostaną ustawione jako prywatne, będzie ona miała postać widoczną na listingu 3.32. Listing 3.32. class Punkt { private int x; private int y; } O tym, że obydwa pola takiej klasy Punkt rzeczywiście są prywatne, przekonamy się, pisząc testową klasę Main, w której spróbujemy się odwołać do dowolnego z nich. Taka przykładowa klasa jest widoczna na listingu 3.33. Listing 3.33. class Main { public static void main (String args[]) { Punkt punkt = new Punkt(); punkt.x = 100; punkt.y = 200; System.out.println("punkt.x = " + punkt.x); System.out.println("punkt.y = " + punkt.y); } } Po utworzeniu nowego obiektu klasy Punkt przypisujemy go zmiennej punkt. Następnie próbujemy przypisać polom x i y tego obiektu wartości całkowite oraz wyświetlić je na ekranie. Już próba kompilacji udowodni, że nie można w klasie Main odwoływać się do pól x i y obiektu klasy Punkt, gdyż są one oznaczone jako prywatne. Wynikiem kompilacji będzie jedynie seria komunikatów o nieprawidłowych odwołaniach widoczna na rysunku 3.17. 132 Java. Praktyczny kurs Rysunek 3.17. Próba odwołania się do składowych prywatnych klasy Punkt W jaki zatem sposób odwołać się do pola prywatnego? Przypomnijmy sobie opis prywatnej składowej klasy: jest to taka składowa, która jest dostępna z wnętrza danej klasy, czyli dostęp do niej mają wszystkie metody tej klasy. Wystarczy więc napisać publiczne metody pobierające i ustawiające pola prywatne, a będzie można wykonywać na nich operacje. W przypadku klasy Punkt z listingu 3.32 byłyby niezbędne metody ustawX, ustawY oraz pobierzX i pobierzY. Taka klasa jest przedstawiona na listingu 3.34. Listing 3.34. class Punkt { private int x; private int y; public int pobierzX() { return x; } public int pobierzY() { return y; } public void ustawX(int wspX) { x = wspX; } public void ustawY(int wspY) { y = wspY; } } Te metody pozwolą już na bezproblemowe odwoływanie się do obu prywatnych pól. Teraz program z listingu 3.33 trzeba poprawić tak, aby wykorzystywał nowe metody, a zatem zamiast: punkt.x = 100; należy napisać: punkt.ustawX(100); Zamiast: System.out.println("punkt.x = " + punkt.x); Rozdział 3. Programowanie obiektowe. Część I 133 należy napisać: System.out.println("punkt.x = " + punkt.pobierzX()); Analogiczne konstrukcje należy zastosować przy dostępie do pola y. W tym miejscu warto też zwrócić uwagę, że składowe prywatne nie będą dostępne nawet dla klas potomnych. Jeżeli z klasy Punkt z listingu 3.32 wyprowadzimy klasę Punkt3D w postaci: class Punkt3D extends Punkt { private int z; } klasa ta nie będzie miała żadnego dostępu do pól x i y. Dostęp chroniony — protected Składowe klasy oznaczone słowem protected to składowe chronione. Są one dostępne jedynie dla metod danej klasy, klas potomnych oraz klas z tego samego pakietu (patrz podrozdział „Pakiety”). Oznacza to, że jeśli mamy przykładową klasę Punkt, w której znajdzie się chronione pole o nazwie x, to w klasie Punkt3D, o ile jest ona klasą pochodną od Punkt, również będzie można odwoływać się do pola x. Jednak dla każdej innej klasy, która nie dziedziczy po Punkt, pole x będzie niedostępne11. W praktyce klasa Punkt — z polami x i y zadeklarowanymi jako chronione — będzie wyglądała tak jak na listingu 3.35. Listing 3.35. class Punkt { protected int x; protected int y; } Jeśli teraz z klasy Punkt wyprowadzimy klasę Punkt3D w postaci widocznej na listingu 3.36, to będzie ona miała (inaczej niż w przypadku składowych prywatnych) pełny dostęp do składowych x i y klasy Punkt. Listing 3.36. class Punkt3D extends Punkt { protected int z; } Czemu ukrywamy wnętrze klasy? W tym miejscu pojawi się zapewne pytanie: czemu zabraniać przy użyciu modyfikatorów private i protected bezpośredniego dostępu do niektórych składowych klas? Otóż chodzi o ukrycie implementacji wnętrza klasy. Programista, projektując daną klasę, udostępnia na zewnątrz (innym programistom) pewien interfejs służący do posługiwania się jej obiektami. Określa więc sposób, w jaki można korzystać z danego obiektu. To, co 11 Chyba że jest to klasa dostępna w ramach tego samego pakietu co klasa Punkt. 134 Java. Praktyczny kurs znajduje się we wnętrzu, jest ukryte, dzięki temu można całkowicie zmienić wewnętrzną konstrukcję klasy, nie zmieniając zupełnie sposobu korzystania z niej. Mówimy wtedy o hermetyzacji lub enkapsulacji (ang. encapsulation). To, że takie podejście jest przydatne, można pokazać nawet na przykładzie tak prostej klasy, jaką jest nieśmiertelna klasa Punkt. Załóżmy, że ma ona postać widoczną na listingu 3.34. Pola x i y są ukryte, operacje na współrzędnych można wykonywać wyłącznie dzięki publicznym metodom: pobierzX, pobierzY, ustawX, ustawY. Program przedstawiony na listingu 3.37 będzie zatem działał poprawnie. Listing 3.37. class Main { public static void main (String args[]) { Punkt punkt = new Punkt(); punkt.ustawX(100); punkt.ustawY(200); System.out.println("punkt.x = " + punkt.pobierzX()); System.out.println("punkt.y = " + punkt.pobierzY()); } } Załóżmy teraz, że zaistniała konieczność (obojętnie z jakiego powodu) zmiany sposobu reprezentacji współrzędnych punktu na tak zwany układ biegunowy, w którym położenie punktu jest opisywane za pomocą dwóch parametrów: kąta α oraz odległości od początku układu współrzędnych (rysunek 3.18). W klasie Punkt nie będzie już pól x i y, więc przestaną mieć sens wszelkie odwołania do nich. Gdyby pola te były zadeklarowane jako publiczne, byłby to spory problem. Nie dość, że we wszystkich programach wykorzystujących klasę Punkt trzeba byłoby zmieniać odwołania, to dodatkowo w każdym takim miejscu należałoby dokonywać przeliczania współrzędnych. Jednak dzięki temu, że pola x i y są prywatne, a dostęp do nich odbywa się przez publiczne metody, wystarczy tylko odpowiednio zmienić interfejs klasy Punkt. Jak się za chwilę okaże, można całkowicie tę klasę przebudować, a korzystający z niej program z listingu 3.37 nie będzie wymagał nawet najmniejszej poprawki. Rysunek 3.18. Położenie punktu reprezentowane za pomocą współrzędnych biegunowych Rozdział 3. Programowanie obiektowe. Część I 135 Niewątpliwie trzeba zamienić pola x i y typu int na pola reprezentujące kąt i odległość (tzw. moduł). Kąt najlepiej reprezentować za pomocą jego funkcji trygonometrycznej — wybierzmy np. sinus. Nowe pola nazwijmy sinusalfa oraz modul. Zatem podstawowa wersja nowej klasy Punkt będzie miała postać: class Punkt { private double sinusalfa; private double modul; } Dopisać należy teraz wszystkie cztery metody. Aby to zrobić, niezbędne będą wzory przekształcające wartości współrzędnych kartezjańskich (tzn. współrzędne [x, y]) na układ biegunowy (czyli kąt i moduł) oraz wzory odwrotne, czyli przekształcające współrzędne biegunowe na kartezjańskie. Wyprowadzenie tych wzorów nie jest skomplikowane, wystarczy znajomość podstawowych funkcji trygonometrycznych oraz twierdzenia Pitagorasa. Ponieważ ta książka to kurs programowania w Javie, a nie lekcja matematyki, zostaną podane w gotowej postaci12. I tak (dla oznaczeń jak na rysunku 3.18, bez używania polskich liter): y modul sin() x modul 1 sin2 () oraz: modul x 2 y 2 sin() y modul Mając te dane, można przystąpić do napisania odpowiednich metod. W ramach treningu przeanalizujmy metodę pobierzY. Jej postać będzie następująca: public int pobierzY() { double y = modul * sinusalfa; return (int) y; } Deklarujemy zmienną y typu double i przypisujemy jej wynik mnożenia wartości pól modul oraz sinusalfa zgodnie z podanymi wyżej wzorami. Ponieważ metoda ma zwrócić wartość int, a wynikiem obliczeń jest wartość double, przed zwróceniem wyniku dokonujemy konwersji na typ int. Odpowiada za to konstrukcja (int) y (dokładniejszy opis tego typu konstrukcji zostanie przedstawiony w dalszej części książki). Analogicznie napiszemy metodę pobierzX, choć będzie trzeba wykonać nieco więcej obliczeń. Metoda ta przyjmie następującą postać: 12 W celu uniknięcia umieszczania w kodzie klasy dodatkowych instrukcji warunkowych zaciemniających sedno zagadnienia przedstawiony kod i wzory są poprawne dla dodatnich współrzędnych x. Uzupełnienie klasy Punkt w taki sposób, aby możliwe było korzystanie z dowolnych wartości x, można potraktować jako ćwiczenie do samodzielnego wykonania. 136 Java. Praktyczny kurs public int pobierzX() { double x = modul * Math.sqrt(1 - sinusalfa * sinusalfa); return (int) x; } Tym razem deklarujemy, podobnie jak w poprzednim przypadku, zmienną x typu double oraz przypisujemy jej wynik działania: modul * Math.sqrt(1 - sinusalfa * sinusalfa). Wyjaśnienia wymaga zapewne konstrukcja Math.sqrt — to standardowa metoda obliczająca pierwiastek kwadratowy z przekazanego jej argumentu (czyli np. wykonanie instrukcji Math.sqrt(4) da w wyniku 2). W naszym przypadku ten argument to 1 – sinusalfa * 2 sinusalfa, czyli 1 – sinusalfa , zgodnie z podanym wzorem na współrzędną x. Wykonujemy mnożenie zamiast potęgowania, gdyż jest ono po prostu szybsze i wygodniejsze. Pozostały jeszcze do napisania metody ustawX i ustawY. Pierwsza z nich będzie mieć następującą postać: public void ustawX(int wspX) { int x = wspX; int y = pobierzY(); modul = Math.sqrt(x * x + y * y); sinusalfa = y / modul; } Ponieważ zarówno parametr modul, jak i sinusalfa zależą od obu współrzędnych, musimy je najpierw uzyskać. Współrzędna x jest oczywiście przekazywana jako argument, natomiast y uzyskamy, wywołując odpowiednio przygotowaną metodę pobierzY. Dalsza część metody ustawX to wykonanie działań zgodnych z podanymi wzorami. Podobnie jak w przypadku pobierzX, zamiast potęgowania wykonujemy zwykłe mnożenie x * x i y * y. Metoda ustawY będzie miała prawie identyczną postać, z tą różnicą, że skoro będzie jej przekazywana wartość współrzędnej y, to trzeba uzyskać jedynie wartość x, czyli początkowe instrukcje będą wyglądały tak: int x = pobierzX(); int y = wspY; Kiedy złożymy wszystkie napisane do tej pory elementy w jedną całość, uzyskamy klasę Punkt w postaci widocznej na listingu 3.38. Teraz po uruchomieniu programu z listingu 3.37 będzie można się przekonać, że wynik jego działania z nową klasą Punkt jest dokładnie taki sam jak wtedy, gdy korzystaliśmy z jej poprzedniej postaci. Mimo całkowitej wymiany wnętrza klasy Punkt program działa tak, jakby nic się nie zmieniło (rysunek 3.19). Listing 3.38. class Punkt { private double sinusalfa; private double modul; public int pobierzY() { double y = modul * sinusalfa; return (int) y; } Rozdział 3. Programowanie obiektowe. Część I 137 Rysunek 3.19. Zmiana implementacji klasy Punkt nie wpłynęła na działanie programu z listingu 3.37 public int pobierzX() { double x = modul * Math.sqrt(1 - sinusalfa * sinusalfa); return (int) x; } public void ustawX(int wspX) { int x = wspX; int y = pobierzY(); modul = Math.sqrt(x * x + y * y); sinusalfa = y / modul; } public void ustawY(int wspY) { int x = pobierzX(); int y = wspY; modul = Math.sqrt(x * x + y * y); sinusalfa = y / modul; } } Pakiety Klasy w Javie są grupowane w jednostki nazywane pakietami. Pakiet to inaczej biblioteka, zestaw powiązanych ze sobą tematycznie klas. Wraz z JDK programista otrzymuje naprawdę pokaźną liczbę takich pakietów zawierających w sumie kilka (to nie pomyłka) tysięcy klas. Aby więc sprawnie programować w Javie, trzeba zapoznać się przynajmniej w podstawowym zakresie z jednostką biblioteczną, jaką jest pakiet. Tworzenie pakietów Do tworzenia pakietów służy słowo kluczowe package, po którym następuje nazwa pakietu zakończona znakiem średnika. Schematycznie wygląda to więc następująco: package nazwa_pakietu; Ten zapis musi być umieszczony na początku pliku, przed nim nie mogą znajdować się żadne inne instrukcje. Przed package mogą występować jedynie komentarze. Jeśli zatem przykładowa klasa Punkt miałaby znajdować się w pakiecie o nazwie grafika, plik powinien mieć następującą strukturę: package grafika; class Punkt { /* 138 Java. Praktyczny kurs …treść klasy Punkt… */ } Aby móc korzystać z obiektów takiej klasy Punkt w innych klasach (np. klasie Main), trzeba jej nazwę poprzedzić nazwą pakietu (pisząc grafika.Punkt, o czym dalej) lub użyć polecenia import w schematycznej postaci: import nazwa_pakietu.nazwa_klasy; przykładowy plik z klasą Main mógłby więc mieć następującą strukturę: import grafika.Punkt; class Main { /* …treść klasy Main… */ } Polecenie import grafika.Punkt mówi kompilatorowi, że będziemy korzystać z klasy Punkt znajdującej się w pakiecie grafika. Po jego zastosowaniu w klasie Main można odwoływać się bezpośrednio do klasy Punkt. Jeżeli jednak instrukcji import zabraknie, to w przypadku odwoływania się do klasy Punkt trzeba będzie podać pełną nazwę pakietową, tak więc utworzenie obiektu punkt klasy Punkt musiałoby wyglądać wtedy następująco: grafika.Punkt punkt = new grafika.Punkt(); Natomiast aby zaimportować wszystkie klasy z pakietu grafika, należy użyć polecenia import w postaci: import grafika.*; Jest ono często stosowane, nawet jeśli używa się tylko kilku klas. Jak nazywać pakiety? Pakiety są przydatne nie tylko ze względu na możliwość zgrupowania powiązanych tematycznie klas w jednostki biblioteczne. Pozwalają również na uniknięcie konfliktu nazw. Bardzo prawdopodobna jest przecież sytuacja, kiedy dwóch programistów utworzy klasy o tej samej nazwie. Bez wykorzystania techniki pakietów nie można byłoby skorzystać z obu tych klas naraz w programie. Wystąpiłby przecież konflikt nazw. Jeśli jednak zostaną one umieszczone w dwóch różnych pakietach, konfliktu nie będzie. Przyjmuje się, że nazwy pakietów piszemy w całości małymi literami, a jeśli pakiet ma być udostępniony publicznie, należy poprzedzić go odwróconą nazwą domeny13. To oczywiście nie jest obligatoryjne, ale pozwala na stworzenie, z dużym prawdopodobieństwem, nazwy unikalnej w skali globu. Jeśli zatem mamy przykładową domenę marcinlis.com i chcemy stworzyć pakiet o nazwie grafika, jego pełna nazwa powinna brzmieć com.marcinlis.grafika. Z kolei wszystkie klasy tego pakietu będą musiały być umieszczone w strukturze katalogów odpowiadających tej nazwie (rysunek 3.20). 13 Rzecz jasna, o ile posiadamy zarejestrowaną domenę internetową. Rozdział 3. Programowanie obiektowe. Część I 139 Rysunek 3.20. Struktura katalogów dla pakietu o nazwie com.marcinlis.grafika Należy więc zbudować drzewo katalogów o nazwach kolejnych poziomów: com, marcinlis, grafika. To jednak nie wszystko — przecież kompilator musi „wiedzieć”, w którym miejscu to drzewo się zaczyna. Na rysunku 3.20 widać, że wcześniej znajduje się jeszcze jeden katalog o nazwie java. Informację o tym należy przekazać, ustawiając zmienną środowiskową CLASSPATH. Musi ona zawierać oddzielone średnikami nazwy wszystkich katalogów, w których zaczynają się struktury pakietów. W omawianym przypadku, przy założeniu, że pracujemy w systemie Windows, a katalog java z rysunku 3.20 znajduje się na dysku C:, zmiennej CLASSPATH należy przypisać wartość: c:\java\ (na zasadach opisanych w rozdziale 1.). Korzystanie z pakietów Spróbujmy teraz wykonać poglądowy przykład pokazujący wykorzystanie dwóch klas o takich samych nazwach, ale znajdujących się w różnych pakietach. Obie będą przechowywały informacje o punktach, ale w przypadku jednej z nich będą to punkty na płaszczyźnie, a drugiej — w przestrzeni trójwymiarowej. Pierwsza z nich zostanie umieszczona w pakiecie o nazwie grafika2d, a druga — w pakiecie grafika3d. Obie klasy są widoczne na listingu 3.39. Listing 3.39. package grafika2d; public class Punkt { public int x; public int y; } package grafika3d; public class public int public int public int } Punkt { x; y; z; Oczywiście klasę Punkt z pakietu grafika2d umieszczamy w pliku o nazwie Punkt w katalogu grafika2d, a klasę Punkt z pakietu grafika3d — także w pliku Punkt, ale w katalogu o nazwie grafika3d. Ścieżka dostępu do obu katalogów musi być określona w zmiennej 140 Java. Praktyczny kurs środowiskowej CLASSPATH (zgodnie z podanymi wcześniej informacjami)14. Aby teraz w jednym programie skorzystać z obu klas, przy deklaracji niezbędne będzie podanie pełnej nazwy pakietowej dla klasy, zatem deklaracja przykładowej zmiennej i utworzenie obiektu dla klasy z pakietu grafika2d będą wyglądały tak: grafika2d.Punkt punkt2d = new grafika2d.Punkt(); natomiast dla pakietu grafika3d tak: grafika3d.Punkt punkt3d = new grafika3d.Punkt(); Przykładowa klasa Main wykorzystująca obie deklaracje jest zaprezentowana na listingu 3.40, a efekt jej działania widać na rysunku 3.21. Listing 3.40. class Main { public static void main (String args[]) { grafika2d.Punkt punkt2d = new grafika2d.Punkt(); grafika3d.Punkt punkt3d = new grafika3d.Punkt(); punkt2d.x = 100; punkt2d.y = 200; System.out.println("punkt2d.x = " + punkt2d.x); System.out.println("punkt2d.y = " + punkt2d.y); System.out.println(""); punkt3d.x = 110; punkt3d.y = 120; punkt3d.z = 130; System.out.println("punkt3d.x = " + punkt3d.x); System.out.println("punkt3d.y = " + punkt3d.y); System.out.println("punkt3d.y = " + punkt3d.z); } } Rysunek 3.21. Wykorzystanie obiektów dwóch różnych klas o tej samej nazwie Punkt 14 Jeśli nie chce się modyfikować zmiennej środowiskowej CLASSPATH, można umieścić katalogi z klasami pakietów w folderze, w którym znajduje się kod klas korzystających z tych pakietów. A zatem w omawianym przypadku katalogi grafika2d i grafika3d można umieścić w folderze, w którym znajduje się kod klasy Main. Rozdział 3. Programowanie obiektowe. Część I 141 Klasy publiczne i pakietowe Klasy w Javie można podzielić na trzy typy: publiczne, pakietowe, wewnętrzne. Klasy wewnętrzne zostaną omówione dopiero w rozdziale 5., teraz zajmiemy się jedynie klasami pakietowymi i publicznymi. Otóż jeśli przed nazwą klasy nie ma modyfikatora public, jest to klasa pakietowa, czyli dostępna jedynie dla innych klas z tego samego pakietu. Jeśli natomiast przed nazwą klasy znajduje się modyfikator public, jest to klasa publiczna, czyli dostępna dla wszystkich innych klas. Deklaracja klasy pakietowej ma więc postać: class nazwa_klasy { //pola i metody klasy } natomiast klasy publicznej: public class nazwa_klasy { //pola i metody klasy } Klasy z listingu 3.39 zostały zadeklarowane właśnie jako publiczne, inaczej program (dokładniej klasa Main) z listingu 3.40 nie miałby do nich dostępu i nie mógłby tworzyć obiektów obu klas Punkt. Można się o tym przekonać, zmieniając ich dostęp na pakietowy, czyli usuwając słowa public. Przy próbie kompilacji programu testowego pojawi się jedynie seria komunikatów o błędach. Trzeba jeszcze wyjaśnić jedną kwestię — otóż do tej pory trzymaliśmy się zasady, że w jednym pliku o rozszerzeniu java można umieścić tylko jedną klasę. To oczywiście nieprawda albo też nie cała prawda. Otóż ta zasada powinna brzmieć: w jednym pliku java (czyli w jednej jednostce kompilacji) może znaleźć się tylko jedna klasa o dostępie publicznym. Wynika z tego, że w takim pliku można umieścić dodatkowo klasy o dostępie pakietowym. I rzeczywiście tak jest. Po co zdefiniowano taką zasadę? Po to, aby umożliwić wprowadzenie klas pomocniczych, usługowych wobec klasy głównej. Takich klas pomocniczych nie udostępnia się publicznie, mają one jedynie współpracować z klasą główną. Umieszcza się je wtedy jako klasy pakietowe w tym samym pliku co klasę główną. Jeśli na przykład dopiszemy do klasy Punkt klasę usługową o nazwie Pomocnicza, plik Punkt.java będzie miał postać taką jak przedstawiona na listingu 3.41. Nie należy zapomnieć, że tym razem, inaczej niż w większości dotychczas przedstawionych przykładów, trzeba zapisać cały listing w jednym pliku o nazwie Punkt.java — oczywiście w podkatalogu grafika, czyli podkatalogu o nazwie zgodnej z nazwą pakietu. 142 Java. Praktyczny kurs Listing 3.41. package grafika; public class Punkt { int x; int y; Pomocnicza obiektPomocniczy; /* …dalsza część klasy Punkt… */ } class Pomocnicza { /* …pola i metody klasy Pomocnicza… */ } Cały listing trzeba zapisać w jednym pliku o nazwie Punkt.java. Ponieważ obie klasy należą do pakietu grafika, o czym informuje znajdująca się na początku deklaracja package grafika;, plik Punkt.java powinien znajdować się w podkatalogu grafika. Ścieżkę do tego katalogu należy zapisać w zmiennej środowiskowej CLASSPATH. Klasa Punkt jest klasą publiczną, zawiera trzy pola: dwa typu int i jedno typu Pomocnicza. Klasa Pomocnicza jest pakietowa i dostęp do niej jest możliwy wyłącznie dla klas z pakietu grafika, czyli w tym przypadku jedynie dla klasy Punkt (w pakiecie grafika nie ma bowiem żadnych innych klas). W wyniku skompilowania pliku Punkt.java powstaną dwa pliki class: Punkt.class oraz Pomocnicza.class. Do klasy Pomocnicza będzie się jednak można odwołać jedynie z wnętrza klasy Punkt. Testowa klasa Main, której kod jest widoczny na listingu 3.42, nie będzie miała dostępu do klasy Pomocnicza, nie będzie więc mogła tworzyć jej obiektów. Próba kompilacji15 tego kodu skończy się komunikatami o błędach widocznymi na rysunku 3.22. Kiedy jednak usunięte zostaną odwołania do klasy Pomocnicza, kompilacja przebiegnie bez problemów. Listing 3.42. import grafika.*; class Main { public static void main (String args[]) { Punkt punkt = new Punkt(); Pomocnicza pomocnicza = new Pomocnicza(); } } 15 W celu uproszczenia zadania można podkatalog grafika umieścić bezpośrednio w katalogu, w którym znajduje się plik Main.java. Nie trzeba wtedy modyfikować zmiennej CLASSPATH. Rozdział 3. Programowanie obiektowe. Część I 143 Rysunek 3.22. Próba odwołania się do klasy o dostępie pakietowym kończy się błędami kompilacji Warto zatem podkreślić jeszcze raz: publiczna klasa Punkt znajdująca się w pakiecie grafika ma pełny dostęp do pakietowej klasy Pomocnicza (obie znajdują się wręcz w tym samym pliku), jednak klasa Main (spoza pakietu grafika) nie może odwoływać się do klasy Pomocnicza. W klasie Main wolno jedynie tworzyć obiekty publicznej klasy Punkt. Oczywiście dostęp do pakietu jest możliwy dzięki poleceniu import grafika.*. Można zadać sobie pytanie, jakiego rodzaju w takim razie były klasy Punkt i Main, których używaliśmy, począwszy od lekcji 13. W pliku Punkt.java umieszczany był kod klasy Punkt w postaci: class Punkt { /* …treść klasy Punkt… */ } natomiast w pliku Main.java kod klasy Main w postaci: class Main { /* …treść klasy Main… */ } A zatem nie było ani modyfikatora public, ani też słowa package. Czyżby nie były one ani publiczne, ani pakietowe? Oczywiście nie. Obie klasy w takiej postaci były klasami pakietowymi. Wskazówką jest to, że znajdowały się w jednym katalogu. Kompilator tworzył więc tymczasowy pakiet domyślny, niemający nazwy, do którego zaliczał obie klasy. Dlatego też możliwa była kompilacja i wykonanie prostych przykładowych programów. Ćwiczenia do samodzielnego wykonania Ćwiczenie 17.1. Napisz klasę Punkt3D dziedziczącą po klasie Punkt zaprezentowanej na listingu 3.35, taką, że pole z będzie polem publicznym. Zastanów się, do których pól klasy 3D będzie można się odwoływać z innych klas oraz czy taka konstrukcja będzie miała praktyczny sens. 144 Java. Praktyczny kurs Ćwiczenie 17.2. Napisz wersję klasy Punkt, w której pola x i y będą przechowywane w tablicy. Klasa powinna współpracować z klasą Main z listingu 3.37. Zastanów się, jakie składowe klasy będą niezbędne i jakich modyfikatorów dostępu należy użyć. Ćwiczenie 17.3. Utwórz pakiet o nazwie biblioteka, umieść w nim dwie przykładowe klasy: Nowela i Powiesc. Napisz dodatkową publiczną klasę Main, która będzie się odwoływała do obiektów tych klas. Pamiętaj o użyciu instrukcji import. Ćwiczenie 17.4. Przygotuj taką strukturę klas, aby w jednym programie można było używać trzech wersji klasy Punkt: z listingu 3.34, z listingu 3.38 i z ćwiczenia 17.2 (wszystkie wersje mają mieć nazwę Punkt; możesz zmienić typ klasy z pakietowej na publiczną lub odwrotnie). Napisz przykładowy program, w którym powstaną obiekty tych trzech różnych klas. Ćwiczenie 17.5. Utwórz pakiet o nazwie mojpakiet. Umieść w nim klasę o dostępie publicznym i nazwie MojaKlasa. W tej klasie zadeklaruj dwa pola typu int o nazwach liczba1 i liczba2. Pierwsze z nich ma być polem publicznym, drugie polem pakietowym. Zastanów się, czy będzie można w dowolnej klasie publicznej tworzyć obiekty klasy MojaKlasa, a jeśli tak, do których pól tej klasy będzie można się swobodnie odwoływać. Lekcja 18. Przesłanianie metod i składowe statyczne W lekcji 14. była mowa o przeciążaniu metod, teraz omówimy, w jaki sposób dziedziczenie wpływa na przeciążanie, oraz przeanalizujemy technikę przesłaniania pól i metod. Technika ta pozwala na uzyskanie bardzo ciekawego efektu umieszczenia składowych o identycznych nazwach zarówno w klasie bazowej, jak i potomnej. Drugim poruszanym tematem będą składowe statyczne, czyli takie, które mogą istnieć nawet wtedy, kiedy nie istnieją obiekty danej klasy. Przeciążanie metod a dziedziczenie Przeciążanie metod, czyli możliwość umieszczenia w jednej klasie kilku metod o tej samej nazwie, różniących się argumentami, było omawiane w lekcji 14. Pytanie: jak ma się to do dziedziczenia, czyli czy można przeciążać metody klasy bazowej w klasie potomnej? Odpowiedź brzmi — tak. Jeśli mamy np. klasę o nazwie A, a w niej bezargumentową metodę f (czyli klasę w postaci widocznej na listingu 3.43): Rozdział 3. Programowanie obiektowe. Część I 145 Listing 3.43. public class A { public void f() { System.out.println("Metoda f() z klasy A"); } } i z niej wyprowadzimy klasę pochodną o nazwie B, to w klasie B da się zdefiniować metodę f np. przyjmującą jeden argument typu int, tak jak jest to widoczne na listingu 3.44. Listing 3.44. public class B extends A { public void f(int liczba) { System.out.println("Metoda f(int) z klasy B"); } } Zatem w klasie A została zdefiniowana bezargumentowa metoda o nazwie f. Jej zadaniem jest wyświetlenie na ekranie nazwy klasy, w której została zdefiniowana. W klasie B dziedziczącej po klasie A również została zdefiniowana metoda o nazwie f, ale przyjmująca jeden argument typu int. Jedynym zadaniem metody f z klasy B jest również wypisanie na ekranie nazwy klasy, w której została zdefiniowana. Po umieszczeniu obu klas w jednym katalogu można je bez problemu skompilować, a prosty program testowy widoczny na listingu 3.45 pozwala przekonać się, że działanie będzie takie samo jak wtedy, gdy wszystko odbywa się w jednej klasie (przykłady z lekcji 14.). Listing 3.45. public class Main { public static void main (String args[]) { A a = new A(); B b = new B(); //prawidłowe wywołanie, w klasie A istnieje metoda f() a.f(); //nieprawidłowe wywołanie, w klasie A nie ma metody f(int) //a.f(0); //oba wywołania prawidłowe, w klasie B istnieją metody //f() i f(int) b.f(); b.f(0); } } Definiujemy tu dwa obiekty, a klasy A oraz b klasy B. Następnie wywołujemy w kilku wariantach metodę f. Wywołanie pierwsze — a.f() — jest prawidłowe, obiekt a jest typu (klasy) A, a więc zawiera bezargumentową metodę f. Na ekranie zostanie zatem wyświetlony tekst Metoda f() z klasy A. Wywołanie drugie — a.f(0) — zostało ujęte 146 Java. Praktyczny kurs w komentarz, gdyż jest oczywiście nieprawidłowe. W obiekcie klasy A nie ma bowiem metody o nazwie f, która przyjmowałaby argument typu int. Wywołania trzecie — b.f() — oraz czwarte — b.f(0) — są prawidłowe. W obiekcie klasy B istnieje zarówno bezargumentowa metoda f odziedziczona po klasie A (wywołanie b.f() wyświetli zatem na ekranie napis Metoda f() z klasy A), jak i metoda o nazwie f (zdefiniowana w klasie B), która przyjmuje argument typu int (czyli wywołanie b.f(0) wyświetli na ekranie napis Metoda f(int) z klasy B). Ostatecznie w wyniku uruchomienia klasy Main na ekranie pojawią się napisy widoczne na rysunku 3.23. Rysunek 3.23. Ilustracja mechanizmu przeciążania metod w klasach potomnych Przesłanianie pól i metod Przesłanianie metod Wiadomo już, że w klasach potomnych można przeciążać metody zdefiniowane w klasie bazowej, jest to wręcz zgodne z logiką i intuicją. Byłoby to dziwne, gdyby zabrakło takiej możliwości. Co się jednak stanie, kiedy w klasie potomnej ponownie zdefiniujemy metodę o takiej samej nazwie i takich samych argumentach jak w klasie bazowej? Albo inaczej: jakiego zachowania metod należałoby się spodziewać w przypadku klas przedstawionych na listingu 3.46 (klasy należy zapisać w jednym katalogu w dwóch plikach: A.java i B.java)? Listing 3.46. public class A { public void f() { System.out.println("Metoda f z klasy A"); } } public class B extends A { public void f() { System.out.println("Metoda f z klasy B"); } } W klasie A znajduje się bezargumentowa metoda o nazwie f wyświetlająca na ekranie tekst z nazwą klasy, w której została zdefiniowana. Klasa B dziedziczy po klasie A (jest klasą pochodną od A), zgodnie z zasadami dziedziczenia przejmuje więc metodę f z klasy A. Tymczasem w klasie B została ponownie zadeklarowana bezargumentowa metoda f (również wyświetlająca nazwę klasy, w której została zdefiniowana, czyli tym razem klasy B). Wydawać by się mogło, że w takim wypadku wystąpi konflikt Rozdział 3. Programowanie obiektowe. Część I 147 nazw (dwukrotne zadeklarowanie metody f), jednak próba kompilacji wykaże, że kompilator nie zgłasza żadnych błędów. Czemu konflikt nazw nie występuje? Otóż zasada jest następująca: jeśli w klasie bazowej i pochodnej są metody o tej samej nazwie i argumentach, metoda z klasy bazowej jest przesłaniana. Tę technikę nazywamy przesłanianiem metod (przykrywaniem, nadpisywaniem16, ang. overriding). W obiektach klasy bazowej będzie zatem obowiązywała metoda z klasy bazowej, a w obiektach klasy pochodnej — metoda z klasy pochodnej. Sprawdźmy to na przykładzie klas z listingu 3.46. Co pojawi się na ekranie po uruchomieniu klasy Main z listingu 3.47, która korzysta z obiektów klas A i B? Oczywiście najpierw tekst Metoda f z klasy A, a następnie Metoda f z klasy B. Skoro bowiem obiekt a jest typu A, to wywołanie a.f() powoduje uruchomienie metody f z klasy A, czyli wyświetlenie tekstu Metoda f z klasy A. Z kolei typem obiektu b jest B, zatem wywołanie b.f() powoduje uruchomienie metody f z klasy B i wyświetlenie na ekranie tekstu Metoda f z klasy B (rysunek 3.24). Listing 3.47. public class Main { public static void main (String args[]) { A a = new A(); B b = new B(); a.f(); b.f(); } } Rysunek 3.24. Efekt wywoływania przesłoniętych metod Może się w tym miejscu pojawić pytanie, czy jest w takim razie możliwe wywołanie w klasie pochodnej przesłoniętej metody z klasy bazowej. Ten problem może się wydawać zawiły. Spójrzmy na przykład klas z listingu 3.46 — chodzi o to, czy w obiekcie klasy B można wywołać metodę f pochodzącą z klasy A. Nie jest to zagadnienie czysto teoretyczne, gdyż w praktyce programistycznej takie odwołania często upraszczają kod i ułatwiają tworzenie spójnych hierarchii klas. Skoro tak, to takie odwołanie oczywiście jest możliwe. Jak wiadomo z lekcji 16., jeśli trzeba było wywołać konstruktor klasy bazowej, wystarczyło skorzystać ze słowa super. W tym przypadku jest dokładnie tak samo. Odwołanie do przesłoniętej metody klasy bazowej uzyskujemy dzięki wywołaniu w schematycznej postaci: super.nazwa_metody(argumenty); 16 Akurat ten termin nie oddaje w pełni zasady działania omawianego mechanizmu. 148 Java. Praktyczny kurs Wywołanie takie najczęściej stosuje się w metodzie przesłaniającej (np. metodzie f klasy B), ale możliwe jest ono również w dowolnej innej metodzie klasy pochodnej. Jeśli zatem przykładowe klasy A i B wyglądałyby jak na listingu 3.48, wykonanie kodu klasy Main widocznego na listingu 3.49 dałoby wynik jak na rysunku 3.25. Listing 3.48. public class A { public void f() { System.out.println("Klasa A"); } } public class B extends A { //ta metoda przesłania metodę f z klasy A, //ale wywołuje również przesłoniętą metodę f //z klasy A public void f() { super.f(); System.out.println("Klasa B"); } //ta metoda wywołuje metodę f z klasy B public void g() { f(); } //ta metoda wywołuje metodę f z klasy A public void h() { super.f(); } } Listing 3.49. public class Main { public static void main (String args[]) { A a = new A(); B b = new B(); System.out.println("Wynik wywołania a.f():"); a.f(); System.out.println("\nWynik wywołania b.f():"); b.f(); System.out.println("\nWynik wywołania b.g():"); b.g(); System.out.println("\nWynik wywołania b.h():"); b.h(); } } Rozdział 3. Programowanie obiektowe. Część I 149 Rysunek 3.25. Ilustracja działania techniki przesłaniania metod w klasach potomnych Przeanalizujmy zasadę działania tego zestawu klas i zastanówmy się, czemu rezultat jest właśnie taki. Klasa A ma postać znaną nam z wcześniejszych przykładów. Została w niej zdefiniowana metoda f, której jedynym zadaniem jest wyświetlenie nazwy klasy na ekranie. Klasa B jest klasą pochodną od A, czyli dziedziczy po A. Zdefiniowano w niej metodę f, która przesłania metodę f z klasy A. Jednakże w tej metodzie jest wywoływana, za pomocą składni super, metoda przesłonięta. Dopiero po tym wywołaniu na ekranie zostaje wyświetlona nazwa klasy B. W klasie B występują jeszcze dodatkowo dwie inne metody, g i h. Pierwsza z nich wywołuje w standardowy sposób metodę f (czyli będzie to metoda f z klasy B), druga korzysta ze składni super w postaci: super.f(), zatem wywołuje metodę f zdefiniowaną w klasie A. Trzecia klasa — Main — jest klasą testową. Tworzony jest w niej jeden obiekt o nazwie a (typu A) i jeden o nazwie b (typu B). Następnie jest wywoływana metoda f obiektu klasy A, zatem na ekranie pojawia się napis Klasa A. W kolejnym kroku jest wywoływana metoda f obiektu b. Ponieważ najpierw wywołuje ona metodę f klasy A, a dopiero potem wyświetla na ekranie nazwę klasy B, wynikiem jej działania są dwa teksty: Klasa A i Klasa B. Krok trzeci to wywołanie metody g obiektu b, której jedynym zadaniem jest wywołanie metody f z klasy B. Zatem zachowanie będzie takie samo jak w kroku drugim: na ekranie pojawią się napisy Klasa A i Klasa B. W kroku ostatnim — czwartym — jest wywoływana metoda h obiektu b. Jest w niej uruchamiana, przy użyciu składni super, metoda f z klasy A, zatem na ekranie pojawia się tekst Klasa A. Ostatecznie otrzymywany jest wynik widoczny na rysunku 3.25. Przesłanianie pól Pola klas bazowych są przesłaniane w sposób analogiczny do wykorzystywanego w przypadku metod. Jeśli więc w klasie pochodnej zdefiniowane zostanie pole o takiej samej nazwie jak w klasie bazowej, bezpośrednio dostępne będzie tylko pole klasy pochodnej. Taka sytuacja została zobrazowana na listingu 3.50. 150 Java. Praktyczny kurs Listing 3.50. public class A { public int liczba; } public class B extends A { public int liczba; } W klasie A zostało zdefiniowane pole o nazwie liczba i typie int. W klasie B, która dziedziczy po A, ponownie zostało zadeklarowane pole o takiej samej nazwie i identycznym typie. Trzeba sobie jednak dobrze uświadomić, że każdy obiekt klasy B będzie w takiej sytuacji zawierał DWA pola o nazwie liczba — jedno pochodzące z klasy A, drugie z B. Tym polom można z kolei przypisywać różne wartości. Jeśli mamy obiekt takiej klasy B, to z dowolnej klasy zewnętrznej jesteśmy w stanie dostać się jedynie do pola liczba zdefiniowanego w klasie B. Jednak już z wnętrza klasy B za pomocą składni super możemy odwołać się również do drugiego pola liczba (pochodzącego z klasy A). Taka sytuacja została przedstawiona na listingu 3.51. Listing 3.51. public class A { public int liczba; } public class B extends A { public int liczba; public void ustawLiczbaA(int liczba) { super.liczba = liczba; } public void ustawLiczbaB(int liczba) { this.liczba = liczba; } public int pobierzLiczbaA() { return super.liczba; } public int pobierzLiczbaB() { return liczba; } } Do klasy B zostały dopisane cztery metody operujące na obydwu polach liczba. Metody ustawLiczbaB i pobierzLiczbaB operują na polu liczba w sposób standardowy (czyli odwołują się do pola pochodzącego z klasy B). Pierwsza z nich pozwala na jego ustawianie, druga zwraca jego aktualną wartość. Dwie pozostałe metody, czyli ustawLiczbaA i pobierzLiczbaA, operują na polu liczba odziedziczonym po klasie A. Aby odwołać się do tego pola, wykorzystują składnię super, czyli odwołanie wygląda następująco: super.liczba Rozdział 3. Programowanie obiektowe. Część I 151 Możemy teraz napisać klasę testową Main, która udowodni, że obiekt klasy B naprawdę zawiera dwa pola o takiej samej nazwie. Taka klasa Main jest zaprezentowana na listingu 3.52. Listing 3.52. public class Main { public static void main (String args[]) { B b = new B(); b.ustawLiczbaA(0); b.ustawLiczbaB(1); System.out.print("Wartość przesłoniętego pola liczba: "); System.out.println(b.pobierzLiczbaA()); System.out.print("Wartość pola liczba z klasy B: "); System.out.println(b.pobierzLiczbaB()); } } Tworzymy tu obiekt b klasy B. Następnie wywołujemy metodę ustawLiczbaA, która zapisze przekazany jej argument w polu liczba pochodzącym z klasy A, oraz metodę ustawLiczbaB, która zapisze przekazany jej argument w polu liczba pochodzącym z klasy B. Dalej wyświetlamy zawartość obu pól na ekranie (wykorzystując metody pobierzLiczbaA i pobierzLiczbaB). Na rysunku 3.26 został przedstawiony wynik działania tego programu. Widać wyraźnie, że obiekt b przechował obie wartości, zatem rzeczywiście znajdują się w nim dwie wersje pola o nazwie liczba. Rysunek 3.26. Ilustracja przesłaniania pól klasy bazowej Warto tu zauważyć, że pole przesłaniające nie musi być tego samego typu co przesłaniane. Tak więc może istnieć w klasie A pole o nazwie pole1 i typie int, a w dziedziczącej po niej klasie B pole o nazwie pole1, ale typie np. char. Taka sytuacja została przedstawiona na listingu 3.53. Podobnych konstrukcji należy jednak zdecydowanie unikać — nie ma zwykle praktycznej potrzeby stosowania tego typu techniki, wprowadza ona bowiem niepotrzebne zamieszanie w budowie klas. Listing 3.53. public class A { protected int pole1; } public class B extends A { protected char pole1; } 152 Java. Praktyczny kurs Składowe statyczne Składowe statyczne to pola i metody klasy, które mogą istnieć, nawet jeśli nie istnieje obiekt tej klasy. Takie metody lub pola są wspólne dla wszystkich obiektów danej klasy. Składowe te są oznaczane słowem static. W dotychczasowych przykładach wykorzystywana była jedna metoda tego typu — main, od której rozpoczyna się wykonywanie programu. Metody statyczne Metodę statyczną oznacza się słowem static, które zwyczajowo powinno znaleźć się zaraz za specyfikatorem dostępu17, zatem schematycznie deklaracja metody statycznej będzie wyglądała następująco: specyfikator_dostępu static typ_zwracany nazwa_metody(argumenty) { //treść metody } Przykładowa klasa z zadeklarowaną metodą statyczną może wyglądać tak jak na listingu 3.54. Listing 3.54. public class A { public static void f() { System.out.println("Metoda f klasy A"); } } Tak napisaną metodę można wywołać klasycznie, to znaczy po utworzeniu obiektu klasy A, np. w postaci: A a = new A(); a.f(); Ten sposób jest nam doskonale znany — dotychczas wszystkie metody były wywoływane za pomocą tej techniki: nazwa_obiektu.nazwa_metody(argumenty metody); Ponieważ jednak metody statyczne istnieją nawet wtedy, kiedy nie ma żadnego obiektu danej klasy, jest możliwe wywołanie w postaci: nazwa_klasy.nazwa_metody(argumenty metody); W przypadku klasy A wywołanie tego typu miałoby następującą postać: A.f(); 17 W rzeczywistości słowo kluczowe static może pojawić się również przed specyfikatorem dostępu, ta kolejność nie jest bowiem istotna z perspektywy kompilatora. Przyjmuje się jednak, że — ze względu na ujednolicenie notacji — o ile występuje specyfikator dostępu metody, słowo static powinno znaleźć się za nim; przykładowo: public static void main, a nie static public void main. Rozdział 3. Programowanie obiektowe. Część I 153 Na listingu 3.55 jest przedstawiona przykładowa klasa Main, która korzysta z takiego wywołania. Uruchomienie tego kodu pozwoli się przekonać, że w przypadku metody statycznej rzeczywiście nie trzeba tworzyć obiektu. Listing 3.55. public class Main { public static void main (String args[]) { A.f(); } } Dlatego też metoda main, od której rozpoczyna się wykonywanie kodu klasy, jest metodą statyczną, może bowiem zostać wykonana, mimo że w trakcie uruchamiania aplikacji nie powstały jeszcze żadne obiekty. Należy jednak zdawać sobie sprawę, że metoda statyczna jest umieszczana w specjalnie zarezerwowanym do tego celu obszarze pamięci i jeśli powstaną obiekty danej klasy, będzie ona dla nich wspólna. To znaczy, że dla każdego obiektu klasy nie tworzy się kopii metody statycznej. Najłatwiej będzie jednak pokazać to na przykładzie pól statycznych. Pola statyczne Do pól oznaczonych jako statyczne można się odwoływać, podobnie jak w przypadku metod statycznych, nawet jeśli nie istnieje żaden obiekt danej klasy. Pola takie deklarujemy, umieszczając przed typem słowo static. Schematycznie deklaracja taka wygląda następująco: static typ_pola nazwa_pola; lub: specyfikator_dostępu static typ_pola nazwa_pola; Jeśli zatem w naszej przykładowej klasie A ma się pojawić pole statyczne o nazwie liczba typu int o dostępie publicznym, taka klasa będzie miała postać widoczną na listingu 3.5618. Listing 3.56. public class A { public static int liczba; } 18 Podobnie jak w przypadku metod statycznych, słowo static może się znaleźć przed specyfikatorem dostępu, czyli przykładowo: static public int liczba. Jednak dla ujednolicenia notacji będziemy konsekwentnie stosować formę zaprezentowaną w powyższym akapicie, czyli public static int liczba. 154 Java. Praktyczny kurs Do pól statycznych odwołujemy się w sposób klasyczny, tak jak do innych pól klasy — poprzedzając je nazwą obiektu (oczywiście jeśli wcześniej stworzymy dany obiekt), czyli stosując konstrukcję: nazwa_obiektu.nazwa_pola bądź też poprzedzając je nazwą klasy: nazwa_klasy.nazwa_pola Podobnie jak metody statyczne, również pola tego typu znajdują się w wyznaczonym obszarze pamięci i są wspólne dla wszystkich obiektów danej klasy. Tak więc niezależnie od liczby obiektów danej klasy pole statyczne o danej nazwie będzie tylko jedno. Przykład ilustrujący to zagadnienie jest widoczny na listingu 3.57 (korzystamy w nim z klasy A z listingu 3.56). Listing 3.57. public class Main { public static void main (String args[]) { //krok 1 A.liczba = 100; System.out.println("-=- Po przypisaniu: A.liczba = 100 -=-"); System.out.println("A.liczba: " + A.liczba); System.out.println(""); //krok 2 A obiekt1 = new A(); A obiekt2 = new A(); System.out.println("-=- Po utworzeniu obiektów -=-"); System.out.println("obiekt1.liczba: " + obiekt1.liczba); System.out.println("obiekt1.liczba: " + obiekt2.liczba); System.out.println(""); //krok 3 obiekt1.liczba = 200; System.out.println("-=- Po przypisaniu: obiekt1.liczba = 200 -=-"); System.out.println("obiekt1.liczba: " + obiekt1.liczba); System.out.println("obiekt1.liczba: " + obiekt2.liczba); System.out.println(""); //krok 4 obiekt2.liczba = 300; System.out.println("-=- Po przypisaniu: obiekt2.liczba = 300 -=-"); System.out.println("obiekt1.liczba: " + obiekt1.liczba); System.out.println("obiekt1.liczba: " + obiekt2.liczba); } } W pierwszym kroku przypisujemy polu statycznemu liczba z klasy A wartość 100. Stosujemy odwołanie w postaci nazwa_klasy.nazwa_pola = wartość, czyli A.liczba = 100. Można tu zauważyć, że nie powstał jeszcze żaden obiekt klasy A, tymczasem polu Rozdział 3. Programowanie obiektowe. Część I 155 liczba faktycznie została przypisana wartość 100 (wyświetlamy ją przecież na ekranie). To najlepszy dowód na to, że pole statyczne istnieje niezależnie od obiektów danej klasy. W drugim kroku tworzymy dwa obiekty klasy A i przypisujemy je zmiennym obiekt1 i obiekt2. Następnie wyświetlamy zawartość pola liczba za pomocą odwołań obiekt1.liczba oraz obiekt2.liczba. To kolejny dowód na to, że pole to jest niezależne od obiektów. Po ich utworzeniu nie przypisywaliśmy polu żadnej wartości, gdy tymczasem ma ono wartość 100, czyli przypisaną w kroku pierwszym. Krok trzeci to wykonanie instrukcji obiekt1.liczba = 200, czyli przypisanie polu liczba wartości 200. I tym razem pozwala to przekonać się, że jest tylko jedno takie pole mimo istnienia dwóch obiektów klasy A. Zarówno odwołanie w postaci obiekt1.liczba, jak i obiekt2.liczba daje bowiem w wyniku wartość 200. Krok czwarty to już tylko formalność. Tym razem przypisujemy polu liczba wartość 300, stosując odwołanie w postaci obiekt2.liczba = 300. Oczywiste jest, że i tym razem efekt będzie analogiczny do uzyskanego w poprzednich przypadkach. Tak więc odczytanie wartości zarówno za pomocą konstrukcji obiekt1.liczba, jak i obiekt2.liczba da w wyniku wartość 300. Nic dziwnego, skoro istnieje tylko jedno pole liczba, po prostu odwołujemy się do niego w różny sposób. Ostatecznie otrzymamy wynik widoczny na rysunku 3.27. Rysunek 3.27. Różne typy odwołań do pola statycznego z klasy A Ćwiczenia do samodzielnego wykonania Ćwiczenie 18.1. Napisz klasę Punkt3D dziedziczącą po klasie Punkt zaprezentowanej na listingu 3.3 (dostęp do klasy i do składowych zmień na publiczny). Zdefiniuj w niej pole typu int o nazwie z oraz metodę wyswietlWspolrzedne przesłaniającą metodę wyswietlWspolrzedne z klasy Punkt. 156 Java. Praktyczny kurs Ćwiczenie 18.2. Napisz klasę Opakowanie zawierającą pole typu int o nazwie liczba oraz klasę Nowe Opakowanie dziedziczącą po Opakowanie i również zawierającą pole liczba. W klasie NoweOpakowanie zdefiniuj metodę pobierzWartosc przyjmującą jeden argument typu boolean. Jeśli wartością argumentu będzie true, metoda ta ma zwracać wartość pola liczba zdefiniowanego w klasie NoweOpakowanie, a jeśli tą wartością będzie false, ma ona zwrócić wartość pola liczba odziedziczonego po klasie Opakowanie. Ćwiczenie 18.3. Do klasy NoweOpakowanie z ćwiczenia 18.2 dopisz metodę ustawWartosc przyjmującą dwa argumenty: pierwszy typu int, drugi typu boolean. Jeśli wartością drugiego argumentu będzie true, wartość pierwszego przypisz polu liczba zdefiniowanemu w klasie NoweOpakowanie, jeśli natomiast wartością drugiego argumentu będzie false, wartość pierwszego przypisz polu liczba zdefiniowanemu w klasie Opakowanie. Ćwiczenie 18.4. Napisz klasę Dodawanie zawierającą metodę statyczną dodaj przyjmującą dwa argumenty typu int. Metoda ta powinna zwrócić wartość będącą wynikiem dodawania obu argumentów. Ćwiczenie 18.5. Napisz klasę Przechowalnia zawierającą metodę statyczną o nazwie przechowaj przyjmującą jeden argument typu int. Klasa ta ma zapamiętywać argument przekazany metodzie przechowaj w taki sposób, że każde wywołanie tej metody spowoduje zwrócenie poprzednio zapisanej wartości i zapamiętanie aktualnie przekazanej. Lekcja 19. Klasy i składowe finalne W Javie klasa lub jej składowa może być finalną, czyli taką, której nie można zmieniać. Umożliwia to słowo kluczowe final. W tej lekcji zostanie pokazane, jak z niego korzystać i co ono znaczy w odniesieniu do klasy jako takiej, a co w przypadku metod i pól. Ponadto przedstawione zostanie to, jak zachowują się pola finalne typów prostych, a jak pola finalne typów obiektowych. Okaże się również, że finalne mogą być także argumenty metod. Klasy finalne Klasa finalna to taka, z której nie wolno wyprowadzać innych klas. Pozwala to programiście tworzyć klasy, które nie będą miały klas pochodnych — ich postać będzie po prostu z góry ustalona. Jeśli klasa ma stać się finalna, należy przed jej nazwą umieścić słowo kluczowe final zgodnie ze schematem: specyfikator_dostępu final class nazwa_klasy { //pola i metody klasy } Rozdział 3. Programowanie obiektowe. Część I 157 Tak więc przykładowa klasa finalna Ostateczna mogłaby wyglądać jak na listingu 3.58. Listing 3.58. public final class Ostateczna { public int liczba; public void wyswietl() { System.out.println(liczba); } } Podobnie jak w przypadku słowa kluczowego static (lekcja 18.), nie ma formalnego znaczenia to, czy napiszemy public final class, czy final public class, niemniej dla przejrzystości i ujednolicenia notacji będziemy konsekwentnie stosować pierwszy przedstawiony sposób zapisu. Z klasy Ostateczna przedstawionej na listingu 3.58 nie można wyprowadzić żadnej innej klasy. Tak więc widoczna na listingu 3.59 klasa RozszerzonaOstateczna dziedzicząca po Ostateczna jest niepoprawna. Java nie dopuści do kompilacji takiego kodu. Kompilator zgłosi komunikat o błędzie zaprezentowany na rysunku 3.28. Listing 3.59. public class RozszerzonaOstateczna extends Ostateczna { public int liczba2; /* …dalsze pola i metody klasy… */ } Rysunek 3.28. Próba dziedziczenia po klasie finalnej kończy się błędem kompilacji Pola finalne Pole klasy oznaczone słowem final staje się finalne, a więc jego wartość jest stała i nie można jej zmieniać. Słowo kluczowe final zwyczajowo umieszcza się przed nazwą typu danego pola — pisze się zatem: final typ_pola nazwa_pola lub ogólniej: specyfikator_dostępu [static] final typ_pola nazwa_pola 158 Java. Praktyczny kurs Tak więc poprawne są wszystkie poniższe deklaracje: final int liczba; public final double liczba; public static final char znak; W rzeczywistości specyfikator dostępu oraz słowa final i static mogą występować w dowolnej kolejności, jednak dla zachowania spójnego stylu i przejrzystości kodu będziemy konsekwentnie trzymać się przedstawionej zasady, że na pierwszym miejscu umieszcza się specyfikator dostępu, słowo final występuje zawsze tuż przed typem pola, natomiast słowo static zawsze tuż przed final. Pola finalne typów prostych Przykładowa klasa zawierająca pola finalne jest przedstawiona na listingu 3.60. Listing 3.60. public final class Obliczenia { public final int liczba1 = 100; public int liczba2; public void licz() { //prawidłowo: odczyt pola liczba1, zapis pola liczba2 liczba2 = 2 * liczba1; //prawidłowo: odczyt pól liczba1 i liczba2, zapis pola liczba2 liczba2 = liczba2 + liczba1; //nieprawidłowo: niedozwolony zapis pola liczba1 //liczba1 = liczba2 / 2; System.out.println(liczba1); System.out.println(liczba2); } public static void main(String args[]) { Obliczenia obliczenia = new Obliczenia(); obliczenia.licz(); } } Zostały tu zadeklarowane dwa pola, liczba1 i liczba2, oba publiczne o typie int. Pierwsze jest również polem finalnym, przypisanej mu wartości nie wolno zmieniać. W klasie znajduje się także dodatkowa metoda licz, która wykonuje działania, wykorzystując wartości przypisane zadeklarowanym polom. Na początku zmiennej liczba2 przypisujemy wynik mnożenia 2 * liczba1. Jest to poprawna instrukcja, gdyż wolno odczytywać wartość pola finalnego liczba1 oraz przypisywać wartości zwykłemu polu liczba2. Identyczna sytuacja zachodzi w przypadku drugiego działania. Trzecia instrukcja została ujęta w komentarz, gdyż jest nieprawidłowa i spowodowałaby błąd kompilacji widoczny na rysunku 3.29. Występuje tu bowiem próba przypisania wyniku działania liczba2 / 2 polu liczba1, które jest polem finalnym. Takiej operacji nie wolno wykonywać, zatem po usunięciu znaków komentarza z tej instrukcji kompilator zdecydowanie zaprotestuje. Do klasy Obliczenia została też dopisana metoda main (lekcja 14.), w której tworzymy nowy obiekt typu Obliczenia i wywołujemy jego metodę licz. Rozdział 3. Programowanie obiektowe. Część I 159 Rysunek 3.29. Próba przypisania wartości zmiennej finalnej Pola finalne typów referencyjnych Zachowanie pól finalnych w przypadku typów prostych jest jasne — nie wolno zmieniać ich wartości. To znaczy wartość przypisana polu pozostaje niezmienna przez cały czas działania programu. W przypadku typów referencyjnych jest oczywiście tak samo, trzeba jednak uświadomić sobie, co to w takim przypadku oznacza. Otóż pisząc: nazwa_klasy nazwa_pola = new nazwa_klasy(argumenty_konstruktora) polu nazwa_pola przypisujemy referencję do nowo powstałego obiektu klasy nazwa_klasy. Przykładowo w przypadku klasy Punkt poznanej w lekcji 13. deklaracja: Punkt punkt = new Punkt() oznacza przypisanie referencji do powstałego na stercie obiektu klasy Punkt zmiennej punkt. Gdyby pole to było finalne, tej referencji nie wolno byłoby zmieniać, jednak nic nie stałoby na przeszkodzie, aby modyfikować pola obiektu, na który ta referencja wskazuje. Czyli po wykonaniu instrukcji: final Punkt punkt = new Punkt(); możliwe byłoby odwołanie w postaci (zakładając publiczny dostęp do pola x): punkt.x = 100; Aby lepiej to zrozumieć, warto spojrzeć na kod przedstawiony na listingu 3.61. Listing 3.61. public class Punkt { public int x; public int y; } public class Main { public static final Punkt punkt = new Punkt(); public static void main (String args[]) { //prawidłowo, można modyfikować pola obiektu punkt punkt.x = 100; punkt.y = 200; //nieprawidłowo, nie można zmieniać referencji finalnej //punkt = new Punkt(); } } 160 Java. Praktyczny kurs Są tu widoczne dwie publiczne klasy: Main i Punkt (należy je oczywiście zapisać w dwóch plikach: Main.java i Punkt.java). Klasa Punkt zawiera dwa publiczne pola typu int o nazwach x i y. Klasa Main zawiera jedno publiczne, statyczne i finalne pole o nazwie punkt, któremu została przypisana referencja do obiektu klasy Punkt. Ponieważ pole jest publiczne, mają do niego dostęp wszystkie inne klasy. Ponieważ jest statyczne, może istnieć nawet wtedy, kiedy nie istnieje żaden obiekt klasy Main (z taką sytuacją mamy właśnie do czynienia, bo nie utworzyliśmy takiego obiektu). Natomiast ponieważ jest finalne, nie wolno zmienić jego wartości. Ale uwaga: zgodnie z tym, co zostało napisane we wcześniejszych akapitach, nie wolno zmienić referencji, przy czym nic nie stoi na przeszkodzie, aby modyfikować pola obiektu, na który ona wskazuje. Dlatego też pierwsze dwa odwołania w metodzie main są poprawne. Wolno przypisać dowolne wartości polom x i y obiektu wskazywanego przez pole punkt. Nie należy natomiast zmieniać samej referencji, zatem ujęta w komentarz instrukcja punkt = new Punkt() jest nieprawidłowa. Inicjalizacja pól finalnych W dotychczasowych przykładach pola finalne były inicjalizowane w momencie ich deklaracji, zatem w przypadku pól typów prostych wyglądało to tak: final nazwa_typu nazwa_pola = wartość; a w przypadku typów referencyjnych: final nazwa_klasy nazwa_pola = new nazwa_klasy(); Wydaje się to logiczne, jednak wcale nie musi tak być. Otóż pola finalne można deklarować również bez ich inicjalizacji. Wartością takiego pola staje się wartość użyta w pierwszym przypisaniu i dopiero od chwili tego przypisania nie wolno jej zmieniać. Pole takie musi jednak zostać zainicjalizowane w konstruktorze danej klasy. Należy tylko pamiętać, że po pierwszym przypisaniu wartości polu finalnemu nie wolno mu już przypisywać innych wartości. Kilka przykładowych, poprawnych oraz niepoprawnych inicjalizacji zostało pokazanych na listingu 3.62. Listing 3.62. public class Main { public final int liczba1 = 100; public final int liczba2; public final Punkt punkt1 = new Punkt(); public final Punkt punkt2; public Main() { //prawidłowo, można modyfikować pola obiektu punkt1 punkt1.x = 100; punkt1.y = 200; //nieprawidłowo, nie ma jeszcze obiektu punkt2 punkt2.x = 200; punkt2.y = 300; //nieprawidłowo, nie można zmieniać referencji finalnej punkt1 = new Punkt(); Rozdział 3. Programowanie obiektowe. Część I 161 //prawidłowo, inicjalizacja pola finalnego punkt2 = new Punkt(); //prawidłowo, można modyfikować pola obiektu punkt2 punkt2.x = 200; punkt2.y = 300; //nieprawidłowo, nie można modyfikować pola finalnego liczba1 = 200; //prawidłowo, inicjalizacja pola finalnego liczba2 = 300; //nieprawidłowo, pole liczba2 zostało już zainicjalizowane liczba2 = 400; } } Metody finalne Metoda oznaczona słowem final staje się finalna, co oznacza, że jej przesłonięcie w klasie potomnej nie będzie możliwe. Słowo final umieszcza się przed typem wartości zwracanej przez metodę, czyli schematycznie konstrukcja taka wygląda następująco: final typ_zwracany nazwa_metody(argumenty) lub ogólniej: specyfikator_dostępu [static] final typ_zwracany nazwa_metody(argumenty). Prawidłowe będą więc wszystkie następujące przykładowe deklaracje: final void metoda(){/*kod metody*/}; public final int metoda(){/*kod metody*/}; public static final void metoda(){/*kod metody*/}; public static final int metoda(int argument){/*kod metody*/}; Ponieważ w jednej klasie nie mogą znaleźć się dwie metody o tej samej nazwie i takich samych argumentach, metody finalne będą się różniły od zwykłych tylko w przypadku dziedziczenia. Dokładniej, zgodnie z tym, co zostało napisane powyżej, metod finalnych nie można przesłaniać w klasach potomnych. Zilustrowano to w przykładzie widocznym na listingu 3.63. Listing 3.63. public class Bazowa { public final void metoda() { //kod metody } } public class Pochodna extends Bazowa { public void metoda() { //kod metody } } 162 Java. Praktyczny kurs Są na nim widoczne dwie klasy, pierwsza Bazowa i druga Pochodna, dziedzicząca po Bazowa. W klasie Bazowa została zdefiniowana publiczna i finalna metoda o nazwie metoda. W Pochodna metoda ta została przesłonięta (lekcja 14.). Zgodnie z powyższymi wyjaśnieniami kod ten powinien być zatem nieprawidłowy (nie wolno przesłaniać metody finalnej) i tak jest w istocie. Próba kompilacji spowoduje wyświetlenie komunikatu o błędzie widocznego na rysunku 3.30. Rysunek 3.30. Próba przesłonięcia metody finalnej kończy się błędem kompilacji Argumenty finalne Wiadomo już, że finalne mogą być klasy, pola oraz metody. Finalne mogą być również argumenty metod. Argument finalny to taki, którego nie wolno zmieniać wewnątrz metody. Aby uzyskać argument finalny, należy umieścić słowo final przed jego typem. Schematycznie konstrukcja taka będzie wyglądała następująco: specyfikator_dostępu [static][final] typ_zwracany nazwa_metody(final typ_argumentu nazwa_argumentu) Przykładowo deklaracja publicznej metody o nazwie metoda1, niezwracającej żadnej wartości, przyjmującej natomiast jeden argument finalny typu int o nazwie argument1, będzie miała postać: public void metoda1(final int argument1){/*treść metody*/} Spójrzmy teraz na listing 3.64. Została na nim przedstawiona klasa z metodami ilustrującymi zachowanie argumentów finalnych. Listing 3.64. public class ArgumentyFinalne { public void metoda1(final int liczba) { //prawidłowo, można odczytywać wartość argumentu int x = liczba; //nieprawidłowo, nie można modyfikować argumentu finalnego //liczba = 100; } public void metoda2(final Punkt punkt) { //prawidłowo, można odczytywać wartości pól obiektu //wskazywanego przez punkt int x = punkt.x; int y = punkt.y; Rozdział 3. Programowanie obiektowe. Część I 163 //nieprawidłowo, nie można modyfikować argumentu finalnego //punkt = new Punkt(); //prawidłowo, można zapisywać pola obiektu //wskazywanego przez argument finalny punkt punkt.x = 100; punkt.y = 100; } } Ćwiczenia do samodzielnego wykonania Ćwiczenie 19.1. Napisz klasę zawierającą dwa pola statyczne i finalne, jedno typu int, drugie typu double. Oba pola zainicjuj w trakcie deklaracji, przypisując im wartość 1. Ćwiczenie 19.2. Napisz klasę zawierającą jedno pole statyczne klasy Punkt. Użyj klasy Punkt z listingu 3.19. Pole zainicjuj, wykorzystując konstruktor dwuargumentowy przyjmujący dwie wartości typu int. Współrzędna x pola ma zostać zainicjowana wartością 100, a y wartością 200. Ćwiczenie 19.3. Napisz klasę finalną Punkt3D dziedziczącą po klasie Punkt z listingu 3.1. W klasie Punkt3D zdefiniuj jedno pole typu int o nazwie z. Ćwiczenie 19.4. Zmodyfikuj klasę Punkt z listingu 3.1 tak, aby stała się klasą publiczną zawierającą dwa pola finalne typu int o nazwach x i y. Z tej klasy wyprowadź klasę Punkt3D, w której będzie zdefiniowane dodatkowe pole finalne o nazwie z i typie int. Czy takie klasy będą miały praktyczne zastosowanie? 164 Java. Praktyczny kurs Rozdział 4. Wyjątki Praktycznie w każdym większym programie powstają jakieś błędy. Powodów jest bardzo wiele — może to być skutek niefrasobliwości programisty, założenia, że wprowadzone dane są zawsze poprawne, niedokładnej specyfikacji poszczególnych modułów aplikacji, użycia niesprawdzonych bibliotek czy nawet zwykłego zapomnienia o zainicjowaniu tylko jednej zmiennej. Na szczęście w Javie, tak jak w większości współczesnych języków programowania, istnieje mechanizm tzw. wyjątków, który pozwala na przechwytywanie błędów. Ta właśnie tematyka zostanie przedstawiona w trzech kolejnych lekcjach. Lekcja 20. Blok try…catch Lekcja 20. jest poświęcona wprowadzeniu do tematu wyjątków. Zostaną w niej omówione sposoby zapobiegania powstawaniu niektórych typów błędów w programach oraz to, jak stosować przechwytujący błędy blok instrukcji try…catch. Przedstawiony zostanie też bliżej wyjątek o nieco egzotycznej dla początkujących programistów nazwie ArrayIndexOutOfBoundsException, dzięki któremu można uniknąć błędów związanych z przekroczeniem dopuszczalnego zakresu indeksów tablic. Sprawdzanie poprawności danych Powróćmy na chwilę do rozdziału 2. i lekcji 11. Znalazł się tam przykład, w którym następowało odwołanie do nieistniejącego elementu tablicy (listing 2.39). Było to spowodowane następującą sekwencją instrukcji: int tab[] = new int[10]; tab[10] = 5; Doświadczony programista od razu zauważy, że te instrukcje są błędne, jako że zadeklarowana została tablica 10-elementowa, zatem — ponieważ indeksowanie tablicy zaczyna się od 0 — ostatni element tablicy ma indeks 9. Tak więc instrukcja tab[10] = 5 powoduje próbę odwołania się do nieistniejącego jedenastego elementu tablicy. Ten błąd jest jednak stosunkowo prosty do wychwycenia, nawet wtedy, gdy pomiędzy deklaracją tablicy a nieprawidłowym odwołaniem są umieszczone inne instrukcje. 166 Java. Praktyczny kurs Dużo więcej kłopotów mogłaby sprawić sytuacja, w której np. tablica byłaby deklarowana w jednej klasie, a odwołanie do niej następowałoby w innej. Taka sytuacja została przedstawiona na listingu 4.1. Listing 4.1. public class Tablica { private int[] tablica = new int[10]; public int pobierzElement(int indeks) { return tablica[indeks]; } public void ustawElement(int indeks, int wartosc) { tablica[indeks] = wartosc; } } public class Main { public static void main (String args[]) { Tablica tablica = new Tablica(); tablica.ustawElement(5, 10); int liczba = tablica.pobierzElement(10); System.out.println(liczba); } } Widoczne są dwie klasy: Tablica oraz Main. W klasie Tablica zostało zadeklarowane prywatne pole typu tablicowego o nazwie tablica, któremu została przypisana 10-elementowa tablica liczb całkowitych. Ponieważ to pole jest prywatne (lekcja 17.), dostęp do niego mają jedynie inne składowe klasy Tablica. Dlatego też powstały dwie metody, pobierzElement oraz ustawElement, operujące na elementach tablicy. Metoda pobierzElement zwraca wartość zapisaną w komórce o indeksie przekazanym jako argument, natomiast ustawElement zapisuje wartość drugiego argumentu w komórce o indeksie wskazywanym przez argument pierwszy. W klasie Main tworzymy obiekt klasy Tablica i wykorzystujemy metodę ustawElement do zapisania w komórce o indeksie 5 wartości 10. W kolejnej linii popełniamy drobną pomyłkę. W metodzie pobierzElement odwołujemy się do nieistniejącego elementu o indeksie 10 (została pomylona wartość z indeksem). Musi to spowodować wystąpienie błędu w trakcie działania aplikacji (rysunek 4.1). Błąd tego typu bardzo łatwo popełnić, gdyż w klasie Main nie widać rozmiarów tablicy. Rysunek 4.1. Odwołanie do nieistniejącego elementu w klasie Tablica Rozdział 4. Wyjątki 167 Jak poradzić sobie z takim problemem? Pierwszym nasuwającym się sposobem jest sprawdzenie w metodach pobierzElement i ustawElement, czy przekazane argumenty nie przekraczają dopuszczalnych wartości. Jeśli takie przekroczenie nastąpi, należy zasygnalizować błąd. To jednak prowokuje pytanie: w jaki sposób ten błąd sygnalizować? Jednym z pomysłów jest zwracanie przez funkcję (metodę) wartości –1 w przypadku błędu oraz wartości nieujemnej (najczęściej 0), jeśli błąd nie wystąpił. To rozwiązanie będzie dobre w przypadku metody ustawElement, która będzie wtedy wyglądała następująco: public int ustawElement(int indeks, int wartosc) { if(indeks >= tablica.length || indeks < 0){ return –1; } else{ tablica[indeks] = wartosc; return 0; } } Wystarczyłoby teraz w klasie Main testować wartość zwróconą przez ustawElement, aby sprawdzić, czy nie został przekroczony dopuszczalny indeks tablicy. Niestety tej techniki nie można zastosować w przypadku metody pobierzElement — przecież zwraca ona wartość zapisaną w jednej z komórek tablicy. Czyli –1 i 0 użyte przed chwilą do zasygnalizowania, czy operacja zakończyła się błędem, mogą być wartościami odczytanymi z tablicy. Trzeba zatem wymyślić inną metodę. Może to być np. wykorzystanie dodatkowego pola sygnalizującego w klasie Tablica. Pole to byłoby typu boolean. Ustawione na true oznaczałoby, że ostatnia operacja zakończyła się błędem, natomiast ustawione na false — że zakończyła się sukcesem. Klasa Tablica miałaby wtedy postać jak na listingu 4.2. Listing 4.2. public class Tablica { private int[] tablica = new int[10]; public boolean wystapilBlad = false; public int pobierzElement(int indeks) { if(indeks >= tablica.length){ wystapilBlad = true; return 0; } else{ wystapilBlad = false; return tablica[indeks]; } } public void ustawElement(int indeks, int wartosc) { if(indeks >= tablica.length){ wystapilBlad = true; } else{ tablica[indeks] = wartosc; wystapilBlad = false; } } } 168 Java. Praktyczny kurs Do klasy zostało dodane pole typu boolean o nazwie wystapilBlad. Jego początkowa wartość to false. W metodzie pobierzElement sprawdzamy najpierw, czy przekazany indeks nie przekracza maksymalnej dopuszczalnej wartości1. Jeśli tak, ustawiamy pole wystapilBlad na true oraz zwracamy wartość 0. Oczywiście w tym przypadku zwrócona wartość nie ma żadnego praktycznego znaczenia (przecież wystąpił błąd), niemniej coś musimy zwrócić. Użycie instrukcji return i zwrócenie wartości typu int jest konieczne, inaczej kompilator zgłosi błąd. Jeśli jednak argument przekazany metodzie nie przekracza dopuszczalnego indeksu tablicy, pole wystapilBlad ustawiamy na false oraz zwracamy wartość znajdującą się pod wskazanym indeksem. W metodzie ustawElement postępujemy podobnie. Sprawdzamy, czy przekazany indeks nie przekracza maksymalnej dopuszczalnej wartości. Jeśli przekracza, pole wystapilBlad ustawiamy na true, w przeciwnym razie dokonujemy przypisania wartości z argumentu wartosc wskazanej komórce tablicy i ustawiamy wystapilBlad na false. Po takiej modyfikacji obu metod w klasie Main można już bez problemu stwierdzić, czy operacje wykonywane na klasie Tablica zakończyły się sukcesem. Przykład wykorzystania możliwości, jakie daje nowe pole, został przedstawiony na listingu 4.3. Listing 4.3. public class Main { public static void main (String args[]) { Tablica tablica = new Tablica(); tablica.ustawElement(5, 10); int liczba = tablica.pobierzElement(10); if (tablica.wystapilBlad){ System.out.println("Nieprawidłowy indeks tablicy..."); } else{ System.out.println(liczba); } } } Podstawowe wykonywane operacje są takie same jak w przypadku klasy z listingu 4.1, tyle że po pobraniu elementu sprawdzamy, czy operacja ta zakończyła się sukcesem, i wyświetlamy odpowiedni komunikat na ekranie. Identyczne sprawdzenie można byłoby wykonać również po wywołaniu metody ustawElement. Wykonanie kodu z listingu 4.3 spowoduje oczywiście wyświetlenie napisu Nieprawidłowy indeks tablicy... (rysunek 4.2). Sposobów radzenia sobie z problemem przekroczenia indeksu tablicy można byłoby zapewne wymyślić jeszcze kilka. Metody tego typu mają jednak poważną wadę: programiści mają tendencję do ich… niestosowania. Często możliwy błąd wydaje się zbyt banalny, aby się nim zajmować, czasami po prostu zapomina się o sprawdzaniu pewnych warunków. Dodatkowo przedstawione wyżej sposoby, czyli zwracanie wartości sygnalizacyjnej 1 Nie jest natomiast badana dopuszczalna wartość minimalna. Będzie to ćwiczenie do samodzielnego wykonania. Rozdział 4. Wyjątki 169 Rysunek 4.2. Efekt wykonania programu z listingu 4.3 czy korzystanie z dodatkowych zmiennych, powodują niepotrzebne rozbudowywanie kodu aplikacji, co paradoksalnie może prowadzić do powstawania kolejnych błędów, a więc błędów wynikających z napisania kodu zajmującego się obsługą błędów… W Javie zastosowano w związku z tym mechanizm tak zwanych wyjątków, znany na pewno programistom C++ i Object Pascala2. Wyjątki w Javie Wyjątek (ang. exception) jest to byt programistyczny, który powstaje w sytuacji wystąpienia błędu. Z powstaniem wyjątku mieliśmy już do czynienia w rozdziale 2., w lekcji 11. Był to wyjątek spowodowany przekroczeniem dopuszczalnego zakresu tablicy. Zobaczmy go raz jeszcze: na listingu 4.4 została zaprezentowana odpowiednio spreparowana klasa Main. Listing 4.4. public class Main { public static void main (String args[]) { int tab[] = new int[10]; tab[10] = 100; } } Deklarujemy tablicę liczb typu int o nazwie tab oraz próbujemy przypisać elementowi o indeksie 10 (czyli wykraczającym poza zakres tablicy) wartość 100. Jeśli skompilujemy i uruchomimy taki program, na ekranie pojawi się obraz podobny do tego z rysunku 4.1 (tym razem w komunikacie wskazany będzie tylko jeden plik źródłowy — Main.java). Został tu wygenerowany wyjątek o nazwie ArrayIndexOutOfBoundsException, oznaczający, że indeks tablicy znajduje się poza dopuszczalnymi granicami. Oczywiście gdyby możliwości wyjątków kończyły się na wyświetlaniu informacji na ekranie i przerywaniu działania programu, ich przydatność byłaby mocno ograniczona. Na szczęście wygenerowany wyjątek można przechwycić i wykonać własny kod obsługi błędu. Do takiego przechwycenia służy blok instrukcji try…catch. W najprostszej postaci wygląda on następująco: try{ //instrukcje mogące spowodować wyjątek } 2 Sama technika obsługi sytuacji wyjątkowych sięga jednak lat sześćdziesiątych ubiegłego stulecia (czyli wieku XX). 170 Java. Praktyczny kurs catch (TypWyjątku identyfikatorWyjątku){ //obsługa wyjątku } W nawiasie klamrowym występującym po słowie try została umieszczona instrukcja, która może spowodować wygenerowanie wyjątku. W bloku występującym po catch umieszcza się kod, który ma zostać wykonany, kiedy zostanie wygenerowany wyjątek. W przypadku klasy Main z listingu 4.4 blok try…catch należałoby zastosować w sposób przedstawiony na listingu 4.5. Listing 4.5. public class Main { public static void main (String args[]) { int tab[] = new int[10]; try{ tab[10] = 100; } catch(ArrayIndexOutOfBoundsException e){ System.out.println("Nieprawidłowy indeks tablicy!"); } } } Jak widać, wszystko odbywa się tu zgodnie z wcześniejszym ogólnym opisem. W bloku try znalazła się instrukcja tab[10] = 100, która — jak już wiadomo — spowoduje wygenerowanie wyjątku. W nawiasie okrągłym występującym po instrukcji catch został wymieniony rodzaj wyjątku, który będzie wygenerowany: ArrayIndexOutOfBoundsException, oraz jego identyfikator: e. Identyfikator to nazwa3, która pozwala na wykonywanie operacji związanych z wyjątkiem, tym jednak zajmiemy się w kolejnej lekcji. W bloku po catch znajduje się instrukcja System.out.println wyświetlająca odpowiedni komunikat na ekranie. Tym razem po uruchomieniu kodu pojawi się widok podobny do zaprezentowanego na rysunku 4.2. Blok try…catch nie musi jednak obejmować tylko jednej instrukcji ani też tylko instrukcji mogących wygenerować wyjątek. Powróćmy na chwilę do listingu 2.39. Blok try mógłby w tym wypadku objąć wszystkie trzy instrukcje, a klasa Main miałaby wtedy postać jak na listingu 4.6. Listing 4.6. class Main { public static void main (String args[]) { try{ int tab[] = new int[10]; tab[10] = 5; System.out.println("Dziesiąty element tablicy ma wartość: " + tab[10]); } catch(ArrayIndexOutOfBoundsException e){ System.out.println("Nieprawidłowy indeks tablicy!"); 3 Dokładniej, jest to nazwa zmiennej obiektowej (referencyjnej), co zostanie bliżej wyjaśnione w lekcji 21. Rozdział 4. Wyjątki 171 } } } Nie trzeba również obejmować blokiem try instrukcji bezpośrednio generujących wyjątek, tak jak miało to miejsce w dotychczas przedstawionych przykładach. Wyjątek wygenerowany przez obiekt klasy Y może być bowiem przechwytywany w klasie X, która korzysta z obiektów klasy Y. Sprawdźmy to na przykładzie klas z listingu 4.1. Klasa Tablica pozostanie bez zmian, natomiast klasę Main zmodyfikujemy tak, aby miała wygląd zaprezentowany na listingu 4.7. Listing 4.7. public class Main { public static void main (String args[]) { Tablica tablica = new Tablica(); try{ tablica.ustawElement(5, 10); int liczba = tablica.pobierzElement(10); System.out.println(liczba); } catch(ArrayIndexOutOfBoundsException e){ System.out.println("Nieprawidłowy indeks tablicy!"); } } } Spójrzmy: w bloku try zostają wykonane trzy instrukcje, z których jedna, int liczba = tablica.pobierzElement(10), jest odpowiedzialna za wygenerowanie wyjątku. Ale wyjątek jest przecież generowany we wnętrzu metody pobierzElement klasy Tablica, a nie w klasie Main! Zostanie on jednak przekazany klasie Main, jako że wywołuje ona metodę z klasy Tablica. Tym samym w klasie Main można zastosować blok try…catch. Identyczna sytuacja ma miejsce w przypadku hierarchicznego wywołania metod jednej klasy, czyli kiedy metoda f wywołuje metodę g, która wywołuje metodę h generującą z kolei wyjątek. W każdej z wymienionych metod można zastosować blok try…catch do przechwycenia tego wyjątku. Dokładnie taki przykład jest widoczny na listingu 4.8. Listing 4.8. public class Wyjatki { public void f() { try{ g(); } catch(ArrayIndexOutOfBoundsException e){ System.out.println("Wyjątek: metoda f"); } } public void g() { try{ h(); } 172 Java. Praktyczny kurs catch(ArrayIndexOutOfBoundsException e){ System.out.println("Wyjątek: metoda g"); } } public void h() { int[] tab = new int[0]; try{ tab[0] = 1; } catch(ArrayIndexOutOfBoundsException e){ System.out.println("Wyjątek: metoda h"); } } public static void main(String args[]) { Wyjatki ex = new Wyjatki(); try{ ex.f(); } catch(ArrayIndexOutOfBoundsException e){ System.out.println("Wyjątek: metoda main"); } } } Taką klasę da się skompilować bez żadnych problemów. Trzeba jednak zdawać sobie sprawę z tego, jak taki kod będzie wykonywany. Pytanie brzmi: które bloki try zostaną wykonane? Zasada jest następująca: zostanie wykonany blok znajdujący się najbliżej instrukcji powodującej wystąpienie wyjątku. Tak więc w przypadku przedstawionym na listingu 4.8 będzie to jedynie blok obejmujący wywołanie metody tab[0] = 1; w metodzie h. Jeśli jednak będziemy usuwać kolejne bloki try najpierw z metody h, następnie g, f i ostatecznie z main, zobaczymy, że w rzeczywistości wykonywany jest zawsze blok najbliższy miejsca wystąpienia błędu. Po usunięciu wszystkich instrukcji try wyjątek nie zostanie obsłużony w klasie Wyjatki i obsłuży go maszyna wirtualna Javy, co zaowocuje znanym nam już komunikatem na konsoli. Co jednak warte uwagi, w tym wypadku zostanie wyświetlona cała hierarchia metod, w których propagowany był wyjątek (rysunek 4.3). Rysunek 4.3. Przy hierarchicznym wywołaniu metod po wystąpieniu wyjątku otrzymamy ich nazwy Rozdział 4. Wyjątki 173 Ćwiczenia do samodzielnego wykonania Ćwiczenie 20.1. Popraw kod klasy z listingu 4.2 tak, aby w metodach pobierzElement i ustawElement sprawdzane było również to, czy przekazany indeks nie przekracza minimalnej dopuszczalnej wartości. Ćwiczenie 20.2. Zmień kod klasy Main z listingu 4.3 w taki sposób, by sprawdzane było również to, czy wywołanie metody ustawElement zakończyło się sukcesem. Ćwiczenie 20.3. Zmień kod z listingu 4.2 tak, aby do wychwytywania błędów był wykorzystywany mechanizm wyjątków, a nie instrukcja warunkowa if (przy czym zewnętrzna sygnalizacja wystąpienia błędu nadal ma się odbywać poprzez pole wystapilBlad). Ćwiczenie 20.4. Napisz klasę Wyjatki, w której będzie się znajdowała metoda o nazwie a, wywoływana z kolei przez metodę o nazwie b. W metodzie a wygeneruj wyjątek ArrayIndexOutOf BoundsException. Napisz następnie klasę Main zawierającą metodę main, w której zostanie utworzony obiekt klasy Wyjatki i zostaną wywołane metody a oraz b tego obiektu. W metodzie main zastosuj bloki try…catch przechwytujące powstałe wyjątki. Lekcja 21. Wyjątki to obiekty W lekcji 20. został omówiony wyjątek sygnalizujący przekroczenie dopuszczalnego zakresu tablicy. To nie jest oczywiście jedyny dostępny typ — czas poznać również inne. W lekcji 21. okaże się, że wyjątki są tak naprawdę obiektami, a także że tworzą one hierarchiczną strukturę. Omówimy przechwytywanie wielu wyjątków w jednym bloku try, zagnieżdżanie bloków try…catch oraz przypadek, gdy jeden wyjątek ogólny może obsłużyć wiele błędów. Dzielenie przez zero Rodzajów wyjątków jest bardzo wiele. Wiadomo już, jak reagować na przekroczenie zakresu tablicy. Czas poznać inny typ wyjątku, generowanego, kiedy zostanie podjęta próba wykonania dzielenia przez zero. W tym celu musimy spreparować odpowiedni fragment kodu. Wystarczy w metodzie main umieścić przykładową instrukcję: int liczba = 10 / 0; Kompilacja takiego kodu przebiegnie bez problemu, jednak próba wykonania musi skończyć się komunikatem o błędzie, widocznym na rysunku 4.4. Widać wyraźnie, że tym razem został zgłoszony wyjątek ArithmeticException (wyjątek arytmetyczny). 174 Java. Praktyczny kurs Rysunek 4.4. Próba wykonania dzielenia przez zero Dzięki wiedzy zdobytej w ramach lekcji 20. nikt nie powinien mieć żadnych problemów z napisaniem kodu, który przechwyci taki wyjątek. Wykorzystajmy więc dobrze nam znaną instrukcję try…catch w postaci: try{ int liczba = 10 / 0; } catch(ArithmeticException e){ //instrukcje do wykonania, kiedy wystąpi wyjątek } Intuicja podpowiada zapewne, że rodzajów wyjątków może być bardzo, bardzo dużo. I faktycznie tak jest, w klasach dostarczonych w JDK (w wersji SE) jest ich zdefiniowanych blisko 500. Aby sprawnie się nimi posługiwać, trzeba się dowiedzieć, czym tak naprawdę są. Wyjątek jest obiektem Wyjątek, określany wcześniej jako byt programistyczny, to nic innego jak obiekt powstający, kiedy w programie wystąpi sytuacja wyjątkowa. Skoro wyjątek jest obiektem, to wspomniany wcześniej typ wyjątku (ArrayIndexOutOfBoundsException, Arithmetic Exception) nie jest niczym innym niż klasą opisującą ten obiekt. Jeśli teraz spojrzymy ponownie na ogólną postać instrukcji try…catch: try{ //instrukcje mogące spowodować wyjątek } catch(TypWyjątku identyfikatorWyjątku){ //obsługa wyjątku } stanie się jasne, że w takim razie identyfikatorWyjątku to zmienna obiektowa (referencyjna) wskazująca na obiekt wyjątku. Na tym obiekcie można wykonywać operacje zdefiniowane w klasie wyjątku. Możliwe jest np. uzyskanie systemowego komunikatu o błędzie. Wystarczy wywołać metodę getMessage(). Można to zilustrować na przykładzie wyjątku generowanego podczas próby wykonania dzielenia przez zero. Jest on zaprezentowany na listingu 4.9. Listing 4.9. public class Main { public static void main (String args[]) { try{ int liczba = 10 / 0; Rozdział 4. Wyjątki 175 } catch(ArithmeticException e){ System.out.println("Wystąpił wyjątek arytmetyczny..."); System.out.println("Komunikat systemowy:"); System.out.println(e.getMessage()); } } } Wykonujemy tutaj próbę niedozwolonego dzielenia przez zero oraz przechwytujemy wyjątek typu ArithmeticException. W bloku catch najpierw wyświetlamy własne komunikaty o błędzie, a następnie komunikat systemowy (pobrany za pomocą metody getMessage()). Po uruchomieniu kodu na ekranie pojawi się widok zaprezentowany na rysunku 4.5. Rysunek 4.5. Wyświetlenie systemowego komunikatu o błędzie Istnieje jeszcze jedna możliwość uzyskania komunikatu o wyjątku — umieszczenie w argumencie instrukcji System.out.println zmiennej wskazującej na obiekt wyjątku, czyli w przypadku listingu 4.9: System.out.println(e); W pierwszej chwili może się to wydawać nieco dziwne, bo niby skąd instrukcja System. out.println ma „wiedzieć”, co ma w takim wypadku wyświetlić. Jest to sytuacja analogiczna do tej, z jaką mieliśmy do czynienia w przypadku typów prostych (por. lekcja 5.). Skoro udawało się automatycznie przekształcać np. zmienną typu int na ciąg znaków, uda się również przekształcić zmienną typu obiektowego. Tym problemem zajmiemy się bliżej w rozdziale 5. Jeśli zatem w programie z listingu 4.9 zamienimy instrukcję: System.out.println(e.getMessage()); na: System.out.println(e); otrzymamy nieco dokładniejszy komunikat określający dodatkowo klasę wyjątku, tak jak jest to widoczne na rysunku 4.6. 176 Java. Praktyczny kurs Rysunek 4.6. Pełniejszy komunikat o typie wyjątku Hierarchia wyjątków Każdy wyjątek jest obiektem pewnej klasy. Klasy podlegają z kolei regułom dziedziczenia, zgodnie z którymi powstaje ich hierarchia. Kiedy zatem pracuje się z wyjątkami, trzeba tę kwestię wziąć pod uwagę. Wszystkie standardowe wyjątki, które można przechwytywać w naszych aplikacjach za pomocą bloku try…catch, dziedziczą po klasie Exception, dziedziczącej z kolei po Throwable oraz Object. Hierarchia klas dla wyjątku ArithmeticException, który był wykorzystywany we wcześniejszych przykładach, jest zaprezentowana na rysunku 4.74. Rysunek 4.7. Hierarchia klas dla wyjątku ArithmeticException Wynika z tego kilka własności. Przede wszystkim, jeśli spodziewamy się, że dana instrukcja może wygenerować wyjątek typu X, możemy zawsze przechwycić wyjątek ogólniejszy, czyli taki, którego typem będzie jedna z klas nadrzędnych do X. Jest to bardzo wygodna technika. Przykładowo po klasie RuntimeException dziedziczy bardzo wiele klas wyjątków odpowiadających najróżniejszym błędom. Jedną z nich jest ArithmeticException. Jeśli instrukcje, które są obejmowane blokiem try…catch, mogą spowodować wiele różnych wyjątków, zamiast stosować wiele oddzielnych instrukcji przechwytujących konkretne typy błędów, często lepiej jest użyć jednej przechwytującej wyjątek bardziej ogólny. Taka sytuacja została przedstawiona na listingu 4.10. 4 Kierunek strzałek na rysunku jest niezgodny z metodyką modelowania UML, za to bardziej zgodny z intuicją. Rozdział 4. Wyjątki 177 Listing 4.10. public class Main { public static void main (String args[]) { try{ int liczba = 10 / 0; } catch(RuntimeException e){ System.out.println("Wystąpił wyjątek czasu wykonania..."); System.out.println("Komunikat systemowy:"); System.out.println(e); } } } Jest to znany nam już program, który generuje błąd polegający na próbie wykonania niedozwolonego dzielenia przez zero. Tym razem jednak zamiast wyjątku klasy ArithmeticException przechwytywany jest wyjątek klasy nadrzędnej — Runtime Exception. Co więcej, nic nie stoi na przeszkodzie, aby przechwycić wyjątek jeszcze bardziej ogólny, czyli klasy nadrzędnej do RuntimeException. Jak widać na rysunku 4.7, byłaby to klasa Exception. Przechwytywanie wielu wyjątków W jednym bloku try…catch można przechwytywać wiele wyjątków. Konstrukcja taka zawiera wtedy jeden blok try i wiele bloków catch. Schematycznie wygląda to następująco: try{ //instrukcje mogące spowodować wyjątek } catch(KlasaWyjątku1 identyfikatorWyjątku1){ //obsługa wyjątku 1 } catch(KlasaWyjątku2 identyfikatorWyjątku2){ //obsługa wyjątku 2 } /* …dalsze bloki catch… */ catch(KlasaWyjątkuN identyfikatorWyjątkuN){ //obsługa wyjątku n } Po wygenerowaniu wyjątku następuje sprawdzenie, czy jest on klasy KlasaWyjątku1 (inaczej: czy jego typem jest KlasaWyjątku1). Jeśli tak — są wykonywane instrukcje obsługi tego wyjątku i blok try…catch jest opuszczany. Jeżeli jednak wyjątek nie jest klasy KlasaWyjątku1, następuje sprawdzenie, czy jest on klasy KlasaWyjątku2 itd. Przy tego typu konstrukcjach należy jednak pamiętać o hierarchii wyjątków, nie jest bowiem obojętne, w jakiej kolejności będą one przechwytywane. Ogólna zasada jest taka, że nie ma znaczenia kolejność, o ile wszystkie wyjątki są na jednym poziomie hierarchii. Jeśli jednak przechwytuje się wyjątki z różnych poziomów, najpierw muszą to być te bardziej szczegółowe, czyli stojące niżej w hierarchii, a dopiero po nich bardziej ogólne, czyli stojące wyżej w hierarchii. 178 Java. Praktyczny kurs Nie można zatem najpierw przechwycić wyjątku RuntimeException, a dopiero po nim ArithmeticException (por. rysunek 4.7), gdyż skończy się to błędem kompilacji. Jeśli więc dokonamy próby kompilacji przykładowego programu przedstawionego na listingu 4.11, efektem będą komunikaty widoczne na rysunku 4.8. Listing 4.11. public class Main { public static void main (String args[]) { try{ int liczba = 10 / 0; } catch(RuntimeException e){ System.out.println(e); } catch(ArithmeticException e){ System.out.println(e); } } } Rysunek 4.8. Błędna hierarchia wyjątków powoduje błąd kompilacji Dzieje się tak dlatego, że (można powiedzieć) błąd bardziej ogólny zawiera już w sobie błąd bardziej szczegółowy. Jeśli zatem przechwyci się najpierw wyjątek RuntimeException, to tak jakby przechwycić już wyjątki wszystkich klas dziedziczących po RuntimeException. Dlatego kompilator protestuje. Kiedy może się przydać rozwiązanie, w którym najpierw przechwytuje się wyjątek szczegółowy, a dopiero potem ogólny? Otóż wtedy, kiedy chcemy w specyficzny sposób zareagować na konkretny typ wyjątku, a wszystkie pozostałe wyjątki z danego poziomu hierarchii obsłużyć w identyczny, standardowy sposób. Taki przykład jest przedstawiony na listingu 4.12. Listing 4.12. public class Main { public static void main (String args[]) { Punkt punkt = null; int liczba; try{ liczba = 10 / 0; punkt.x = liczba; } catch(ArithmeticException e){ System.out.println("Nieprawidłowa operacja arytmetyczna"); Rozdział 4. Wyjątki 179 System.out.println(e); } catch(Exception e){ System.out.println("Błąd ogólny"); System.out.println(e); } } } Zostały zadeklarowane dwie zmienne: pierwsza typu Punkt o nazwie punkt oraz druga typu int o nazwie liczba. Zmiennej punkt została przypisana wartość pusta null, nie został zatem utworzony żaden obiekt klasy Punkt. W bloku try są wykonywane dwie błędne instrukcje. Pierwsza z nich to znane z poprzednich przykładów dzielenie przez zero. Druga instrukcja z bloku try to z kolei próba odwołania się do pola x nieistniejącego obiektu klasy Punkt (przecież zmienna punkt zawiera wartość null). Ponieważ chcemy w sposób niestandardowy zareagować na błąd arytmetyczny, najpierw przechwytujemy błąd typu ArithmeticException i w przypadku kiedy wystąpi, wyświetlamy na ekranie napis Nieprawidłowa operacja arytmetyczna. W drugim bloku catch przechwytujemy wszystkie inne możliwe wyjątki, w tym także NullPointerException występujący podczas próby wykonania operacji na zmiennej obiektowej, która zawiera wartość null. Po uruchomieniu kodu z listingu 4.12 na ekranie pojawi się zgłoszenie tylko pierwszego błędu. Dzieje się tak dlatego, że po jego wystąpieniu blok try został przerwany, a sterowanie zostało przekazane blokowi catch. Jeśli więc w bloku try któraś z instrukcji spowoduje wygenerowanie wyjątku, dalsze instrukcje z tego bloku nie zostaną wykonane. Nie będzie więc miała szansy zostać wykonana nieprawidłowa instrukcja punkt.x = liczba;. Jeśli jednak usuniemy wcześniejsze dzielenie przez zero, przekonamy się, że i ten błąd zostanie przechwycony przez drugi blok catch, a na ekranie pojawi się stosowny komunikat (rysunek 4.9). Rysunek 4.9. Odwołanie do pustej zmiennej obiektowej zostało wychwycone przez drugi blok catch W Java 7 i nowszych dostępna jest możliwość przychwycenie kilku typów wyjątków w jednym bloku catch. Poszczególne typy należy oddzielić od siebie pionową kreską |. Wtedy niezależnie od tego, jaki wyjątek z wymienionych wystąpi, zostanie wykonany taki sam kod (zawarty w bloku catch). Schemat takiej konstrukcji jest następujący: try{ //instrukcje mogące spowodować wyjątek } catch (TypWyjątku1 | TypWyjątku2 |…TypWyjątkuN identyfikatorWyjątku){ //obsługa wyjątku } 180 Java. Praktyczny kurs A zatem gdybyśmy w programie z listingu 4.12 chcieli w identyczny sposób obsługiwać błąd arytmetyczny oraz wynikający z niezainicjowania zmiennej punkt, a w inny sposób reagować na wszystkie pozostałe wyjątki, kod mógłby wyglądać tak jak na listingu 4.13. Listing 4.13. public class Main { public static void main (String args[]) { Punkt punkt = null; int liczba; try{ liczba = 10 / 0; punkt.x = liczba; } catch(ArithmeticException | NullPointerException e){ System.out.println("Błąd arytmetyczny lub niezainicjowana zmienna."); System.out.println(e); } catch(Exception e){ System.out.println("Błąd ogólny"); System.out.println(e); } } } Zagnieżdżanie bloków try…catch Bloki try…catch można zagnieżdżać. To znaczy, że w jednym bloku przechwytującym wyjątek X może istnieć drugi blok, który będzie przechwytywał wyjątek Y. Schematycznie taka konstrukcja wygląda następująco: try{ //instrukcje mogące spowodować wyjątek 1 try{ //instrukcje mogące spowodować wyjątek 2 } catch (TypWyjątku2 identyfikatorWyjątku2){ //obsługa wyjątku 2 } } catch (TypWyjątku1 identyfikatorWyjątku1){ //obsługa wyjątku 1 } Zagnieżdżenie może być wielopoziomowe, czyli w już zagnieżdżonym bloku try można umieścić kolejny taki blok. W praktyce tego rodzaju piętrowych konstrukcji zazwyczaj się nie stosuje, bo zwykle nie ma takiej potrzeby. Z reguły nie korzysta się z bezpośredniego zagnieżdżenia więcej niż dwóch poziomów. Aby na praktycznym przykładzie przyjrzeć się takiej dwupoziomowej konstrukcji, zmodyfikujmy kod z listingu 4.12. Zamiast obejmować jednym blokiem try dwie instrukcje powodujące błąd, zastosujemy zagnieżdżenie, tak jak jest to przedstawione na listingu 4.14. Rozdział 4. Wyjątki 181 Listing 4.14. public class Main { public static void main (String args[]) { Punkt punkt = null; int liczba; try{ try{ liczba = 10 / 0; } catch(ArithmeticException e){ System.out.println("Nieprawidłowa operacja arytmetyczna"); System.out.println("Przypisuję zmiennej liczba wartość 10"); liczba = 10; } punkt.x = liczba; } catch(Exception e){ System.out.println("Błąd ogólny"); System.out.println(e); } } } Podobnie jak w poprzednim przypadku, deklarujemy dwie zmienne: punkt typu Punkt oraz liczba typu int. Zmiennej punkt przypisujemy wartość pustą null. W wewnętrznym bloku try próbujemy wykonać nieprawidłowe dzielenie przez zero i przechwytujemy wyjątek ArithmeticException. Jeśli on wystąpi, zmienna liczba otrzymuje domyślną wartość równą 10, dzięki czemu można wykonać kolejną operację, czyli próbę przypisania polu x obiektu wskazywanego przez punkt wartości zmiennej liczba. Rzecz jasna przypisanie takie nie może zostać wykonane, gdyż zmienna punkt jest pusta, jest zatem generowany wyjątek NullPointerException, który zostaje przechwycony przez zewnętrzny blok try. Widać więc, że zagnieżdżanie bloków try może być przydatne, choć warto zauważyć, że identyczny efekt można osiągnąć, korzystając z niezagnieżdżonej postaci instrukcji try…catch (ćwiczenie 21.3). Ćwiczenia do samodzielnego wykonania Ćwiczenie 21.1. Popraw kod z listingu 4.11 tak, aby przechwytywanie wyjątków odbywało się w prawidłowej kolejności. Ćwiczenie 21.2. Zmodyfikuj kod z listingu 4.12 tak, by zgłoszone zostały oba typy błędów: Arithmetic Exception oraz NullPointerException. 182 Java. Praktyczny kurs Ćwiczenie 21.3. Zmodyfikuj kod z listingu 4.14 w taki sposób, aby usunąć zagnieżdżenie bloków try…catch, ale by nie zmieniać efektów działania programu. Ćwiczenie 21.4. Napisz kod klasy TablicaPunktow przechowującej do czterech obiektów typu Punkt oraz testowy program ilustrujący różne sytuacje powstające przy korzystaniu z obiektu tej klasy (zadbaj o przechwytywanie wyjątków). Klasa ma zawierać jedynie dwie metody: pierwszą — przyjmującą trzy argumenty typu int i ustawiającą współrzędne x i y punktu przechowywanego pod zadanym indeksem tablicy, oraz drugą — zwracającą referencję do punktu znajdującego się pod wskazanym indeksem. Program ma odpowiednio reagować, gdy następuje próba użycia nieistniejącego indeksu tablicy, a także wtedy, gdy następuje próba zmiany współrzędnych nieistniejącego punktu (punkt powinien zostać wówczas utworzony). Ćwiczenie 21.5. Jeśli w rozwiązaniu ćwiczenia 21.4 użyłeś instrukcji if, rozwiąż je, nie stosując tej instrukcji. W przeciwnym przypadku rozwiąż zadanie, korzystając z instrukcji if. Dodatkowo do klasy TablicaPunktow dopisz metodę usuwającą punkt znajdujący się pod wskazanym indeksem tablicy. Lekcja 22. Własne wyjątki Wyjątki można przechwytywać, aby zapobiec niekontrolowanemu zakończeniu działania programu w przypadku wystąpienia błędu. Ta technika została omówiona w lekcjach 20. i 21. Okazuje się jednak, że wyjątki można również zgłaszać samemu, a także że można tworzyć nowe, nieistniejące wcześniej typy wyjątków. Tej właśnie tematyce jest poświęcona bieżąca lekcja. Zgłaszanie wyjątku Wiadomo już, że wyjątki są obiektami. Zgłoszenie5 własnego wyjątku będzie polegało na utworzeniu nowego obiektu klasy opisującej wyjątek oraz użyciu instrukcji throw. Dokładniej: za pomocą instrukcji new należy utworzyć nowy obiekt klasy, która pośrednio lub bezpośrednio dziedziczy po klasie Throwable. W najbardziej ogólnym przypadku będzie to klasa Exception. Tak utworzony obiekt musi stać się parametrem instrukcji throw. Jeśli zatem gdziekolwiek w pisanym kodzie ma być zgłoszony wyjątek ogólny, wystarczy napisać: throw new Exception(); 5 Stosuje się również termin „wyrzucenie wyjątku”, od ang. throw — rzucać, wyrzucać. Rozdział 4. Wyjątki 183 Jeśli taki wyjątek zostanie obsłużony przez znajdującą się w danym bloku (danej metodzie) instrukcję try…catch, nie trzeba robić nic więcej. Jeśli jednak nie zostanie obsłużony (i nie jest wyjątkiem klasy RuntimeException lub pochodnej od niej), w specyfikacji metody należy zaznaczyć, że może ona taki wyjątek zgłaszać (rzucać, wyrzucać). Robi się to za pomocą instrukcji throws, w ogólnej postaci: specyfikator_dostępu [static] [final] typ_zwracany nazwa_metody(argumenty) throws KlasaWyjątku1, KlasaWyjątku2, ... , KlasaWyjątkuN { //treść metody } Zobaczmy, jak to wygląda w praktyce. Załóżmy, że mamy klasę Main, a w niej metodę main. Jedynym zadaniem tej metody będzie zgłoszenie wyjątku typu Exception. Taka klasa jest widoczna na listingu 4.15. Listing 4.15. public class Main { public static void main (String args[]) throws Exception { throw new Exception(); } } W deklaracji metody main poprzez użycie słów throws Exception zostało zaznaczone, że może ona generować wyjątek klasy Exception. W ciele metody main została natomiast wykorzystana instrukcja throw, która jako argument otrzymała nowy obiekt klasy Exception. Po uruchomieniu opisanego programu na ekranie pojawi się widok zaprezentowany na rysunku 4.10. Jest to najlepszy dowód, że rzeczywiście udało się zgłosić wyjątek. Rysunek 4.10. Zgłoszenie wyjątku klasy Exception Utworzenie obiektu wyjątku nie musi mieć miejsca bezpośrednio w instrukcji throw, można go utworzyć wcześniej, przypisać zmiennej obiektowej (referencyjnej) i dopiero tej zmiennej użyć jako parametru dla throw. Zamiast więc pisać: throw new Exception(); można równie dobrze zastosować konstrukcję: Exception exception = new Exception(); throw exception; W obu przedstawionych przypadkach efekt będzie identyczny, najczęściej korzysta się jednak z pierwszego zaprezentowanego sposobu. 184 Java. Praktyczny kurs Aby zgłaszanemu wyjątkowi został przypisany komunikat, należy przekazać go jako parametr konstruktora klasy Exception, a więc użyć instrukcji w postaci: throw new Exception("komunikat"); lub: Exception exception = new Exception("komunikat"); throw exception; Oczywiście można tworzyć obiekty wyjątków klas dziedziczących po Exception. Przykładowo: jeśli wykryjemy próbę dzielenia przez zero, być może zechcemy samodzielnie wygenerować wyjątek, nie czekając, aż zgłosi go maszyna wirtualna. Taka sytuacja została przedstawiona na listingu 4.16. Listing 4.16. public class Dzielenie { public double podziel(int liczba1, int liczba2) { if(liczba2 == 0) throw new ArithmeticException("Dzielenie przez zero: " + liczba1 + "/" + liczba2); return liczba1 / liczba2; } public static void main(String args[]) { Dzielenie obj = new Dzielenie(); double wynik = obj.podziel(20, 10); System.out.println("Wynik pierwszego dzielenia: " + wynik); wynik = obj.podziel (20, 0); System.out.println("Wynik drugiego dzielenia: " + wynik); } } W klasie Dzielenie jest zdefiniowana metoda podziel, która przyjmuje dwa argumenty typu int. Ma ona zwracać wynik dzielenia wartości przekazanej w argumencie liczba1 przez wartość przekazaną w argumencie liczba2. Jest zatem jasne, że liczba2 nie może mieć wartości 0. Sprawdzamy to, wykorzystując instrukcję warunkową if. Jeśli okaże się, że liczba2 ma wartość 0, za pomocą instrukcji throw zgłaszamy nowy wyjątek klasy ArithmeticException. W konstruktorze klasy przekazujemy komunikat informujący o dzieleniu przez zero. Podajemy w nim wartości argumentów metody podziel, tak by łatwo można było stwierdzić, jakie parametry spowodowały błąd. W celu przetestowania działania metody podziel w klasie Dzielenie pojawiła się również metoda main. Tworzymy w niej nowy obiekt klasy Dzielenie i odniesienie do niego przypisujemy zmiennej o nazwie obj. Następnie dwukrotnie wywołujemy metodę podziel, raz przekazując jej argumenty równe 20 i 10, drugi raz równe 20 i 0. Można się spodziewać, że w drugim przypadku program zgłosi wyjątek ArithmeticException ze zdefiniowanym przez nas komunikatem. Program zachowa się właśnie tak, co jest widoczne na rysunku 4.11. Rozdział 4. Wyjątki 185 Rysunek 4.11. Zgłoszenie własnego wyjątku klasy ArithmeticException Ponowne zgłoszenie przechwyconego wyjątku Wiadomo już, jak przechwytywać wyjątki oraz jak je samodzielnie zgłaszać. To pozwoli omówić technikę ponownego zgłaszania (potocznie: wyrzucania) już przechwyconego wyjątku. Jak pamiętamy, bloki try…catch można zagnieżdżać bezpośrednio, a także stosować je w przypadku kaskadowo wywoływanych metod. Jeśli jednak na którymkolwiek poziomie przechwytywaliśmy wyjątek, jego obsługa ulegała zakończeniu. Nie zawsze jest to korzystne zachowanie, czasami istnieje potrzeba, aby po wykonaniu bloku obsługi wyjątek nie był niszczony, ale był przekazywany dalej. Aby tak się stało, należy zastosować instrukcję throw. Schematycznie wygląda to następująco: try{ //instrukcje mogące spowodować wyjątek } catch(TypWyjątku identyfikatorWyjątku){ //instrukcje obsługujące sytuację wyjątkową throw indentyfikatorWyjątku } Na listingu 4.17 widać, jak taka sytuacja wygląda w praktyce. Listing 4.17. public class Main { public static void main (String args[]) { try{ int liczba = 10 / 0; } catch(ArithmeticException e){ System.out.println("Tu wyjątek został przechwycony."); throw e; } } } W bloku try jest wykonywana niedozwolona instrukcja dzielenia przez zero. W bloku catch najpierw wyświetlamy na ekranie informację o przechwyceniu wyjątku, a następnie za pomocą instrukcji throw ponownie wyrzucamy (zgłaszamy) przechwycony już wyjątek. Ponieważ w programie nie ma innego bloku try…catch, który mógłby przechwycić ten wyjątek, zostanie on obsłużony standardowo przez maszynę wirtualną. Dlatego też na ekranie pojawi się widok zaprezentowany na rysunku 4.12. 186 Java. Praktyczny kurs Rysunek 4.12. Ponowne zgłoszenie raz przechwyconego wyjątku W przypadku zagnieżdżonych bloków try sytuacja wygląda analogicznie. Wyjątek przechwycony w bloku wewnętrznym i ponownie zgłoszony może być obsłużony w bloku zewnętrznym, w którym może być oczywiście zgłoszony kolejny raz itd. Jest to zobrazowane na listingu 4.18. Listing 4.18. public class Main { public static void main (String args[]) { //tu dowolne instrukcje try{ //tu dowolne instrukcje try{ int liczba = 10 / 0; } catch(ArithmeticException e){ System.out.println("Tu wyjątek został przechwycony pierwszy raz."); throw e; } } catch(ArithmeticException e){ System.out.println("Tu wyjątek został przechwycony drugi raz."); throw e; } } } Mamy tu dwa zagnieżdżone bloki try. W bloku wewnętrznym zostaje wykonana nieprawidłowa instrukcja dzielenia przez zero. Zostaje ona w tym bloku przechwycona, a na ekranie wyświetlany jest komunikat o pierwszym przechwyceniu wyjątku. Następnie wyjątek jest ponownie zgłaszany. W bloku zewnętrznym następuje drugie przechwycenie, wyświetlenie drugiego komunikatu oraz kolejne zgłoszenie wyjątku. Ponieważ nie istnieje trzeci blok try…catch, ostatecznie wyjątek jest obsługiwany przez maszynę wirtualną. Dlatego po uruchomieniu przykładu wynik będzie taki, jak zaprezentowano na rysunku 4.13. Identycznie będą się zachowywały wyjątki w przypadku kaskadowego wywoływania metod. Z sytuacją tego typu mieliśmy do czynienia w przykładzie z listingu 4.8. W klasie Wyjatki były wtedy zadeklarowane cztery metody: main, f, g, h. Metoda main wywoływała metodę f, ta metodę g, a ta z kolei metodę h. W każdej z metod znajdował się blok try…catch, jednak zawsze działał tylko ten najbliższy miejsca wystąpienia wyjątku (lekcja 20.). Zmodyfikujmy kod z listingu 4.8 tak, aby za każdym razem wyjątek był Rozdział 4. Wyjątki 187 Rysunek 4.13. Przechwytywanie i ponowne zgłaszanie wyjątków po przechwyceniu ponownie zgłaszany. Kod realizujący to zadanie jest przedstawiony na listingu 4.19. Listing 4.19. public class Wyjatki { public void f() { try{ g(); } catch(ArrayIndexOutOfBoundsException e){ System.out.println("Wyjątek: metoda f"); throw e; } } public void g() { try{ h(); } catch(ArrayIndexOutOfBoundsException e){ System.out.println("Wyjątek: metoda g"); throw e; } } public void h() { int[] tab = new int[0]; try{ tab[0] = 1; } catch(ArrayIndexOutOfBoundsException e){ System.out.println("Wyjątek: metoda h"); throw e; } } public static void main(String args[]) { Wyjatki wyjatki = new Wyjatki(); try{ wyjatki.f(); } catch(ArrayIndexOutOfBoundsException e){ System.out.println("Wyjątek: metoda main"); throw e; } } } 188 Java. Praktyczny kurs Wszystkie wywołania metod pozostały niezmienione, w każdym bloku catch została natomiast dodana instrukcja throw ponownie zgłaszająca przechwycony wyjątek. Na rysunku 4.14 jest widoczny efekt uruchomienia przedstawionego kodu. Widać wyraźnie, jak wyjątek jest propagowany po wszystkich metodach, począwszy od metody h, a skończywszy na main. Ponieważ w bloku try…catch metody main jest on ponownie zgłaszany, na ekranie widać także reakcję maszyny wirtualnej. Rysunek 4.14. Kaskadowe przechwytywanie wyjątków Tworzenie własnych wyjątków Programując w Javie, nie trzeba zdawać się wyłącznie na wyjątki systemowe, które dostaje się wraz z JDK. Nic nie stoi na przeszkodzie, aby tworzyć własne klasy wyjątków. Wystarczy napisać klasę pochodną pośrednio lub bezpośrednio z klasy Throwable, a będzie można wykorzystywać ją do zgłaszania własnych wyjątków. W praktyce jednak wyjątki zwykle są wyprowadzane z Exception i klas od niej pochodnych. Klasa taka w najprostszej postaci będzie miała postać: public class nazwa_klasy extends Exception { //treść klasy } Przykładowo można utworzyć bardzo prostą klasę o nazwie GeneralException (wyjątek ogólny) w postaci: public class GeneralException extends Exception { } To w zupełności wystarczy. Nie musimy dodawać żadnych nowych pól i metod. Ta klasa jest pełnoprawną klasą obsługującą wyjątki, z której możemy korzystać w taki sam sposób jak ze wszystkich innych klas opisujących wyjątki. Na listingu 4.20 jest widoczna przykładowa klasa Main, która generuje wyjątek GeneralException. Listing 4.20. public class Main { public static void main (String args[]) throws GeneralException { throw new GeneralException(); } } Rozdział 4. Wyjątki 189 W metodzie main za pomocą instrukcji throws zaznaczamy, że metoda ta może zgłaszać wyjątek klasy GeneralException, sam wyjątek zgłaszamy natomiast przez zastosowanie instrukcji throw dokładnie w taki sam sposób jak we wcześniejszych przykładach. Na rysunku 4.15 jest widoczny efekt działania takiego programu: rzeczywiście zgłoszony został wyjątek nowej klasy GeneralException (oczywiście aby przykład udało się skompilować, kod klasy GeneralException należy zapisać w pliku GeneralException.java). Rysunek 4.15. Zgłaszanie własnych wyjątków Własne klasy wyjątków można wyprowadzać również z klas pochodnych od Exception. Można by np. rozszerzyć klasę ArithmeticException o wyjątek zgłaszany wyłącznie wtedy, kiedy wykryte zostanie dzielenie przez zero. Taką klasę można nazwać np. DivideByZeroException. Miałaby ona postać widoczną na listingu 4.21. Listing 4.21. public class DivideByZeroException extends ArithmeticException { } Możemy teraz zmodyfikować program z listingu 4.16 tak, aby po wykryciu dzielenia przez zero był zgłaszany wyjątek nowego typu, czyli DivideByZeroException. Taka klasa została przedstawiona na listingu 4.22. Listing 4.22. public class Dzielenie { public double podziel(int liczba1, int liczba2) { if(liczba2 == 0) throw new DivideByZeroException(); return liczba1 / liczba2; } public static void main(String args[]) { Dzielenie obj = new Dzielenie(); double wynik = obj.podziel(20, 10); System.out.println("Wynik pierwszego dzielenia: " + wynik); wynik = obj.podziel (20, 0); System.out.println("Wynik drugiego dzielenia: " + wynik); } } W stosunku do kodu z listingu 4.16 różnice nie są duże, ograniczają się do zmiany typu zgłaszanego wyjątku w metodzie podziel. Zadaniem tej metody nadal jest zwrócenie wyniku dzielenia argumentu liczba1 przez argument liczba2. W metodzie main dwukrotnie 190 Java. Praktyczny kurs wywołujemy metodę podziel, pierwszy raz z prawidłowymi danymi i drugi raz z tymi, które spowodują wygenerowanie wyjątku. Efekt działania przedstawionego kodu jest widoczny na rysunku 4.16. Rysunek 4.16. Zgłoszenie wyjątku DivideByZeroException Zwróćmy jednak uwagę, że pierwotnie (listing 4.16) przy zgłaszaniu wyjątku argumentem konstruktora był komunikat tekstowy (czyli wartość typu String). Tym razem nie można było zastosować tego typu konstrukcji, gdyż klasa DivideByZeroException nie ma konstruktora przyjmującego ciąg znaków jako argument, ale jedynie bezargumentowy konstruktor domyślny. Aby zatem możliwe było przekazywanie własnych komunikatów, należy dopisać do klasy DivideByZeroException odpowiedni konstruktor (przyjmujący argument typu String). Będzie on miał postać widoczną na listingu 4.23. Listing 4.23. public class DivideByZeroException extends ArithmeticException { public DivideByZeroException(String str) { super(str); } } Teraz instrukcja throw z listingu 4.22 mogłaby wyglądać np. tak: throw new DivideByZeroException("Dzielenie przez zero: " + liczba1 + "/" + liczba2); Sekcja finally Do bloku try można dołączyć sekcję finally, która będzie wykonywana zawsze, niezależnie od tego, co będzie działo się w bloku try. Schemat takiej konstrukcji wygląda następująco: try{ //instrukcje mogące spowodować wyjątek } catch(TypWyjątku identyfikatorWyjątku){ //instrukcje sekcji catch } finally{ //instrukcje sekcji finally } Rozdział 4. Wyjątki 191 Zgodnie z tym, co zostało napisane wcześniej, instrukcje sekcji finally są wykonywane zawsze, niezależnie od tego, czy w bloku try wystąpi wyjątek, czy nie. Zostało to zobrazowane w przykładzie z listingu 4.24, który jest oparty na kodzie z listingów 4.21 i 4.22. Listing 4.24. public class Dzielenie { public double podziel(int liczba1, int liczba2) { if(liczba2 == 0) throw new DivideByZeroException("Dzielenie przez zero: " + liczba1 + "/" + liczba2); return liczba1 / liczba2; } public static void main(String args[]) { Dzielenie obj = new Dzielenie(); double wynik; try{ wynik = obj.podziel(20, 10); } catch(DivideByZeroException e){ System.out.println("Przechwycenie wyjątku 1"); } finally{ System.out.println("Sekcja finally 1"); } try{ wynik = obj.podziel(20, 0); } catch(DivideByZeroException e){ System.out.println("Przechwycenie wyjątku 2"); } finally{ System.out.println("Sekcja finally 2"); } } } Jest to znana nam już klasa Dzielenie z metodą podziel wykonującą dzielenie przekazanych jej argumentów. Tym razem metoda podziel pozostała bez zmian w stosunku do wersji z listingu 4.22, czyli zgłasza błąd DivideByZeroException. Zmodyfikowana została natomiast metoda main. Oba wywołania metody zostały ujęte w bloki try…catch…finally. Pierwsze wywołanie nie powoduje powstania wyjątku, nie jest więc wykonywany pierwszy blok catch, ale jest wykonywany pierwszy blok finally. Tym samym na ekranie pojawia się napis Sekcja finally 1. Drugie wywołanie metody podziel powoduje wygenerowanie wyjątku, zostaną zatem wykonane zarówno instrukcje bloku catch, jak i finally. Na ekranie pojawią się więc dwa napisy: Przechwycenie wyjątku 2 oraz Sekcja finally 2. Ostatecznie wynik działania całego programu będzie taki, jak zaprezentowano na rysunku 4.17. 192 Java. Praktyczny kurs Rysunek 4.17. Blok finally jest wykonywany niezależnie od tego, czy pojawi się wyjątek, czy nie Sekcji finally można użyć również w przypadku instrukcji, które nie powodują wygenerowania wyjątku. Stosuje się wtedy instrukcję try…finally w postaci: try{ //instrukcje } finally{ //instrukcje } Działanie jest takie samo jak w przypadku bloku try…catch…finally, to znaczy kod z bloku finally zostanie wykonany zawsze, niezależnie od tego, jakie instrukcje znajdą się w bloku try. Przykładowo: nawet jeśli w bloku try znajdzie się instrukcja return lub zostanie wygenerowany wyjątek, blok finally i tak zostanie wykonany. Zostało to zobrazowane w przykładzie widocznym na listingu 4.25. Listing 4.25. public class Wyjatki { public int f1() { try{ return 0; } finally{ System.out.println("Sekcja finally f1"); } } public void f2() { try{ int liczba = 10 / 0; } finally{ System.out.println("Sekcja finally f2"); } } public static void main(String args[]) { Wyjatki wyjatki = new Wyjatki (); wyjatki.f1(); wyjatki.f2(); } } Metoda f1 zawiera instrukcję return 0 kończącą wykonywanie metody i zwracającą wartość 0. Ta instrukcja została ujęta w blok try…finally, w którym wyświetlana jest informacja o nazwie metody. Dzięki temu będzie można się przekonać, że nawet prze- Rozdział 4. Wyjątki 193 rwanie wykonywania funkcji nie jest w stanie zapobiec wykonaniu instrukcji z bloku finally. Konstrukcja metody f2 jest bardzo podobna. Różnica jest taka, że w bloku try jest wykonywane dzielenie przez zero, czyli powstaje wyjątek. Wyjątek nie jest jednak przechwytywany w tej metodzie, a zatem zostanie przekazany do metody main, a następnie obsłużony przez maszynę wirtualną. Również w tym przypadku (mimo wygenerowania wyjątku) zostanie wykonany blok finally. Zatem po skompilowaniu i uruchomieniu programu zobaczymy widok przedstawiony na rysunku 4.18. Rysunek 4.18. Efekt działania kodu z listingu 4.25 Ćwiczenia do samodzielnego wykonania Ćwiczenie 22.1. Napisz klasę, w której zostaną zadeklarowane metody f i main. W metodzie f napisz dowolną instrukcję generującą wyjątek NullPointerException. W main wywołaj metodę f i przechwyć wyjątek za pomocą bloku try…catch. Ćwiczenie 22.2. Zmodyfikuj kod z listingu 4.18 tak, aby generowany, przechwytywany i ponownie zgłaszany był wyjątek ArrayIndexOutOfBoundsException. Ćwiczenie 22.3. Napisz klasę o takim układzie metod jak w przypadku klasy Wyjatki z listingu 4.19. W najbardziej zagnieżdżonej metodzie h wygeneruj wyjątek ArithmeticException. Przechwyć go w metodzie g i zgłoś wyjątek klasy nadrzędnej do ArithmeticException, czyli RuntimeException. Wyjątek ten przechwyć w metodzie f i zgłoś wyjątek nadrzędny do RuntimeException, czyli Exception. Ten ostatni wyjątek przechwyć w klasie main. Ćwiczenie 22.4. Napisz klasę wyjątku o nazwie NegativeValueException oraz klasę Main, która będzie z niego korzystać. W klasie Main napisz metodę o nazwie f przyjmującą dwa argumenty typu int. Metoda f powinna zwracać wartość będącą wynikiem odejmowania pierwszego argumentu od drugiego. Gdyby jednak wynik ten był ujemny, powinien zostać zgłoszony wyjątek NegativeValueException. Dopisz metodę main, która przetestuje działanie metody f. 194 Java. Praktyczny kurs Ćwiczenie 22.5. Na podstawie przykładu z listingu 4.1 napisz kod klasy Tablica, w której po wykryciu nieprawidłowego indeksu (zarówno przy zapisie, jak i odczycie danych) będzie generowany wyjątek nowej klasy o nazwie InvalidIndexException (lub podobnej). Przetestuj działanie nowego mechanizmu klasy Tablica, zmieniając odpowiednio klasę Main. Rozdział 5. Programowanie obiektowe. Część II W rozdziale 3. zostały omówione podstawy programowania obiektowego. Była w nim mowa o podstawowych technikach i zasadach niezbędnych do sprawnego programowania w języku Java. Materiał przedstawiony w tym rozdziale pozwoli na znaczne poszerzenie zdobytej dotąd wiedzy. Treść tego rozdziału jest podzielona na trzy główne tematy: polimorfizm, interfejsy oraz klasy wewnętrzne. Obejmują one zagadnienia bardziej zaawansowane niż te, które zostały do tej pory przedstawione, niemniej nie można ich pominąć. Każdy, kto myśli poważnie o programowaniu w Javie, powinien się z nimi zapoznać. W szczególności dotyczy to kwestii polimorfizmu będącego jednym z filarów programowania obiektowego, a także interfejsów, bez których nie da się sprawnie pracować z klasami zawartymi w JDK, o czym będzie mowa później, w rozdziałach 6. i 8. Polimorfizm Lekcja 23. Konwersje typów i rzutowanie obiektów Lekcja 23. jest poświęcona konwersji typów oraz rzutowaniu obiektów. Będzie się można dzięki niej dowiedzieć, czy da się przypisać zmiennej typu int wartość double oraz w jaki sposób są wykonywane domyślne zmiany typów i jak je kontrolować. Zostanie opisana także technika pozwalająca na przypisanie do zmiennej referencyjnej o określonym typie obiektu klasy nadrzędnej lub potomnej w stosunku do tego typu. Znajdzie się tu również wyjaśnienie, jak to się dzieje, że jako argumentu instrukcji System.out.println można użyć dowolnego typu obiektowego. 196 Java. Praktyczny kurs Konwersje typów prostych W lekcji 6. rozważaliśmy, co się wydarzy w programie, jeśli np. wynikiem dzielenia dwóch liczb całkowitych będzie liczba ułamkowa, która zostanie przypisana zmiennej typu całkowitoliczbowego — a zatem w jaki sposób zostanie wykonana przykładowa instrukcja: int liczba = 9 / 2; Okazało się wtedy, że wynik takiego dzielenia zostanie zaokrąglony w dół, czyli zmiennej liczba zostanie w tym przypadku przypisana wartość 4. Czas wyjaśnić to dokładniej. Otóż w takiej sytuacji zostanie wykonana automatyczna konwersja typów danych. Wynik, którym jest liczba zmiennoprzecinkowa typu double (9/2 = 4,5), zostanie skonwertowany na typ int. W efekcie tej konwersji zostanie utracona część ułamkowa i dlatego właśnie zmienna liczba otrzyma wartość 4. A zatem najważniejsza operacja to konwersja typów danych, a zaokrąglenie jest jedynie efektem ubocznym tej konwersji. Programista nie musi zdawać się na konwersje automatyczne, w wielu przypadkach są one wręcz niemożliwe. Często niezbędne jest wykonanie konwersji jawnej. Takiej operacji dokonuje się poprzez umieszczenie przed wyrażeniem nazwy typu docelowego ujętej w nawias okrągły. Schematycznie taka konstrukcja wygląda następująco: (typ_docelowy) wyrażenie; Przykładowo: aby dokonać jawnej konwersji wyniku dzielenia 9 / 2, który jest wartością typu double, na typ int, należy zastosować instrukcję: int liczba = (int) 9 / 2; Można też dokonać jawnej konwersji zmiennej typu double na typ int, np.: double liczba1 = 10.5; int liczba2 = (int) liczba1; Zwróćmy uwagę, że przy tego typu przypisaniu jawna konwersja jest niezbędna — jeśli nie zostanie dokonana, kompilator nie dopuści do kompilacji programu. Aby to sprawdzić, wystarczy użyć programu przedstawionego na listingu 5.1. Widać na nim przykładową klasę Main, w której wartość zmiennej typu double jest przypisywana zmiennej typu int. Kompilator wygeneruje w takim przypadku błąd widoczny na rysunku 5.1. Listing 5.1. public class Main { public static void main (String args[]) { double liczba1 = 0; int liczba2 = liczba1; } } Rozdział 5. Programowanie obiektowe. Część II 197 Rysunek 5.1. Błąd kompilacji spowodowany brakiem jawnej konwersji Mówiąc ogólnie, jeśli zmiennej, która reprezentuje węższy zakres wartości (np. int), przypisuje się zmienną mogącą przedstawiać szerszy zakres wartości (np. double), trzeba zawsze dokonać jawnej konwersji typu (oczywiście o ile taka konwersja jest możliwa). Należy zdawać sobie sprawę, że w sytuacji odwrotnej konwersja typów również występuje, choć najczęściej jest po prostu niezauważalna. Jeśli spróbujemy przypisać zmiennej typu double (reprezentującej szerszy zakres wartości) wartość całkowitą lub wartość zmiennej typu int (reprezentującej węższy zakres wartości), konwersja również zostanie wykonana, tyle że będzie w pełni automatyczna. Można więc spokojnie wykonać instrukcje: int liczba1 = 10; double liczba2 = liczba1; Operacje takie zostaną wykonane poprawnie, warto jednak wiedzieć, że — formalnie rzecz biorąc — kompilator potraktuje ten zapis tak, jakby miał on postać: int liczba1 = 10; double liczba2 = (double) liczba1; Rzutowanie typów obiektowych Typy obiektowe, tak jak typy proste, podlegają konwersjom. Wróćmy na chwilę do końcowej części lekcji 16. Pojawiło się tam stwierdzenie, że jeśli oczekiwany jest argument klasy X, a podany zostanie argument klasy Y, która jest klasą potomną dla X, błędu nie będzie. Dzieje się tak dlatego, że w takiej sytuacji zostanie dokonane tak zwane automatyczne rzutowanie typu obiektu. Wiadomo, że obiekt klasy potomnej zawiera w sobie wszystkie pola i metody klasy bazowej. Można więc powiedzieć, że zawiera już w sobie obiekt tej klasy. W związku z tym nie ma żadnych przeciwwskazań, aby w miejscu, gdzie powinien się znaleźć obiekt klasy bazowej, umieścić obiekt klasy potomnej. Weźmy dla przykładu dobrze nam znane z rozdziału 3. klasy Punkt i Punkt3D w postaci zaprezentowanej na listingu 5.2. Listing 5.2. public class Punkt { public int x; public int y; } public class Punkt3D extends Punkt { 198 Java. Praktyczny kurs public int z; } Jeśli zadeklarujemy teraz zmienną typu Punkt, to będzie można przypisać jej nowy obiekt klasy Punkt3D (dokładniej: odniesienie do nowego obiektu klasy Punkt3D). Poprawne zatem będą instrukcje: Punkt punkt; punkt = new Punkt3D(); Oczywiście odwołując się teraz w standardowy sposób do metod i pól obiektu wskazywanego przez zmienną punkt, można uzyskać dostęp jedynie do metod zdefiniowanych w klasie Punkt. W związku z tym niepoprawne będzie np. odwołanie: punkt.z = 10; Zostało to zobrazowane w kodzie z listingu 5.3. Próba jego kompilacji zakończy się błędem widocznym na rysunku 5.2. Listing 5.3. public class Main { public static void main (String args[]) { Punkt punkt; punkt = new Punkt3D(); punkt.z = 10; } } Rysunek 5.2. Błędne odwołanie do nieistniejącego w klasie Punkt pola z Nie powinno to dziwić. Dla kompilatora typem zmiennej punkt jest Punkt (potocznie mówimy, że „zmienna jest klasy Punkt”), a w klasie Punkt nie ma pola z, dlatego też wyświetlane są komunikaty o błędzie. W rzeczywistości instrukcja: punkt = new Punkt3D(); jest przez kompilator rozumiana jako: punkt = (Punkt) new Punkt3D(); Rozdział 5. Programowanie obiektowe. Część II 199 Przypomina to konwersje typów prostych, jednak znaczenie tego zapisu jest nieco inne. Zostało tu wykonane rzutowanie wskazania do obiektu klasy Punkt3D na klasę Punkt. Jest to informacja dla kompilatora: traktuj zmienną punkt wskazującą obiekt klasy Punkt3D tak, jakby wskazywała obiekt klasy Punkt, a w uproszczeniu: traktuj obiekt klasy Punkt3D tak, jak gdyby był on klasy Punkt. Obiekt Punkt3D nie zmienia się jednak ani nie traci żadnych informacji, jest po prostu inaczej traktowany. Rzutowania można dokonać również w przypadku zmiennych już istniejących, np.: Punkt3D punkt3D = new Punkt3D(); Punkt punkt = (Punkt) punkt3D; Jest to też doskonały dowód, że dokonuje się tu rzutowania typów, a nie konwersji. Warto przypomnieć, że w przypadku typów prostych konwersja typu bardziej ogólnego na bardziej szczegółowy powodowała utratę części informacji. Przykładowo wartość 4,5 typu double po konwersji na typ int zmieniała się na 4. W takiej sytuacji część ułamkowa zostanie bezpowrotnie utracona i nawet powtórna konwersja na typ double nie przywróci poprzedniej wartości. Zobrazowano to w przykładzie widocznym na listingu 5.4. Listing 5.4. public class Main { public static void main (String args[]) { double x = 4.5; int y = (int) x; double z = (double) y; System.out.println("x = " + x); System.out.println("y = " + y); System.out.println("z = " + z); } } Deklarujemy zmienną x typu double i przypisujemy jej wartość 4,5. Następnie deklarujemy zmienną y typu int oraz przypisujemy jej wartość konwersji zmiennej x na typ int (int y = (int) x;). W kroku trzecim deklarujemy zmienną z typu double, której przypisujemy wynik konwersji zmiennej y na typ double. Na zakończenie wyświetlamy wartości wszystkich trzech zmiennych na ekranie (rysunek 5.3). Widać wyraźnie, że nie da się odzyskać informacji utraconej przez konwersję typu double na typ int. Rysunek 5.3. Utrata informacji w wyniku konwersji typów prostych W przypadku zmiennych obiektowych będzie zupełnie inaczej, co wynika z ich właściwości. Na listingu 5.5 widoczny jest przykład obrazujący wyniki rzutowania obiektów. 200 Java. Praktyczny kurs Listing 5.5. public class Main { public static void main (String args[]) { Punkt3D punkt3D1 = new Punkt3D(); punkt3D1.x = 10; punkt3D1.y = 20; punkt3D1.z = 30; System.out.println("punkt3D1"); System.out.println("x = " + punkt3D1.x); System.out.println("y = " + punkt3D1.y); System.out.println("z = " + punkt3D1.z); System.out.println(""); Punkt punkt = (Punkt) punkt3D1; System.out.println("punkt"); System.out.println("x = " + punkt.x); System.out.println("y = " + punkt.y); System.out.println(""); Punkt3D punkt3D2 = (Punkt3D) punkt; System.out.println("punkt3D2"); System.out.println("x = " + punkt3D2.x); System.out.println("y = " + punkt3D2.y); System.out.println("z = " + punkt3D2.z); } } Tworzymy obiekt klasy Punkt3D i przypisujemy go zmiennej punkt3D1 (Punkt3D punkt3D1 = new Punkt3D();) oraz ustalamy wartości jego pól x, y, z na 10, 20 i 30. Następnie wyświetlamy te informacje na ekranie. Dalej tworzymy zmienną punkt klasy Punkt i za pomocą techniki rzutowania przypisujemy jej obiekt, na który wskazuje punkt3D1 (Punkt punkt = (Punkt) punkt3D1;). Zawartość pól wyświetlamy na ekranie. Oczywiście ponieważ typem zmiennej punkt jest Punkt, mamy dostęp jedynie do pól x oraz y. Pole z nie jest dostępne, nie można modyfikować ani odczytywać jego wartości. To jednak nie wszystko. W kolejnym kroku dokonujemy jeszcze jednego rzutowania. Obiekt wskazywany przez punkt przypisujemy zmiennej punkt3D2, która jest klasy Punkt3D. Następnie wyświetlamy zawartość wszystkich pól. Te wartości to 10, 20 i 30. Ostatecznie na ekranie pojawi się widok zaprezentowany na rysunku 5.4. Jest już zatem jasne, że rzutowanie nie zmienia stanu obiektu. W przykładzie utworzony został tylko JEDEN obiekt klasy Punkt3D. Żadna z operacji nie zmieniła typu tego obiektu, nie odbyły się żadne konwersje, które mogłyby doprowadzić do utraty części danych. Powstały natomiast trzy zmienne, przez które spoglądaliśmy na obiekt. Zmienne punkt3D1 i punkt3D2 traktowały go jako obiekt klasy Punkt3D, a zmienna punkt jako obiekt klasy Punkt. Schematycznie przedstawiono tę sytuację na rysunku 5.5. Można zatem powiedzieć, że rzutowanie pozwala spojrzeć na dany obiekt z innej, szerszej lub węższej, perspektywy. Rozdział 5. Programowanie obiektowe. Część II 201 Rysunek 5.4. Ilustracja rzutowania typów obiektowych Rysunek 5.5. Schematyczne przedstawienie zmiennych i obiektu z listingu 5.5 Przykład z listingu 5.5 pokazał również, że rzutowanie obiektów jest możliwe w obie strony, to znaczy obiekt klasy bazowej można rzutować na obiekt klasy potomnej, a obiekt klasy potomnej na obiekt klasy bazowej. Przypadek drugi jest oczywisty, wiadomo już, że obiekt klasy potomnej zawiera w sobie obiekt klasy bazowej. Sytuacja odwrotna jest jednak bardziej skomplikowana. Jak np. potraktować poniższy fragment kodu? Punkt3D punkt3D = (Punkt3D) new Punkt(); punkt3D.z = 10; Czy może on zostać wykonany? Pozostawmy ten przykład do przemyślenia, wrócimy do niego na początku lekcji 24. Rzutowanie na typ Object W Javie wszystkie klasy dziedziczą bezpośrednio lub pośrednio po klasie Object. Nie ma pod tym względem wyjątków. Nawet jeśli definicja nowej klasy bazowej wygląda tak jak w dotychczas przedstawionych przykładach, czyli: 202 Java. Praktyczny kurs public class nazwa_klasy { } to kompilator potraktuje ten fragment kodu jako: public class nazwa_klasy extends Object { } Zatem klasa Object jest praklasą, z której wywodzą się wszystkie inne klasy w Javie. Oznacza to, że każda klasa dziedziczy wszystkie metody i pola klasy Object. Jedną z takich metod jest toString. Często programista nie zdaje sobie nawet sprawy, że jest ona wykonywana. Przypomnijmy sobie np. sposób, w jaki wyświetlany był systemowy komunikat o typie wyjątku w lekcjach z rozdziału 4. Stosowaliśmy m.in. następującą konstrukcję: try{ //instrukcje mogące spowodować wyjątek ArithmeticException } catch(ArithmeticException e){ System.out.println(e); } Zastanawialiśmy się wtedy, jak to się dzieje, że jako argument metody println1 można przekazać obiekt klasy wyjątku, w powyższym przypadku obiekt klasy Arithmetic Exception. Po sprawdzeniu w dokumentacji JDK okazuje się, że istnieje wiele przeciążonych wersji metody println, żadna jednak nie przyjmuje argumentu typu ArithmeticException. Nie istnieje też domyślna konwersja żadnego typu obiektowego na typ String. Wytłumaczenie jest na szczęście bardzo proste. Istnieje przeciążona wersja metody println, która przyjmuje jako argument obiekt klasy Object. W klasie Object istnieje natomiast metoda o nazwie toString, która zwraca opis obiektu w postaci ciągu znaków2. Metoda println wywołuje natomiast metodę toString w celu uzyskania ciągu znaków. Oczywiście metoda toString zdefiniowana w klasie Object nie jest w stanie dostarczyć komunikatu o typie zgłoszonego wyjątku ArithmeticException. Jednak komunikaty z klas potomnych są uzyskiwane dzięki przesłanianiu metody toString w tych klasach. Jak to będzie wyglądało w konkretnym przykładzie, widać na listingu 5.6. Listing 5.6. public class MojaKlasa { public String toString() { return "Jestem obiektem klasy MojaKlasa"; } public static void main(String args[]) { MojaKlasa mk = new MojaKlasa(); System.out.println(mk); } } 1 Po wyjaśnieniach z rozdziału 3. łatwo się domyślić, że instrukcja System.out.println to nic innego niż wywołanie metody println obiektu out zdefiniowanego w klasie System. 2 Formalnie rzecz biorąc, w postaci obiektu klasy String. Rozdział 5. Programowanie obiektowe. Część II 203 Jest to klasa MojaKlasa zawierająca publiczną metodę toString. Metoda ta nadpisuje metodę toString z klasy Object. Jedynym jej zadaniem jest wyświetlenie na ekranie napisu widocznego na rysunku 5.6. W metodzie main tworzymy nowy obiekt klasy MojaKlasa i przekazujemy go jako argument dla instrukcji System.out.println. Dzięki temu można się naocznie przekonać, że metoda toString zostanie rzeczywiście wykonana. Rysunek 5.6. Efekt działania przeciążonej metody toString z klasy MojaKlasa Metodę tę można również wywołać jawnie, stosując konstrukcję: System.out.println(mk.toString()); Warto jednak zauważyć, że w tej chwili jest wykorzystywana inna wersja metody println. W poprzednim wypadku została zastosowana wersja przyjmująca jako argument obiekt klasy Object, a tym razem ta metoda przyjmuje obiekt klasy String. Można byłoby więc tę instrukcję rozbić na dwie następujące linie: String komunikat = mk.toString(); System.out.println(komunikat); Jeśli w MojaKlasa nie zostanie zdefiniowana metoda toString, to również będzie można zastosować obiekt tej klasy jako argument metody println. Zostanie wtedy wywołana metoda toString z klasy Object. Komunikat będzie jednak wówczas mało zrozumiały. Taka klasa jest przedstawiona na listingu 5.7, a efekt jej działania widać na rysunku 5.7 (ciąg znaków występujący po @ może być inny). Listing 5.7. public class MojaKlasa { public static void main(String args[]) { MojaKlasa mk = new MojaKlasa(); System.out.println(mk); } } Rysunek 5.7. Efekt wywołania domyślnej metody toString pochodzącej z klasy Object Ćwiczenia do samodzielnego wykonania Ćwiczenie 23.1. Popraw kod z listingu 5.1 tak, aby jego kompilacja była możliwa. 204 Java. Praktyczny kurs Ćwiczenie 23.2. Napisz klasy Bazowa i Pochodna (dziedziczącą po Bazowa) oraz testową klasę Main. W klasie Main utwórz obiekt klasy Pochodna i przypisz go zmiennej typu Bazowa o nazwie bazowa. Następnie zadeklaruj dodatkową zmienną typu Pochodna i przypisz jej obiekt wskazywany przez zmienną bazowa. Ćwiczenie 23.3. Zmodyfikuj kod z listingu 5.6 tak, aby metoda toString z klasy MojaKlasa wyświetlała również komunikat zwracany przez klasę nadrzędną. Ćwiczenie 23.4. Zmodyfikuj kod klasy Punkt z listingu 3.1 z rozdziału 3. tak, by po użyciu obiektu tej klasy jako argumentu metody println na ekranie były wyświetlane współrzędne punktu. Dodatkowo spraw, aby ta klasa pakietowa stała się publiczna, a także by oba jej pola były publiczne. Ćwiczenie 23.5. Napisz klasę Main, która przetestuje działanie metody toString klasy Punkt powstałej w ćwiczeniu 23.4. Lekcja 24. Późne wiązanie i wywoływanie metod klas pochodnych W lekcji 23. pojawiły się pojęcia konwersji oraz rzutowania typów danych. Wiadomo zatem już, że obiekt danej klasy można potraktować jak obiekt klasy nadrzędnej lub podrzędnej i nie powoduje to straty żadnych zapisanych w nim informacji. W lekcji 24. zostanie pokazane, jak w Javie wywoływane są metody oraz kiedy dochodzi do rzutowania typów; pojawi się tu też pojęcie polimorfizmu, jedno z głównych pojęć w programowaniu obiektowym. Rzeczywisty typ obiektu Wiadomo, że możliwe jest rzutowanie obiektu na typ bazowy (tzw. rzutowanie w górę), czyli np. wykorzystywane w lekcji 23. rzutowanie obiektu klasy Punkt3D na klasę Punkt. Zostanie ono opisane dokładniej już za chwilę. Powróćmy tymczasem do tematu rzutowania w dół, który również pojawił się w poprzedniej lekcji. Rzutowanie w dół to rzutowanie typu klasy bazowej na typ klasy pochodnej, tak jak w poniższym fragmencie kodu: Punkt3D punkt3D = (Punkt3D) new Punkt(); punkt3D.z = 10; Taką konstrukcję można zapisać również przy użyciu dodatkowej zmiennej klasy Punkt: Rozdział 5. Programowanie obiektowe. Część II 205 Punkt punkt = new Punkt(); Punkt3D punkt3D = (Punkt3D) punkt; punkt3D.z = 10; Powstaje tu obiekt klasy Punkt. W pierwszym przypadku jest on bezpośrednio rzutowany na klasę Punkt3D i przypisywany zmiennej punkt3D, natomiast w drugim najpierw jest przypisywany zmiennej punkt klasy Punkt, a dopiero potem zawartość tej zmiennej jest rzutowana na klasę Punkt3D i przypisywana zmiennej punkt3D. W obu przypadkach zmienna punkt3D, której typem jest Punkt3D, wskazuje zatem na obiekt klasy Punkt. Schematycznie zostało to zobrazowane na rysunku 5.8. Rysunek 5.8. Zmienna punkt klasy Punkt3D wskazuje na obiekt klasy Punkt Czy można wykonać instrukcję punkt3D.z = 10? Odpowiedź oczywiście musi brzmieć: nie! Obiekt klasy Punkt nie ma pola o nazwie z, nie ma zatem możliwości takiego przypisania. Czy uda się w związku z tym skompilować kod widoczny na listingu 5.8? Odpowiedź brzmi… tak. Listing 5.8. public class Main { public static void main(String args[]) { Punkt punkt = new Punkt(); Punkt3D punkt3D = (Punkt3D) punkt; punkt3D.z = 10; } } Ten program jest syntaktycznie (składniowo) poprawny. Wolno dokonywać rzutowania klasy Punkt na klasę Punkt3D (por. listing 5.5), program w żadnym wypadku nie zostanie jednak poprawnie wykonany. Kompilator nie może „zakładać” złej woli programisty i „nie wie”, że w rzeczywistości punkt3D wskazuje na obiekt, w którym brakuje pola z, czyli obiekt nieprawidłowej klasy. Jednak w trakcie wykonania programu takie sprawdzenie nastąpi i zostanie zgłoszony błąd (wyjątek) ClassCastException. Jest on widoczny na rysunku 5.9. Oczywiście wyjątek ten można przechwycić (por. lekcje z rozdziału 4.). 206 Java. Praktyczny kurs Rysunek 5.9. Próba odwołania się do nieistniejącego w klasie Punkt pola o nazwie z Spróbujmy teraz odwołać się do jednego z istniejących w klasie Punkt pól x lub y, czyli wykonać instrukcje: Punkt punkt = new Punkt(); Punkt3D punkt3D = (Punkt3D) punkt; punkt3D.x = 10; Czy tym razem program zadziała? Otóż spotka nas tu niespodzianka: reakcja będzie identyczna z tą, jaką widzieliśmy w poprzednim przypadku, zostanie wygenerowany wyjątek ClassCastException (rysunek 5.9). Stanie się tak, mimo że obiekt, na który wskazuje zmienna punkt3D, zawiera pole x. To jednak nie ma znaczenia, gdyż podstawowym problemem jest to, że sam obiekt jest innej klasy niż Punkt3D. Maszyna wirtualna w trakcie wykonania programu dokonuje sprawdzenia zgodności klas. Wykonywanie operacji na obiekcie okazuje się możliwe tylko wtedy, kiedy zmienna wskazująca na ten obiekt jest klasy tego obiektu lub klasy nadrzędnej do klasy tego obiektu — nigdy odwrotnie. Jeżeli opisywana zgodność nie następuje, w trakcie wykonania zostanie wygenerowany błąd ClassCastException, tak jak miało to miejsce w ostatnich przykładach. Jest on generowany już przy próbie wykonania instrukcji: Punkt3D punkt3D = (Punkt3D) punkt; nic więc dziwnego, że nie sposób wykonać żadnej instrukcji modyfikującej pola obiektu, niezależnie od tego, czy są one w nim zawarte, czy też nie. Typ obiektu a rzutowanie w górę Zaprezentowane przed chwilą sprawdzanie rzeczywistego (a nie deklarowanego) typu obiektu w trakcie działania programu to właśnie polimorfizm, który jest tematem nadrzędnym bieżących lekcji. Polimorfizm jest nazywany także późnym wiązaniem (ang. late binding), wiązaniem czasu wykonania (ang. runtime binding) czy wiązaniem dynamicznym (ang. dynamic binding). W przypadku rzutowania w dół, omawianego w poprzednim podpunkcie, polimorfizm uniemożliwia wykonanie niedozwolonych operacji. O wiele bardziej użyteczny jest jednak przy rzutowaniu w górę, czyli na klasę nadrzędną. Załóżmy, że zostały utworzone trzy klasy: MojaKlasa oraz dziedziczące po niej Moja DrugaKlasa i MojaTrzeciaKlasa. We wszystkich zostanie zdefiniowana metoda toString, która będzie wyświetlała nazwę danej klasy. Całość przyjmie postać widoczną na listingu 5.9. Rozdział 5. Programowanie obiektowe. Część II 207 Listing 5.9. public class MojaKlasa { public String toString() { return "MojaKlasa"; } } public class MojaDrugaKlasa extends MojaKlasa { public String toString() { return "MojaDrugaKlasa"; } } public class MojaTrzeciaKlasa extends MojaKlasa { public String toString() { return "MojaTrzeciaKlasa"; } } Konstrukcja tych klas nie powinna budzić żadnych wątpliwości. Wykorzystywane jest tu typowe dziedziczenie oraz metoda toString w standardowej postaci. Pytanie brzmi: co się stanie po utworzeniu obiektu klasy MojaDrugaKlasa lub MojaTrzeciaKlasa, przypisaniu zmiennej typu MojaKlasa odniesienia do tego obiektu i wywołaniu metody toString? Inaczej: co pojawi się na ekranie po wykonaniu przykładowego programu widocznego na listingu 5.10? Listing 5.10. public class Main { public static void main(String args[]) { MojaKlasa mk1 = new MojaDrugaKlasa (); MojaKlasa mk2 = new MojaTrzeciaKlasa (); System.out.println(mk1.toString()); System.out.println(mk2.toString()); } } Tworzymy tu dwie zmienne typu (klasy) MojaKlasa. Pierwszej z nich przypisujemy referencję do nowego obiektu klasy MojaDrugaKlasa, drugiej — do obiektu klasy Moja TrzeciaKlasa. Można tego dokonać, jako że obie klasy dziedziczą po MojaKlasa. Jest tu wykorzystywane rzutowanie domyślne. Formalnie można by ten kod zapisać również jako: MojaKlasa mk1 = (MojaKlasa) new MojaDrugaKlasa (); MojaKlasa mk2 = (MojaKlasa) new MojaTrzeciaKlasa (); Wykonujemy następnie dwie instrukcje System.out.println, przekazując im w postaci argumentów wartości zwrócone przez wywołanie metody toString obiektów mk1 i mk2. Wydawać by się mogło, że skoro typem obu zmiennych jest MojaKlasa, to zostaną potraktowane tak, jakby były obiektami tej klasy, i na ekranie dwukrotnie pojawi się napis MojaKlasa. 208 Java. Praktyczny kurs Nic bardziej mylnego! W Javie wywołania metod są domyślnie polimorficzne, to znaczy, że skojarzenie obiektu z metodą jest wykonywane w trakcie działania programu. Tym samym sprawdzeniu podlega rzeczywisty typ obiektu, którego metoda jest wywoływana. Ponieważ rzeczywistym typem obiektu dla mk1 jest MojaDrugaKlasa, a dla mk2 — MojaTrzeciaKlasa, na ekranie pojawi się widok zaprezentowany na rysunku 5.10. Rysunek 5.10. Wynik polimorficznego wywołania metody toString z listingów 5.9 i 5.10 Widać wyraźnie, że zostały wywołane metody toString z klas MojaDrugaKlasa oraz MojaTrzeciaKlasa. Warto zauważyć, że gdyby wiązanie, czyli skojarzenie wywołania metody z obiektem, odbywało się już na etapie kompilacji (tak zwane wczesne wiązanie, ang. early binding), takie wywołanie nie mogłoby mieć miejsca. W trakcie kompilacji oba obiekty, mk1 i mk2, są dla kompilatora obiektami klasy MojaKlasa, więc przypisałby on im kod metody toString z klasy MojaKlasa. Dzięki wywołaniom polimorficznym wiązanie następuje dopiero w trakcie działania programu i pod uwagę jest brany rzeczywisty typ obiektu. To jedna z podstawowych zasad programowania obiektowego, bardzo użyteczna przy pisaniu rzeczywistych aplikacji. Wykonajmy jeszcze jeden przykład. Przy założeniu, że mamy klasę Shape, która jest nadrzędna dla innych klas opisujących figury geometryczne, wyprowadzimy z niej klasy Circle, Rectangle oraz Triangle. W każdej z nich umieścimy metodę draw. Choć w praktyce metoda ta rysowałaby zapewne daną figurę na ekranie, ograniczmy się jedynie do wyświetlenia nazwy. Taki zestaw klas jest widoczny na listingu 5.11. Listing 5.11. public class Shape { public void draw() { System.out.println("Shape"); } } public class Circle extends Shape { public void draw() { System.out.println("Circle"); } } public class Triangle extends Shape { public void draw() { System.out.println("Triangle"); } } public class Rectangle extends Shape { public void draw() { System.out.println("Rectangle"); } } Rozdział 5. Programowanie obiektowe. Część II 209 Struktura przedstawionych klas przypomina poprzednio omawiany przykład. Klasą główną jest Shape (kształt), po niej dziedziczą klasy Triangle (trójkąt), Circle (okrąg) i Rectangle (prostokąt). Każda z wymienionych klas ma zdefiniowaną własną metodę draw, której zadaniem, zgodnie z tym, co zostało napisane powyżej, będzie wyświetlenie na ekranie nazwy klasy. Załóżmy teraz, że do napisania jest klasa Main, w której znajdzie się metoda drawShape rysująca figury. To znaczy metoda taka miałaby przyjmować argument będący obiektem danej klasy i wywoływać jego metodę draw. Bez wywołań polimorficznych niezbędne byłoby napisanie wielu wersji przeciążonej metody drawShape. Jedna przyjmowałaby argumenty klasy Triangle, inna Rectangle, jeszcze inna Circle. Co więcej, utworzenie nowej klasy dziedziczącej po Shape wymagałoby dopisania kolejnej przeciążonej metody drawShape w klasie Main. Tymczasem polimorfizm pozwala napisać tylko jedną metodę drawShape w klasie Main i będzie ona pasować do każdej kolejnej klasy wyprowadzonej z Shape. Zostało to zobrazowane w programie widocznym na listingu 5.12. Listing 5.12. public class Main { public void drawShape(Shape shape) { shape.draw(); } public static void main(String args[]) { Main main = new Main(); Circle circle = new Circle(); Triangle triangle = new Triangle(); Rectangle rectangle = new Rectangle(); main.drawShape(circle); main.drawShape(triangle); main.drawShape(rectangle); } } W klasie Main istnieje tylko jedna metoda drawShape, która jako argument przyjmuje obiekt typu Shape. Będzie więc mogła również otrzymać jako argument obiekt dowolnej klasy dziedziczącej po Shape. W samej metodzie następuje wywołanie metody draw obiektu będącego argumentem. Będzie to oczywiście wywołanie polimorficzne, zatem zostanie wywołana metoda wynikająca z rzeczywistej klasy obiektu (Triangle, Circle lub Rectangle), a nie ta pochodząca z klasy Shape. Dalsza część programu potwierdza takie właśnie zachowanie kodu. W metodzie main tworzymy bowiem obiekty klas Circle, Triangle oraz Rectangle i przekazujemy je jako argumenty metodzie drawShape klasy Main. Wynik działania, zgodny z naszymi oczekiwaniami, jest widoczny na rysunku 5.11. Warto się teraz zastanowić, co by się stało, gdyby w jednej z klas dziedziczących po Shape nie było zdefiniowanej metody draw. Jaki będzie wynik działania programu z listingu 5.12, jeśli np. klasa Triangle z listingu 5.11 przyjmie pokazaną poniżej postać? public class Triangle extends Shape { } 210 Java. Praktyczny kurs Rysunek 5.11. Polimorficzne wywołania metody drawShape Efekt jest widoczny na rysunku 5.12. Zamiast napisu Triangle pojawił się napis Shape. To nie powinno dziwić, bo metoda draw z klasy Triangle przesłaniała metodę draw zdefiniowaną w klasie Shape. Kiedy z definicji klasy Triangle usunięta została metoda draw, doszło do odsłonięcia metody draw odziedziczonej po klasie nadrzędnej, czyli Shape. Została zatem wykonana metoda draw obiektu klasy Triangle, tak jak w poprzednim przypadku, tyle że była to metoda odziedziczona po klasie bazowej i stąd też na ekranie pojawił się napis Shape. Rysunek 5.12. Z klasy Triangle została usunięta metoda draw Metody prywatne Przy wykorzystywaniu polimorficznych wywołań metod należy pamiętać, że mają one miejsce, kiedy metoda X z klasy bazowej jest przesłaniana przez metodę X z klasy potomnej. Problem może wystąpić w sytuacji, kiedy w klasie bazowej zostanie zdefiniowana metoda prywatna. Taka metoda nie może być bowiem przesłonięta. Jest to oczywiste, bo przecież metody prywatne (lekcja 17.) są dostępne wyłącznie dla klasy, w której zostały zdefiniowane. Łatwo tu jednak wpaść w pewną pułapkę. Otóż to, że dana metoda została zadeklarowana w klasie bazowej jako prywatna, nie oznacza wcale, że nie można zdefiniować metody o takiej samej nazwie i argumentach w klasie pochodnej. Choć brzmi to może paradoksalnie, taka możliwość naprawdę istnieje, ale nie zaleca się stosowania tego typu konstrukcji, gdyż zaciemnia to sposób działania kodu. Trzeba natomiast wiedzieć, co mogłoby się dziać w tego typu sytuacji, gdyby się jednak przytrafiła. Co będzie się działo w sytuacji przedstawionej na listingu 5.13? Listing 5.13. public class A { private void f() { System.out.println("Metoda f klasy A"); } } public class B extends A { public void f() { System.out.println("Metoda f klasy B"); } } Rozdział 5. Programowanie obiektowe. Część II 211 Na listingu są dwie klasy: A zawierająca prywatną metodę f oraz dziedzicząca po niej B zawierająca publiczną metodę, również o nazwie f. Obie klasy uda się bez problemu skompilować. W tym miejscu pojawi się zapewne pytanie: czemu jest to możliwe? Otóż prawdą jest, że do metod prywatnych klasy bazowej nie można się odwoływać w klasach potomnych (lekcja 17.). Aby się o tym szybko przekonać, wystarczy spróbować w metodzie f klasy B wywołać metodę f klasy nadrzędnej (A) za pomocą składni super, czyli umieścić w niej instrukcję: super.f(); Tego typu przykład był prezentowany już w rozdziale 3. Jednak w powyższym przypadku tak naprawdę w klasie B została zdefiniowana zupełnie nowa metoda f, która nie ma nic wspólnego z metodą f klasy A. To z kolei ma poważne implikacje praktyczne, związane z polimorficznym wywoływaniem metod. Aby to sprawdzić, dopiszmy do klasy A metodę main zawierającą instrukcje: A a = (A) new B(); a.f(); Deklarujemy tu zmienną a klasy A i przypisujemy jej odniesienie do nowo utworzonego obiektu klasy B. Dokonujemy przy tym jawnego rzutowania na klasę A, chociaż — formalnie rzecz biorąc — nie jest to w tym miejscu konieczne. Identyczny skutek będzie miało zastosowanie instrukcji w postaci: A a = new B(); Klasa A przyjmie zatem postać widoczną na listingu 5.14. Listing 5.14. public class A { private void f() { System.out.println("Metoda f klasy A"); } public static void main(String args[]) { A a = (A) new B(); a.f(); } } Jaki napis pojawi się na ekranie? Na podstawie tego, co już zostało powiedziane o wywołaniach polimorficznych, wydawać by się mogło, że będzie to tekst Metoda f klasy B. Skoro bowiem rzeczywistym typem obiektu jest B, a zostanie to sprawdzone w trakcie wykonywania programu, wydaje się oczywiste, że powinna zostać wykonana metoda f klasy B. Tymczasem uruchomienie klasy A da wynik widoczny na rysunku 5.13. Jak widać, została wykonana metoda f klasy A. Rysunek 5.13. Niespodziewane zachowanie klasy z metodą prywatną 212 Java. Praktyczny kurs Co się stało? Otóż gdyby obie metody f były publiczne, czyli metoda f z klasy B przesłaniałaby metodę f z klasy A, zachowanie programu okazałoby się zgodne z wcześniejszymi oczekiwaniami. Wywołanie polimorficzne spowodowałoby, że choć zmienna a jest typu A, zostałaby wykonana metoda f klasy B, jako że sam obiekt jest typu B. Tymczasem ponieważ metoda f z klasy A jest prywatna, a tym samym finalna, maszyna wirtualna nie ma potrzeby sprawdzania, czy istnieje jej wersja w klasie B (bo taka wersja nie może istnieć), czyli zachowuje się tak, jakby metody f w klasie B nie było. Aby uniknąć tego typu problemów, najlepiej stosować się do zasady, że w klasach potomnych nie używa się metod o takich samych nazwach jak nazwy metod prywatnych w klasie bazowej. Nie ma praktycznej potrzeby stosowania tego typu konstrukcji, a ich unikanie pozwoli również ustrzec się niepotrzebnych problemów i niejasności w konstrukcji kodu programów. Ćwiczenia do samodzielnego wykonania Ćwiczenie 24.1. Popraw kod z listingu 5.8 tak, aby po wystąpieniu wyjątku nie następowało niekontrolowane zakończenie działania programu. Zastosuj właściwy blok try…catch. Ćwiczenie 24.2. Zmodyfikuj kod z listingu 5.10 tak, by metoda toString nie była jawnie wywoływana. Zachowanie programu ma pozostać bez zmian. Ćwiczenie 24.3. Zmodyfikuj kod klas z listingu 5.11 tak, aby wywołanie metody draw powodowało wyświetlenie odpowiedniej figury geometrycznej zbudowanej z dowolnych znaków semigraficznych. Ćwiczenie 24.4. Popraw kod klas z listingów 5.13 tak, by wykonanie kodu metody main z listingu 5.14 powodowało wywołanie metody f z klasy B. Lekcja 25. Konstruktory oraz klasy abstrakcyjne Lekcja 25. jest poświęcona klasom abstrakcyjnym oraz problematyce zachowań konstruktorów w specyficznych sytuacjach związanych z wywołaniami polimorficznymi. Okaże się zatem, co to są klasy i metody abstrakcyjne i jak je stosować w praktyce. Znajdą się tu też informacje o kolejności wywoływania konstruktorów w hierarchii klas, a także o problemach, jakie można napotkać podczas wywoływania w konstruktorach innych metod danej klasy. Rozdział 5. Programowanie obiektowe. Część II 213 Klasy abstrakcyjne W poprzedniej lekcji był wykorzystywany zestaw klas opisujących figury geometryczne, które dziedziczyły po wspólnej klasie bazowej Shape. Klasy te zostały przedstawione na listingu 5.11. W tego typu przypadkach klasa bazowa często jest tylko atrapą, która w rzeczywistości nie wykonuje żadnych zadań, służy jedynie do zdefiniowania zestawu metod, jakimi będą posługiwały się klasy potomne, oraz udostępnienia udogodnień, jakie niesie możliwość korzystania z wywołań polimorficznych (jak w przykładach z lekcji 24.). W takich sytuacjach często nie ma potrzeby lub jest wręcz niewskazane, aby były tworzone obiekty klasy bazowej. W przypadku zwykłych klas nie można jednak nikomu zabronić tworzenia ich instancji3, taką możliwość dają natomiast klasy abstrakcyjne. Klasa abstrakcyjna to taka klasa, która została zadeklarowana z użyciem słowa kluczowego abstract. Przy czym klasa, w której przynajmniej jedna metoda jest abstrakcyjna (oznaczona słowem kluczowym abstract), MUSI być zadeklarowana jako abstrakcyjna4. Schemat takiej konstrukcji wygląda następująco: [public] abstract class nazwa_klasy { [specyfikator_dostępu] abstract typ_zwracany nazwa_metody(argumenty); } Metoda abstrakcyjna ma jedynie definicję, nie może zawierać żadnego kodu (nie wolno użyć nawet pustego nawiasu klamrowego; deklarację należy zakończyć średnikiem). Przykładowo metoda draw z klasy Shape z zestawu klas z listingu 5.11 mogłaby być z powodzeniem metodą abstrakcyjną. Zgodnie z powyższym opisem klasa Shape w takim wypadku również musiałaby być abstrakcyjna. Cała konstrukcja miałaby następującą postać: public abstract class Shape { public abstract void draw(); } Po takiej deklaracji nie będzie można tworzyć obiektów klasy Shape. Próba wykonania przykładowej instrukcji: Shape shape = new Shape(); skończy się komunikatem o błędzie: Shape is abstract; cannot be instantiated. Co jednak ważniejsze, zadeklarowanie metody jako abstrakcyjnej wymusza jej redeklarację w klasie potomnej. Oznacza to, że każda klasa wyprowadzona (czyli dziedzicząca) z klasy Shape musi zawierać metodę draw. Jeżeli w którejś z klas tej metody zabraknie, programu nie uda się skompilować. Tak więc przykładowa klasa Triangle w postaci: public class Triangle extends Shape { } spowoduje błąd kompilacji zaprezentowany na rysunku 5.14 (nie ma w niej bowiem definicji metody draw). 3 Można by co prawda zastosować prywatny konstruktor, ale to nie oznacza, że w ogóle nie da się utworzyć obiektu danej klasy. 4 Nie wyklucza to oczywiście istnienia klas abstrakcyjnych, w których żadna z metod nie jest abstrakcyjna. 214 Java. Praktyczny kurs Rysunek 5.14. Klasa pochodna musi zawierać metody zadeklarowane jako abstrakcyjne w klasie bazowej Można mieć zatem pewność, że jeśli klasa bazowa zawiera metodę abstrakcyjną, to każda klasa potomna również ją zawiera. Da się więc bezpiecznie stosować wywołania polimorficzne, takie jak omówione w poprzedniej lekcji. W tym miejscu może paść pytanie, czemu klasa, która zawiera metodę abstrakcyjną, również musi być zadeklarowana jako abstrakcyjna. Odpowiedź jest prosta: gdyby w zwykłej klasie została zadeklarowana metoda abstrakcyjna, a następnie utworzony zostałby obiekt tej klasy, nie byłoby przecież możliwe wywołanie tej metody, ponieważ nie miałaby ona kodu wykonywalnego. Musiałoby się to skończyć błędem w trakcie wykonania aplikacji. Należy jednak pamiętać, że zgodnie z definicją podaną powyżej klasa musi być zadeklarowana jako abstrakcyjna, jeżeli co najmniej jedna jej metoda jest abstrakcyjna. Wynika z tego, że nie ma żadnych przeciwwskazań, aby pozostałe metody nie były abstrakcyjne. Taka sytuacja została zilustrowana na listingu 5.15. Listing 5.15. public abstract class Shape { public abstract void draw(); public String toString() { return "metoda toString z klasy Shape"; } } public class Rectangle extends Shape { public void draw() { System.out.println("metoda draw z klasy Rectangle"); } public String toString() { return "metoda toString z klasy Rectangle"; } } public class Triangle extends Shape { public void draw() { System.out.println("metoda draw z klasy Triangle"); } } Zostały tu zdefiniowane trzy klasy: klasa abstrakcyjna Shape oraz klasy od niej pochodne Rectangle i Triangle. W klasie Shape są dwie metody: publiczna metoda abstrakcyjna draw oraz zwykła metoda toString. Klasa Rectangle zawiera definicje metod draw Rozdział 5. Programowanie obiektowe. Część II 215 oraz toString, natomiast Triangle jedynie definicję metody draw. Jak już wiadomo, taka sytuacja jest możliwa, gdyż metoda toString klasy Shape nie jest abstrakcyjna, klasy pochodne nie muszą zatem zawierać jej definicji. Aby się przekonać, jak działa taki zestaw klas i metod, napiszmy dodatkowo testową klasę Main. Została ona zaprezentowana na listingu 5.16. Listing 5.16. public class Main { public void drawShape(Shape shape) { shape.draw(); } public static void main(String args[]) { Main main = new Main(); Triangle triangle = new Triangle(); Rectangle rectangle = new Rectangle(); System.out.println("Wywołanie metod draw:"); main.drawShape(triangle); main.drawShape(rectangle); System.out.println(""); System.out.println("Wywołanie metod toString:"); System.out.println(triangle.toString()); System.out.println(rectangle.toString()); } } Klasa Main zawiera metodę drawShape przyjmującą jako argument obiekt klasy Shape. Metodzie tej będą przekazywane obiekty klas Rectangle i Triangle. Jest to konstrukcja wywołania polimorficznego analogiczna do zaprezentowanej na listingu 5.12 w lekcji 24. W metodzie main, od której rozpoczyna się wykonywanie kodu, tworzone są obiekty klas Main, Rectangle i Triangle, a następnie zostają wywołane metody drawShape oraz toString. Wszystkie te konstrukcje były już prezentowane wcześniej, nie wymagają więc dokładniejszego tłumaczenia. Efekt działania klasy Main jest widoczny na rysunku 5.15. W pierwszej sekcji dzięki wywołaniom polimorficznym zostały wykonane metody drawShape klas Triangle oraz Rectangle. Metody te musiały być w tych klasach zdefiniowane, gdyż klasy te dziedziczą po Shape, w której metoda drawShape została zadeklarowana jako abstrakcyjna. W sekcji drugiej zostały wywołane metody toString obiektów triangle oraz rectangle. Ponieważ jednak w klasie Triangle nie była zdefiniowana przeciążona metoda toString, została wywołana metoda toString odziedziczona po klasie Shape. Jest to zachowanie jak najbardziej prawidłowe i zgodne z oczekiwaniami. Warto zatem zapamiętać, że w klasie abstrakcyjnej mogą znaleźć się również metody, które nie są abstrakcyjne. 216 Java. Praktyczny kurs Rysunek 5.15. Wynik działania klas przedstawionych na listingach 5.15 i 5.16 Wywołania konstruktorów Wywołania konstruktorów nie są polimorficzne, ważne jednak, aby wiedzieć, jaka jest kolejność ich wywoływania, szczególnie w odniesieniu do hierarchii klas. Otóż w klasie potomnej zawsze musi zostać wywołany konstruktor klasy bazowej. Powinno to być oczywiste, jako że obiekt klasy potomnej zawiera w sobie obiekt klasy bazowej (ale może nie mieć dostępu do niektórych jego składowych!). Skoro tak, zawsze najpierw powinien zostać wykonany konstruktor klasy bazowej, a dopiero potem klasy potomnej. Programista ma wtedy pewność, że obiekt klasy bazowej został prawidłowo zainicjowany. Sytuacja taka jest zilustrowana na listingu 5.17. Listing 5.17. public class A { public A() { System.out.println("Konstruktor klasy A"); } } public class B extends A { public B() { System.out.println("Konstruktor klasy B"); } public static void main(String args[]) { B obiekt = new B(); } } Klasa A zawiera jeden publiczny konstruktor, którego zadaniem jest wyświetlenie na ekranie napisu oznajmiającego, z jakiej klasy pochodzi. Klasa B, dziedzicząca po klasie A, również zawiera jeden publiczny konstruktor, który wyświetla na ekranie informację, że pochodzi z klasy B. Konstruktory te są domyślne, jako że nie przyjmują żadnych argumentów. W klasie B została dodatkowo zdefiniowana metoda main, w której jest tworzony jeden obiekt klasy B. Zatem zgodnie z tym, co zostało napisane powyżej, w takiej sytuacji najpierw zostanie wywołany konstruktor klasy bazowej A, a dopiero po nim konstruktor klasy potomnej B, co jest widoczne na rysunku 5.16. Obowiązuje zatem zasada, że jeśli w konstruktorze klasy potomnej nie będzie jawnie wywołany żaden konstruktor klasy bazowej, automatycznie zostanie wywołany domyślny konstruktor klasy bazowej (konstruktorem domyślnym jest konstruktor bezargumentowy). Rozdział 5. Programowanie obiektowe. Część II 217 Rysunek 5.16. W klasie potomnej zawsze jest wywoływany konstruktor klasy bazowej Co by się jednak stało, gdyby w klasie A z listingu 5.17 nie umieścić żadnego konstruktora, czyli gdyby miała ona postać zaprezentowaną poniżej? public class A { } Obie klasy udałoby się skompilować, a na ekranie pojawiłby się jedynie napis Konstruktor klasy B. I choć wydawać by się mogło, że w takim razie nie został wywołany żaden konstruktor klasy A, w rzeczywistości tak nie jest. Konstruktor musi zostać wywołany, nawet jeśli nie zostanie umieszczony jawnie w ciele klasy. W takiej sytuacji Java dodaje własny pusty konstruktor domyślny, który rzecz jasna jest wywoływany. Z zupełnie inną sytuacją mamy do czynienia, kiedy w klasie bazowej nie zostanie umieszczony konstruktor domyślny (czyli taki, który nie przyjmuje żadnych argumentów), ale znajdzie się w niej dowolny inny konstruktor. Sytuacja tego typu została zaprezentowana na listingu 5.18. Listing 5.18. public class A { public A(int arg) { System.out.println("Konstruktor klasy A"); } } public class B extends A { public B() { System.out.println("Konstruktor klasy B"); } public static void main(String args[]) { B obiekt = new B(); } } Klasa B nie zmieniła się w stosunku do wersji zaprezentowanej na listingu 5.17, zmodyfikowana została natomiast treść klasy A. Nie ma już konstruktora domyślnego, bezargumentowego, pojawił się natomiast konstruktor przyjmujący jeden argument typu int. Jak już wiadomo, taka konstrukcja jest niepoprawna, gdyż konstruktor klasy B będzie próbował wywołać domyślny konstruktor klasy A, którego po prostu nie ma (pusty konstruktor domyślny zostałby dodany przez kompilator tylko wtedy, gdyby w klasie A nie było żadnego innego konstruktora). Już przy próbie kompilacji zostanie zatem zasygnalizowany błąd, który został zaprezentowany na rysunku 5.17. 218 Java. Praktyczny kurs Rysunek 5.17. Brak konstruktora domyślnego w klasie bazowej Kod z listingu 5.18 można poprawić na dwa sposoby: dopisując konstruktor domyślny do klasy A lub też jawnie wywołując istniejący w niej konstruktor. W tym drugim przypadku należałoby zastosować następującą konstrukcję: public B() { super(0); System.out.println("Konstruktor klasy B"); } Oczywiście wartość przekazana konstruktorowi klasy bazowej (0) jest przykładowa, może to być dowolna wartość typu int. Wykonajmy jeszcze przykład. Obiekty przedstawionych wcześniej klas A i B będą polami dodatkowej klasy C i zostaną utworzone w konstruktorze klasy C. Będzie to sytuacja, którą przedstawia listing 5.19. Listing 5.19. public class A { public A() { System.out.println("Konstruktor klasy A"); } } public class B extends A { public B() { System.out.println("Konstruktor klasy B"); } } public class C { A obiektA; B obiektB; public C() { System.out.println("Konstruktor klasy C"); obiektA = new A(); obiektB = new B(); } public static void main(String args[]) { C obiektC = new C(); } } Rozdział 5. Programowanie obiektowe. Część II 219 Klasy A i B mają tu postać taką jak na listingu 5.17, z tą różnicą, że z B została usunięta metoda main. W klasie C zostały zadeklarowane dwa pola o nazwach obiektA i obiektB. Pierwsze z nich jest typu A, drugie typu B. Oba są inicjowane w konstruktorze klasy C, wcześniej jednak konstruktor ten przedstawia się, czyli wyświetla na ekranie informację, z jakiej klasy pochodzi. W metodzie main z kolei zostaje utworzony nowy obiekt klasy C. Jaka zatem będzie kolejność wykonania konstruktorów w tym przykładzie? Skoro tworzony jest obiekt klasy C, najpierw zostanie wywołany konstruktor tej klasy, a tym samym w pierwszej kolejności pojawi się na ekranie napis Konstruktor klasy C. W tym konstruktorze jest tworzony obiekt klasy A, zatem wywołany zostanie konstruktor tej klasy i zostanie wyświetlony napis Konstruktor klasy A. W kolejnym kroku powstaje obiekt klasy B. Klasa B dziedziczy jednak po A, więc w tym kroku zostaną wywołane dwa konstruktory, najpierw z klasy bazowej A, a następnie z potomnej B. W związku z tym na ekranie pojawi się ciąg napisów widoczny na rysunku 5.18. Rysunek 5.18. Złożone wywołania konstruktorów Wywoływanie metod w konstruktorach Z lekcji 15. z rozdziału 3. wiadomo, że w konstruktorach można wywoływać metody danej klasy. Trzeba jednak uważać, gdyż wywoływanie metod w konstruktorach w połączeniu z polimorfizmem może wprowadzić w pułapkę. Przyjrzyjmy się klasom przedstawionym na listingu 5.20. Listing 5.20. public class A { public A() { //System.out.println("Konstruktor klasy A"); f(); } public void f() { //System.out.println("A:f"); } } public class B extends A { int dzielnik; public B(int dzielnik) { //System.out.println("Konstruktor klasy B"); this.dzielnik = dzielnik; } public void f() { //System.out.println("B:f()"); double wynik = 1 / dzielnik; System.out.println("1 / dzielnik to: " + wynik); 220 Java. Praktyczny kurs } public static void main(String args[]) { B b = new B(1); b.f(); } } Na pierwszy rzut oka ten kod wygląda całkiem poprawnie. Klasa A zawiera publiczną metodę f, która nie robi nic, oraz wywołujący ją konstruktor domyślny. W komentarze zostały ujęte instrukcje System.out.println, które później pozwolą dokładniej prześledzić sposób działania programu. Klasa B dziedziczy po klasie A. Ma ona jeden konstruktor przyjmujący argument typu int. Wartość tego argumentu jest przypisywana polu typu int o nazwie dzielnik. Została również zdefiniowana metoda f, która wykonuje dzielenie wartości 1 przez wartość zapisaną w polu o nazwie dzielnik i wyświetla wynik tego dzielenia na ekranie. W metodzie main jest tworzony nowy obiekt klasy B. Konstruktorowi przekazywana jest wartość 1, zatem pole dzielnik przyjmuje wartość 1. Następnie zostaje wywołana metoda f. Można by się spodziewać, że metoda ta wykona dzielenie 1 / 1, zatem na ekranie pojawi się napis 1 / dzielnik to: 1. Spróbujmy uruchomić taki program i sprawdźmy, co się stanie. Zostało to przedstawione na rysunku 5.19. Rysunek 5.19. Pułapka związana z polimorficznym wywoływaniem metod To zapewne bardzo niemiła niespodzianka, ewidentnie powstał wyjątek klasy Arithmetic Exception, co oznacza, że w metodzie f zostało wykonane dzielenie przez zero. Jak to się jednak mogło stać, skoro — to nie ulega wątpliwości — konstruktor klasy B otrzymał argument o wartości 1? Polu dzielnik musiała zostać przypisana wartość 1. Skąd więc wyjątek? Aby sprawdzić, co się tak naprawdę stało, warto usunąć teraz z obu klas komentarze występujące przy instrukcjach System.out.println oraz ponownie skompilować i uruchomić program. Pojawi się widok zaprezentowany na rysunku 5.20. Jak widać, konstruktor klasy B wcale nie zdążył się wykonać, wyjątek nastąpił wcześniej. Warto prześledzić kolejne etapy działania programu. W metodzie main klasy B jest tworzony obiekt tej klasy. Powoduje to oczywiście wywołanie konstruktora klasy B, ale uwaga: zgodnie z informacjami podanymi już w tej lekcji, ponieważ klasa B dziedziczy po A, przed wykonaniem jej konstruktora jest wywoływany konstruktor domyślny klasy A. Spójrzmy więc, co się dzieje w konstruktorze klasy A: jest tam wywoływana metoda f. Rozdział 5. Programowanie obiektowe. Część II 221 Rysunek 5.20. Konstruktor klasy B nie został do końca wykonany Skoro jednak sam obiekt jest typu B, wywołanie to będzie POLIMORFICZNE, a zatem zostanie wywołana metoda f z klasy B! Oznacza to, że fragment kodu: double wynik = 1 / dzielnik; System.out.println("1 / dzielnik to: " + wynik); zostanie wykonany, ZANIM jeszcze polu dzielnik zostanie przypisana jakakolwiek wartość. Pole dzielnik jest w tym momencie po prostu niezainicjowane. A jak wiadomo z lekcji 13., niezainicjowane jawnie pole typu int ma wartość… 0. Dlatego w tym przykładzie został wygenerowany wyjątek ArithmeticException — przecież przez zero dzielić nie wolno. Koniecznie należy więc pamiętać, że również w konstruktorach wywołania metod są polimorficzne, czyli skojarzenie treści metody odbywa się w trakcie działania programu i brany jest pod uwagę rzeczywisty typ obiektu. To niestety może prowadzić do trudnych do wykrycia błędów. Mechanizm powstawania takich błędów został przedstawiony w powyższym przykładzie. Ćwiczenia do samodzielnego wykonania Ćwiczenie 25.1. Napisz klasę zawierającą abstrakcyjną metodę toString. Wyprowadź z tej klasy klasę potomną. Ćwiczenie 25.2. Napisz klasę First zawierającą abstrakcyjną metodę f, wyprowadź z tej klasy klasę potomną Second zawierającą abstrakcyjną metodę g. Z klasy Second wyprowadź klasę Third. Pamiętaj o zdefiniowaniu wszystkich niezbędnych metod. Ćwiczenie 25.3. Zmodyfikuj kod z listingu 5.19 tak, aby obiekty obiektA i obiektB były inicjowane nie w konstruktorze klasy C, ale w momencie ich deklaracji. Zaobserwuj kolejność wykonywania konstruktorów. 222 Java. Praktyczny kurs Ćwiczenie 25.4. Zmodyfikuj kod z listingu 5.19 tak, by klasa C dziedziczyła po B. Jaka będzie kolejność wykonania konstruktorów? Interfejsy Lekcja 26. Tworzenie interfejsów W lekcji 25. zostały omówione klasy abstrakcyjne, czyli takie, w których znajdowały się metody niemające implementacji. Implementacja tych metod musiała się odbywać w klasach potomnych (o ile klasy potomne nie były również zadeklarowane jako abstrakcyjne). W tej lekcji zostaną przedstawione interfejsy, które można traktować (w wersjach Javy do 7 włącznie) jako klasy czysto abstrakcyjne, niemające żadnej implementacji. Czym są interfejsy? W Javie do wersji 8 interfejs to klasa czysto abstrakcyjna, czyli taka, w której wszystkie metody są traktowane jako abstrakcyjne. W wersji 8 wprowadzono jednak (ze względu na inne zmiany dotyczące języka) możliwość definiowania tzw. metod domyślnych (o czym pod koniec lekcji)5. Interfejs deklaruje się za pomocą słowa kluczowego interface. Może on być publiczny, o ile jest zdefiniowany w pliku o takiej samej nazwie jak nazwa interfejsu, lub pakietowy. W tym drugim przypadku jest dostępny jedynie dla klas wchodzących w skład danego pakietu. Schematyczna konstrukcja interfejsu wygląda więc następująco: [public] interface nazwa_interfejsu { typ_zwracany nazwa_metody1(argumenty); typ_zwracany nazwa_metody2(argumenty); /*…dalsze metody interfejsu…*/ typ_zwracany nazwa_metodyN(argumenty); } Przykładowy interfejs o nazwie Drawable zawierający deklarację tylko jednej metody o nazwie draw został przedstawiony na listingu 5.21. Listing 5.21. public interface Drawable { public void draw(); } Tak zdefiniowany interfejs może być implementowany przez dowolną klasę. Jeśli mówimy, że dana klasa implementuje interfejs, oznacza to, że zawiera ona definicje wszystkich metod zadeklarowanych w interfejsie. Jeśli choć jedna metoda zostanie 5 W interfejsie mogą się również znaleźć metody statyczne oraz tzw. typy zagnieżdżone (ang. nested types). Rozdział 5. Programowanie obiektowe. Część II 223 pominięta, kompilator zgłosi błąd. To, że klasa ma implementować dany interfejs, zaznacza się, wykorzystując słowo kluczowe implements, co schematycznie wygląda następująco: [specyfikator dostępu][abstract] class nazwa_klasy implements nazwa_interfejsu { /* …pola i metody klasy… */ } Zatem aby przykładowa klasa MojaKlasa implementowała interfejs Drawable z listingu 5.21, trzeba napisać: public class MojaKlasa implements Drawable { } Próba kompilacji takiej klasy skończy się… błędem kompilacji. Jest to widoczne na rysunku 5.21. Rysunek 5.21. Brak definicji metody zadeklarowanej w interfejsie Zapomnieliśmy bowiem, że zgodnie z tym, co zostało napisane wcześniej, klasa, która implementuje interfejs, musi zawierać implementację wszystkich jego metod6. W tym przypadku jest to metoda draw. Zatem poprawna wersja klasy MojaKlasa będzie wyglądała tak, jak to zostało przedstawione na listingu 5.22. Oczywiście treść metody draw może być dowolna. Listing 5.22. public class MojaKlasa implements Drawable { public void draw() { System.out.println("MojaKlasa"); } } Widać już więc, że interfejs określa po prostu, jakie metody muszą znaleźć się w klasie, która go implementuje (można powiedzieć, że interfejs to rodzaj kontraktu, który musi być spełniony przez klasę). Wróćmy teraz do przykładu z figurami z lekcji 24. i 25. (klasy Triangle, Rectangle, Circle). Wymuszaliśmy w tych klasach deklarację metody draw. Odbywało się to poprzez umieszczenie w klasie nadrzędnej Shape abstrakcyjnej metody draw. Można to jednak zrobić w inny sposób — wykorzystując interfejs Drawable. 6 Lub musi być klasą abstrakcyjną. 224 Java. Praktyczny kurs Wystarczy, jeśli każda z klas będzie ten interfejs implementowała, tak jak jest to przedstawione na listingu 5.23. W takiej sytuacji również występuje konieczność deklaracji metody draw w każdej z przedstawionych klas. Listing 5.23. public class Circle implements Drawable { public void draw() { System.out.println("metoda draw z klasy Circle"); } } public class Rectangle implements Drawable { public void draw() { System.out.println("metoda draw z klasy Rectangle"); } } public class Triangle implements Drawable { public void draw() { System.out.println("metoda draw z klasy Triangle"); } } Interfejsy a hierarchia klas Przykład z listingu 5.23 pokazał, jak implementować jeden interfejs w wielu klasach, jednak w stosunku do przykładów z poprzednich lekcji została zmieniona hierarchia klas. Wcześniej wszystkie trzy klasy, Triangle, Rectangle oraz Circle, dziedziczyły po klasie Shape, co pozwalało na stosowanie wywołań polimorficznych. Skorzystanie z interfejsów na szczęście nic tu nie zmienia, czyli wszystkie trzy klasy nadal mogą dziedziczyć po Shape i jednocześnie implementować interfejs Drawable. Taka sytuacja jest przedstawiona na listingu 5.24. Listing 5.24. public class Shape { public void draw() { } } public class Circle extends Shape implements Drawable { public void draw() { System.out.println("metoda draw z klasy Circle"); } } public class Rectangle extends Shape implements Drawable { public void draw() { System.out.println("metoda draw z klasy Rectangle"); } } public class Triangle extends Shape implements Drawable { Rozdział 5. Programowanie obiektowe. Część II 225 public void draw() { System.out.println("metoda draw z klasy Triangle"); } } Zwróćmy jednak uwagę na pewien mankament takiego rozwiązania. Otóż klasy Rectangle, Triangle i Circle, czyli pochodne od Shape, implementują interfejs Drawable, ale sama klasa Shape już nie. Oznacza to, że w klasach potomnych wymuszana jest implementacja metody draw, a w klasie bazowej nie. A więc nawet jeśli nie zapomnimy o napisaniu pustej metody draw w klasie Shape i kompilacja uda się bez problemu, nie będziemy mogli skorzystać z udogodnień, jakie dają wywołania polimorficzne. Z kolei jeżeli nie uwzględnimy metody draw w klasie Shape, kompilacja nie będzie możliwa. Łatwo się o tym przekonać, redukując klasę Shape do postaci: public class Shape { } oraz próbując skompilować klasę Main przedstawioną na listingu 5.25 (lub tę z listingu 5.12; obie mają prawie identyczną postać). Spowoduje to błąd widoczny na rysunku 5.22. To nie powinno dziwić, przecież nie istnieje teraz w klasie Shape metoda draw, a próbujemy ją wywołać w metodzie drawShape w klasie Main. Kompilator musi zatem zaprotestować. Listing 5.25. public class Main { public void drawShape(Shape shape) { shape.draw(); } public static void main(String args[]) { Main main = new Main(); Triangle triangle = new Triangle(); Rectangle rectangle = new Rectangle(); Circle circle = new Circle(); System.out.println("Wywołanie metod draw:"); main.drawShape(triangle); main.drawShape(rectangle); main.drawShape(circle); } } Rysunek 5.22. Próba wywołania nieistniejącej metody w klasie Shape 226 Java. Praktyczny kurs Jakie jest rozwiązanie tego problemu? Otóż metoda bazowa również powinna implementować interfejs Drawable, czyli klasa Shape powinna mieć postać: public class Shape implements Drawable { public void draw(){}; } Warto rozważyć jeszcze jeden problem. Jeśli jest wiele klas potomnych i każda z nich ma implementować dany interfejs, łatwo o pominięcie słowa implements w jednej z nich, a tym samym spowodowanie powstania błędu. Jak temu zapobiec? Czy nie dałoby się zmusić klas potomnych do zaimplementowania interfejsu z klasy bazowej? Okazuje się, że można to zrobić, pod warunkiem jednak, że klasa bazowa będzie klasą abstrakcyjną. W takiej sytuacji klasa ta nie będzie musiała implementować metod zawartych w interfejsie, ale będą je musiały implementować wszystkie nieabstrakcyjne klasy pochodne. Taka sytuacja została przedstawiona na listingu 5.26. Listing 5.26. public abstract class Shape implements Drawable { } public class Triangle extends Shape { public void draw() { System.out.println("metoda draw klasy Triangle"); } } public class Rectangle extends Shape { public void draw() { System.out.println("metoda draw klasy Rectangle"); } } public class Circle extends Shape { public void draw() { System.out.println("metoda draw klasy Circle"); } } W tej chwili próba usunięcia metody draw z którejkolwiek z klas: Triangle, Rectangle, Circle, mimo iż bezpośrednio nie implementują one interfejsu Drawable, skończyłaby się błędem kompilacji. Pola interfejsów Interfejsy, oprócz deklaracji metod, mogą również zawierać pola. Pola interfejsu są zawsze publiczne, statyczne oraz finalne, czyli trzeba im przypisać wartości już w momencie ich deklaracji. Można stosować typy zarówno proste, jak i złożone. Pola interfejsów są najczęściej wykorzystywane do tworzenia wyliczeń. Deklaracja pola interfejsu nie różni się od deklaracji pola klasy, schematycznie wygląda tak: [public] interface nazwa_interfejsu { typ_pola1 nazwa_pola1 = wartość_pola1; Rozdział 5. Programowanie obiektowe. Część II 227 typ_pola2 nazwa_pola2 = wartość_pola2; /*…*/ typ_polan nazwa_polan = wartość_polan; } Warto podkreślić jeszcze raz: pole interfejsu musi być zainicjowane już w momencie deklaracji, inaczej kompilator zgłosi błąd. Przykładowy interfejs zawierający trzy pola typu prostego jest przedstawiony na listingu 5.27. Zwróćmy uwagę na konwencję zapisu: ponieważ pola interfejsów są zawsze statyczne i finalne, przyjmuje się, że ich nazwy są pisane wielkimi literami, a poszczególne człony nazwy są oddzielane znakiem podkreślenia. Listing 5.27. public interface NowyInterfejs { int POLE_TYPU_INT = 100; double POLE_TYPU_DOUBLE = 1.0; char POLE_TYPU_CHAR = 'a'; } Nic nie stoi również na przeszkodzie, aby interfejs zawierał pola typu obiektowego, ważne jest tylko, żeby były one inicjowane również w momencie deklaracji, dokładnie tak, jak ma to miejsce w przypadku pól typów prostych. Przykład takiego interfejsu został zaprezentowany na listingu 5.28. Listing 5.28. public interface NowyInterfejs { Rectangle POLE_TYPU_RECTANGLE = new Rectangle(); Triangle POLE_TYPU_TRIANGLE = new Triangle(); Circle POLE_TYPU_CICRLE = new Circle(); } Metody domyślne W Java 8 wprowadzono możliwość definiowania w interfejsach tzw. metod domyślnych (ang. default methods). Taka metoda może mieć definicję (implementację) i ta definicja zostanie odziedziczona przez wszystkie klasy implementujące dany interfejs. Do jej utworzenia należy użyć słowa kluczowego default. Nie wyklucza to jednak istnienia w interfejsie typowych metod abstrakcyjnych (które były omawiane do tej pory). Schemat takiej konstrukcji jest następujący: [public] interface nazwa_interfejsu { typ_zwracany nazwa_metody1(argumenty); typ_zwracany nazwa_metody2(argumenty); /*…dalsze metody abstrakcyjne interfejsu…*/ typ_zwracany nazwa_metody_domyślnej1(argumenty){ /*treść metody domyślnej 1*/ } typ_zwracany nazwa_metody_domyślnej2(argumenty){ /*treść metody domyślnej 2*/ } /*…dalsze metody domyślne interfejsu…*/ } 228 Java. Praktyczny kurs Załóżmy, że w trakcie prac nad interfejsem Drawable (z listingu 5.21) wystąpiła konieczność jego unowocześnienia i dodania metody draw3D. Jej zadaniem ma być np. wyświetlanie danego kształtu z symulacją trójwymiarowości. Jeżeli do interfejsu zostanie dodana zwyczajna deklaracja metody (w sposób dostępny w Javie do wersji 7; będzie to więc metoda abstrakcyjna), przestaną działać wszystkie dotychczas napisane klasy (nie będą bowiem implementowały metody draw3D). W związku z tym trzeba będzie albo dokonać poprawek we wszystkich klasach implementujących interfejs, albo też utworzyć zupełnie nowy interfejs (np. o nazwie Drawable3D). Jednak dzięki dostępnym w Java 8 i w nowszych wersjach metodom domyślnym da się wprowadzić poprawki do istniejącego interfejsu Drawable w taki sposób, aby „stare” klasy (Shape, Rectangle itp.) mogły działać bez problemów. Kod tego interfejsu mógłby wyglądać tak jak na listingu 5.29. Listing 5.29. public interface Drawable { public void draw(); default public void draw3D(){ System.out.println("Domyślny kształt 3D"); } } Teraz bez najmniejszych problemów da się skompilować i uruchomić program z listingu 5.25 (w połączeniu z klasami z listingu 5.24, a także tymi z listingu 5.26) i będzie się on zachowywał zupełnie tak samo jak w przypadku starej wersji interfejsu (z listingu 5.21). Jednocześnie nic też nie będzie stało na przeszkodzie, aby korzystać z nowych możliwości. Zostało to zilustrowane w przykładzie widocznym na listingach 5.30 i 5.31. Na pierwszym z nich znajduje się zmodyfikowana klasa Triangle zawierająca definicję metody draw3D (klasy Shape, Rectangle i Circle należy pozostawić bez zmian, jak na listingu 5.26), a na drugim program bazujący na rozwiązaniu z listingu 5.25, ale korzystający z możliwości, jakie daje najnowsza wersja interfejsu Drawable. Listing 5.30. public class Triangle extends Shape { public void draw() { System.out.println("metoda draw klasy Triangle"); } public void draw3D() { System.out.println("metoda draw3D klasy Triangle"); } } Listing 5.31. public class Main { public void drawShape(Shape shape) { shape.draw(); } public void drawShape3D(Shape shape) { shape.draw3D(); } public static void main(String args[]) { Rozdział 5. Programowanie obiektowe. Część II 229 Main main = new Main(); Triangle triangle = new Triangle(); Rectangle rectangle = new Rectangle(); Circle circle = new Circle(); System.out.println("Wywołanie metod draw i draw3D:"); main.drawShape3D(triangle); main.drawShape3D(rectangle); main.drawShape(circle); } } Klasa Triangle (listing 5.30) dziedziczy po Shape (listing 5.26), która z kolei jest abstrakcyjna i implementuje interfejs Drawable. W klasie znajdują się dwie metody draw oraz draw3D — ich zadaniem jest wyświetlenie komunikatu z nazwą danej metody. Klasa Main z listingu 5.31 zawiera program pozwalający na przetestowanie różnych właściwości najnowszej wersji interfejsu Drawable (z listingu 5.29). Analogicznie do wersji z listingu 5.25 mamy w niej metodę drawShape wywołującą metodę draw obiektu przekazanego jako argument oraz dodatkowo metodę drawShape3D, która wywołuje metodę draw3D. W metodzie main, od której zacznie się wykonywanie programu, tworzony jest obiekt klasy Main oraz obiekty klas Triangle, Rectangle i Circle. Następnie dwukrotnie wywoływana jest metoda drawShape3D obiektu wskazywanego przez zmienną main. Przy pierwszym wywołaniu jako argument przekazywany jest obiekt klasy Triangle, a przy drugim — obiekt klasy Rectangle. W ostatnim wierszu wywoływana jest natomiast metoda drawShape i jest jej przekazywany obiekt klasy Circle. Oczywiście każde z tych wywołań będzie polimorficzne (lekcja 24.). Jaki zatem będzie efekt działania aplikacji i jakie napisy pojawią się po jej uruchomieniu? Widać to na rysunku 5.23. Wszystkie klasy oznaczające figury dziedziczą po klasie bazowej Shape i w związku z tym implementują najnowszą wersję interfejsu Drawable (listing 5.29). Interfejs ten zawiera domyślną metodę draw3D, ale klasa Triangle w wersji z listingu 5.30 ma własną wersję tej metody. Dlatego w pierwszym wywołaniu draw Shape3D zostanie użyta metoda draw3D pochodząca z klasy Triangle. Rysunek 5.23. Efekt użycia metody domyślnej interfejsu W drugim wywołaniu drawShape3D jest już inaczej. Użyta została stara wersja klasy Rectangle (z listingu 5.26), która nic „nie wie” o istnieniu metody draw3D. Dlatego też zostanie zastosowana domyślna metoda pochodząca z interfejsu Drawable, a na ekranie pojawi się napis „Jestem kształtem 3D”. Ostatnie wywołanie dotyczy obiektu klasy 230 Java. Praktyczny kurs Circle oraz metody draw. Ono nie powinno budzić wątpliwości. Nowa wersja interfejsu nie zmienia zachowania starych metod, dlatego efekt będzie taki jak w przykładzie z listingu 5.25. Ćwiczenia do samodzielnego wykonania Ćwiczenie 26.1. Napisz kod interfejsu o nazwie Rysowanie, w którym zostaną zadeklarowane metody rysuj2D i rysuj3D. Następnie napisz klasę Figura, która będzie implementowała ten interfejs. Ćwiczenie 26.2. Zmodyfikuj kod z listingu 5.24 tak, aby klasa Shape implementowała interfejs Drawable, a klasa Rectangle go nie implementowała. Ćwiczenie 26.3. Napisz przykładowy interfejs zawierający dwa pola, jedno typu int, drugie typu double, oraz implementującą go klasę wyświetlającą wartości tych pól. Ćwiczenie 26.4. Napisz przykładowy interfejs zawierający dwa pola, jedno typu int, drugie typu double, oraz klasę wyświetlającą wartości tych pól. Klasa ta nie może implementować tego interfejsu. Lekcja 27. Wiele interfejsów Wiadomo już, w jaki sposób tworzyć i implementować interfejsy w klasach. Zagadnienia te pojawiły się w lekcji 26. Kolejnym tematem, którym się zajmiemy, jest technika pozwalająca na implementację przez jedną klasę wielu interfejsów. Zostanie tu też przedstawione, jak unikać niebezpieczeństw związanych z konfliktem nazw metod w interfejsach, a także to, że interfejsy, tak jak i inne klasy, podlegają regułom dziedziczenia. Okaże się również, że jeden interfejs może przejąć metody nawet z kilku innych. Implementowanie wielu interfejsów W Javie klasa potomna może dziedziczyć tylko po jednej klasie bazowej, nie ma więc znanego m.in. z C++ wielodziedziczenia. Istnieje natomiast możliwość implementowania wielu interfejsów. Interfejsy, które mają być implementowane przez klasę, należy wymienić po słowie implements i oddzielić je znakami przecinka. Schemat takiej konstrukcji wygląda następująco: [public][abstract] class nazwa_klasy implements interfejs1, interfejs2, ... , interfejsN { /* Rozdział 5. Programowanie obiektowe. Część II 231 …pola i metody klasy… */ } Przykładowo: jeśli zostaną napisane dwa interfejsy, pierwszy o nazwie PierwszyInterfejs definiujący metodę f i drugi o nazwie DrugiInterfejs definiujący metodę g, oraz klasa MojaKlasa implementująca oba, wszystkie trzy klasy będą miały postać przedstawioną na listingu 5.32. Listing 5.32. public interface PierwszyInterfejs { public void f(); } public interface DrugiInterfejs { public void g(); } public class MojaKlasa implements PierwszyInterfejs, DrugiInterfejs { public void f(){ } public void g(){ } } Klasa MojaKlasa musi oczywiście zawierać definicje wszystkich metod zadeklarowanych w interfejsach PierwszyInterfejs i DrugiInterfejs. W tym przypadku są to metody f i g. Pominięcie którejkolwiek z nich spowoduje rzecz jasna błąd kompilacji. Po co jednak klasa miałaby implementować wiele interfejsów? Czy nie lepiej byłoby po prostu napisać jeden? Okazuje się, że nie, traci się bowiem wtedy uniwersalność kodu. Dla przykładu załóżmy, że napisane zostały klasy Telewizor i Radio oraz dwa interfejsy WydajeDzwiek oraz WyswietlaObraz. Taka sytuacja jest zobrazowana na listingu 5.33. Listing 5.33. public interface WydajeDzwiek { public void graj(); } public interface WyswietlaObraz { public void wyswietl(); } public class Radio implements WydajeDzwiek { public void graj() { //instrukcje metody graj } } public class Telewizor implements WydajeDzwiek, WyswietlaObraz { public void graj() { //instrukcje metody graj } 232 Java. Praktyczny kurs public void wyswietl() { //instrukcje metody wyswietl } } Jak wiadomo, telewizor zarówno wyświetla obrazy, jak i wydaje dźwięk, implementuje zatem oba interfejsy: WyswietlaObraz i WydajeDzwiek, a więc zawiera również metody graj i wyswietl. Radio nie wyświetla obrazów, w związku z czym implementuje jedynie interfejs WydajeDzwiek, a klasa Radio zawiera wyłącznie metodę graj. Jak widać, dzięki temu, że jedna klasa może implementować wiele interfejsów, mogą one być bardziej uniwersalne. Gdyby taka możliwość nie istniała, dla telewizora musiałby powstać interfejs np. o nazwie WyswietlaObrazIWydajeDzwiek, w którym zostałyby zadeklarowane metody graj i wyswietl, co ograniczałoby jego zastosowania. Może to być jednak ćwiczenie do samodzielnego wykonania. Konflikty nazw Kiedy jedna klasa implementuje wiele interfejsów, mogą powstać budzące wątpliwości sytuacje konfliktu nazw, które niestety czasami prowadzą do powstawania błędów (na szczęście zwykle wykrywanych w trakcie kompilacji). Załóżmy, że mamy dwa interfejsy przedstawione na listingu 5.34. Listing 5.34. public interface PierwszyInterfejs { public void f(); } public interface DrugiInterfejs { public void f(); } Są one w pełni poprawne, kod ten nie budzi żadnych wątpliwości. Co się natomiast stanie, kiedy przykładowa klasa MojaKlasa będzie miała implementować oba te interfejsy? Z kodu pierwszego interfejsu wynika, że klasa MojaKlasa powinna mieć zdefiniowaną metodę o nazwie f, z kodu drugiego wynika dokładnie to samo. Czy zatem klasa ta ma zawierać dwie metody f? Oczywiście nie, nie byłoby to przecież możliwe. Skoro jednak deklaracje metod f w obu interfejsach są takie same, oznacza to, że klasa implementująca oba interfejsy musi zawierać JEDNĄ metodę f o deklaracji public void f(). Zatem przykładowa klasa MojaKlasa będzie miała postać przedstawioną na listingu 5.35. Listing 5.35. public class MojaKlasa implements PierwszyInterfejs, DrugiInterfejs { public void f() { System.out.println("f"); } } Rozdział 5. Programowanie obiektowe. Część II 233 Nieco inna, ale również jednoznaczna sytuacja nastąpi, kiedy w dwóch interfejsach będą metody o takiej samej nazwie, ale innych argumentach. Wtedy również będzie można zaimplementować je w jednej klasie. Takie dwa przykładowe interfejsy zostały przedstawione na listingu 5.36. Listing 5.36. public interface PierwszyInterfejs { public void f(int argument); } public interface DrugiInterfejs { public int f(double argument); } W pierwszym interfejsie została zadeklarowana metoda f przyjmująca jeden argument typu int, natomiast w drugim metoda o takiej samej nazwie przyjmująca argument typu double i zwracająca wartość typu int. Skoro tak, przykładowa klasa MojaKlasa implementująca oba interfejsy będzie musiała mieć implementacje obu metod. Będzie zatem wyposażona w przeciążone metody f (lekcja 14.). Może ona wyglądać np. tak, jak to zostało przedstawione na listingu 5.37. Listing 5.37. public class MojaKlasa implements PierwszyInterfejs, DrugiInterfejs { public void f() { System.out.println("f"); } public void f(int argument) { System.out.println("f:argument = " + argument); } public int f(double argument) { System.out.println("f:argument = " + argument); return (int) argument; } } Klasa ta implementuje dwa interfejsy o nazwach PierwszyInterfejs i DrugiInterfejs. Zawiera również trzy przeciążone metody f. Pierwsza z nich nie zwraca żadnych wartości oraz nie przyjmuje żadnych argumentów. Druga metoda została wymuszona przez interfejs PierwszyInterfejs, również nie zwraca żadnej wartości, przyjmuje natomiast argument typu int (tak jak zostało to zdefiniowane w interfejsie). Trzecia metoda została wymuszona przez interfejs DrugiInterfejs. Przyjmuje ona jeden argument typu double oraz zwraca wartość typu int. Tak więc w tym przypadku kod jest również w pełni poprawny i jednoznaczny (klasa MojaKlasa da się bez problemów skompilować). Co się jednak stanie, kiedy interfejsy przyjmą postać widoczną na listingu 5.38? Czy jakakolwiek klasa może je jednocześnie implementować? Listing 5.38. public interface PierwszyInterfejs { 234 Java. Praktyczny kurs public void f(); } public interface DrugiInterfejs { public int f(); } Odpowiedź na zadane powyżej pytanie brzmi: nie. Co by się bowiem stało, gdyby utworzona wcześniej przykładowa klasa MojaKlasa miała implementować oba interfejsy przedstawione na listingu 5.38? Musiałaby mieć postać z listingu 5.39. Listing 5.39. public class MojaKlasa implements PierwszyInterfejs, DrugiInterfejs { public void f() { //System.out.println("f"); } public int f() { //System.out.println("f"); } } Taka klasa jest w sposób oczywisty nieprawidłowa. W jednej klasie nie mogą istnieć dwie metody o takiej samej nazwie różniące się jedynie typem zwracanego wyniku. Próba kompilacji takiego kodu spowoduje powstanie serii błędów widocznych na rysunku 5.24. Rysunek 5.24. Nieprawidłowa implementacja interfejsów w klasie MojaKlasa Podobna sytuacja jest przedstawiona na listingu 5.40, choć na pierwszy rzut oka kod może wydawać się poprawny. Jeśli bowiem wziąć pod uwagę kod klasy MojaDrugaKlasa oraz interfejsu PierwszyInterfejs, wygląda na to, że nie zawiera on błędu. Nie widać przeciwwskazań do implementacji metody f przyjmującej argument typu int i niezwracającej wyniku. Rozdział 5. Programowanie obiektowe. Część II 235 Listing 5.40. public interface MojInterfejs { public void f(int argument); } public class MojaDrugaKlasa extends MojaKlasa implements MojInterfejs { //public void f(int argument) { //} } public class MojaKlasa { public int f(int argument) { return argument; } } Kompilacja przedstawionego kodu jednak się nie uda i to niezależnie od tego, czy kod metody będzie ujęty w komentarz (tak jak jest to widoczne na listingu), czy nie. Wszystko staje się jasne po przyjrzeniu się klasie bazowej dla MojaDrugaKlasa, czyli klasie Moja Klasa. Znajduje się w niej metoda o nazwie f przyjmująca jeden argument typu int i zwracająca wartość typu int. Ponieważ klasa MojaDrugaKlasa dziedziczy po MojaKlasa, zawiera również tę metodę, a tym samym nie może implementować interfejsu Pierwszy Interfejs. A zatem gdy aktywny jest komentarz w metodzie f klasy MojaDrugaKlasa, kod jest nieprawidłowy, ponieważ ze względu na implementowanie interfejsu MojInterfejs w klasie musi istnieć metoda o deklaracji void f(int). Z kolei gdy taka metoda zostanie dodana (a komentarz usunięty), kompilacja się nie uda, bo metoda o deklaracji void f(int) nie może być użyta ze względu na odziedziczoną po klasie bazowej (MojaKlasa) metodę o deklaracji int f(int). Dziedziczenie interfejsów Interfejsy można budować, stosując mechanizm dziedziczenia. Odbywa się to podobnie jak w przypadku klas. Należy wykorzystać słowo extends. Interfejs potomny będzie zawierał wszystkie swoje metody oraz wszystkie metody z interfejsu bazowego. Schemat zastosowania słowa extends w przypadku dziedziczenia interfejsów wygląda następująco: [public] interface interfejs_potomny extends interfejs_bazowy { //deklaracje pól i metod } Zatem z przykładowego interfejsu I1 zawierającego metodę f można wyprowadzić interfejs I2 zawierający metodę g. Taka sytuacja została przedstawiona na listingu 5.41. Listing 5.41. public interface I1 { public void f(); } public interface I2 extends I1 { 236 Java. Praktyczny kurs public void g(); } Jak widać, interfejs I2 dziedziczy po interfejsie I1, zatem zawiera własną metodę g oraz odziedziczoną f. Przykładowa klasa, która będzie implementowała interfejs I2, będzie w związku z tym musiała zawierać zarówno metodę f, jak i g, bo inaczej nie uda się skompilować jej kodu. Taka przykładowa klasa jest widoczna na listingu 5.42. Listing 5.42. public class MojaKlasa implements I2 { public void f() { //treść metody f } public void g() { //treść metody g } } Interfejs, inaczej niż zwykła klasa, może dziedziczyć nie tylko po jednym, ale też po wielu interfejsach. Interfejsy bazowe należy w takiej sytuacji umieścić po słowie extends i oddzielić ich nazwy przecinkami. Schematycznie taka konstrukcja wygląda następująco: [public] interface nazwa_interfejsu extends interfejs1, interfejs2, ... , interfejsN { //metody i pola interfejsu } Jeśli zatem mamy dwa przykładowe interfejsy bazowe I1 i I2 zawierające metody f i g, a chcemy utworzyć interfejs potomny I3, który odziedziczy wszystkie ich właściwości, powinniśmy zastosować konstrukcję widoczną na listingu 5.43. Listing 5.43. public interface I1 { public void f(); } public interface I2 { public void g(); } public interface I3 extends I1, I2 { public void h(); } Oczywiście przykładowa klasa implementująca interfejs I3 będzie musiała mieć zdefiniowane wszystkie metody z interfejsów I1, I2 i I3, czyli metody f, g i h. Pominięcie którejkolwiek z nich spowoduje błąd kompilacji. Przykładowa klasa MojaKlasa implementująca interfejs I3 została przedstawiona na listingu 5.44. Rozdział 5. Programowanie obiektowe. Część II 237 Listing 5.44. public class MojaKlasa implements I3 { public void f() {//wymuszona przez I1 //treść metody f } public void g() { //wymuszona przez I2 //treść metody g } public void h() {//wymuszona przez I3 //treść metody g } } Przy korzystaniu z mechanizmu dziedziczenia w interfejsach należy pamiętać o możliwych konfliktach nazw, które były omawiane w poprzedniej części lekcji. Takie niebezpieczeństwo istnieje zarówno przy dziedziczeniu pojedynczym, jak i wielokrotnym. Na listingu 5.45 jest przedstawiony przykład nieprawidłowego dziedziczenia po jednym interfejsie bazowym. Otóż w interfejsie I1 została zadeklarowana metoda f niezwracająca wyniku, natomiast w interfejsie I2 metoda o nazwie f, ale zwracająca wynik typu int. W takiej sytuacji interfejs I2 nie może dziedziczyć po I1, gdyż występuje konflikt nazw (próba kompilacji pliku z kodem interfejsu I2 zakończy się niepowodzeniem). Listing 5.45. public interface I1 { public void f(); } public interface I2 extends I1 {//Nieprawidłowo! public int f(); } Błąd pojawi się również w sytuacji przedstawionej na listingu 5.46 — tym razem dotyczy trzech interfejsów. Interfejs I1 zawiera bezargumentową metodę f o typie zwracanym int, I2 zawiera również bezargumentową metodę f, ale o typie zwracanym double. Trzeci interfejs o nazwie I3 dziedziczy zarówno po I1, jak i po I2 (dodatkowo zawiera własną metodę o nazwie h). Oczywiście czegoś takiego nie wolno zrobić, gdyż to prowadzi do konfliktu nazw. Próba kompilacji interfejsu I3 spowoduje pojawienie się komunikatu o błędzie widocznego na rysunku 5.25. Listing 5.46. public interface I1 { public int f(); } public interface I2 { public double f(); } public interface I3 extends I1, I2 { public void h(); } 238 Java. Praktyczny kurs Rysunek 5.25. Konflikt nazw przy dziedziczeniu interfejsów Ćwiczenia do samodzielnego wykonania Ćwiczenie 27.1. Dopisz do interfejsu PierwszyInterfejs z listingu 5.32 bezargumentową metodę g o typie zwracanym void, a do interfejsu DrugiInterfejs bezargumentową metodę f o takim samym typie. Jak powinna w takiej sytuacji wyglądać treść klasy MojaKlasa? Ćwiczenie 27.2. Zmień kod interfejsów z listingu 5.34 tak, aby metoda f interfejsu PierwszyInterfejs przyjmowała argument typu int, a metoda f interfejsu DrugiInterfejs argument typu double. Jakich modyfikacji będzie wymagać klasa MojaKlasa z listingu 5.35, by mogła współpracować z tak zmienionymi interfejsami? Ćwiczenie 27.3. Zmień kod z listingu 5.33 w taki sposób, aby klasa Telewizor implementowała tylko jeden interfejs o nazwie WyswietlaObrazIWydajeDzwiek. Ćwiczenie 27.4. Wykorzystując mechanizm dziedziczenia, połącz interfejsy WyswietlaObraz i Wydaje Dzwiek z listingu 5.33, tak żeby powstał jeden interfejs o nazwie WyswietlaObrazIWydaje Dzwiek. Klasy wewnętrzne Lekcja 28. Klasa w klasie Przy omawianiu pakietów i rodzajów klas w rozdziale 3. pojawiło się pojęcie klas wewnętrznych. Temat nie został wtedy rozwinięty, teraz nadszedł czas na bliższe zapoznanie się z owym nieprzedstawionym jeszcze rodzajem klas. Lekcja 28. jest poświęcona właśnie wprowadzeniu w tę nową tematykę. Zostanie w niej pokazane, w jaki Rozdział 5. Programowanie obiektowe. Część II 239 sposób tworzy się klasy wewnętrzne oraz jakie mają one właściwości, a także jak dostać się do ich składowych i jakie relacje zachodzą między klasą wewnętrzną a zewnętrzną. Tworzenie klas wewnętrznych Klasa wewnętrzna (ang. inner class), jak sama nazwa wskazuje, to taka, która została zdefiniowana we wnętrzu innej klasy7. Konstrukcja ta początkowo może wydawać się nieco dziwna, w praktyce pozwala jednak na wygodne tworzenie różnych konstrukcji programistycznych. Schematyczna deklaracja klasy wewnętrznej wygląda następująco: [specyfikator dostępu] class klasa_zewnętrzna { [specyfikator dostępu] class klasa_wewnętrzna { /* …pola i metody klasy wewnętrznej… */ } /* …pola i metody klasy zewnętrznej… */ } Aby utworzyć klasę o nazwie Outside, która będzie zawierała w sobie klasę Inside, można zastosować kod zaprezentowany na listingu 5.47. Należy go zapisać w pliku o nazwie Outside.java i skompilować ten plik tak jak każdy inny plik z kodem źródłowym w Javie. Po kompilacji powstaną dwa pliki z rozszerzeniem class: Outside.class zawierający kod klasy Outside oraz Outside$Inside.class zawierający kod klasy Inside. Listing 5.47. public class Outside { class Inside { } } W klasie zewnętrznej można bez problemów oraz bez żadnych dodatkowych zabiegów programistycznych korzystać z obiektów klasy wewnętrznej. Można je tworzyć, a także bezpośrednio odwoływać się do zdefiniowanych w nich pól i metod. Przykład odwołań do obiektu klasy wewnętrznej jest widoczny na listingu 5.48. Listing 5.48. public class Outside { class Inside { public int liczba = 100; public void f() { System.out.println("Inside:f liczba = " + liczba); } } 7 Dokładniej rzecz ujmując: taka, która została zdefiniowana we wnętrzu innej klasy i nie jest klasą statyczną. Ściśle zgodnie z terminologią należałoby mówić o klasach zagnieżdżonych (ang. nested classes), które dzielą się na statyczne klasy zagnieżdżone (ang. static nested classes) i klasy wewnętrzne (ang. inner classes). 240 Java. Praktyczny kurs public void g() { Inside ins = new Inside(); ins.f(); ins.liczba = 200; ins.f(); } public static void main(String args[]) { new Outside().g(); } } Klasa wewnętrzna o nazwie Inside zawiera jedno publiczne pole typu int o nazwie liczba, któremu już podczas deklaracji jest przypisywana wartość 100, oraz jedną publiczną metodę o nazwie f, której zadaniem jest wyświetlenie wartości zapisanej w polu liczba. Klasa zewnętrzna Outside zawiera publiczną metodę o nazwie g, w której jest tworzony nowy obiekt klasy Inside. Następnie wywoływana jest metoda f tego obiektu, a dalej polu liczba jest przypisywana wartość 200 i ponownie jest wywoływana metoda f. Oprócz metody g w klasie Outside znajduje się metoda main, od której zaczyna się wykonywanie kodu programu. W tej metodzie jest tworzony nowy obiekt klasy Outside i wywoływana jest jego metoda g. Odbywa się to w jednej linii programu. Niestosowana do tej pory przez nas konstrukcja: new Outside().g(); oznacza: utwórz obiekt klasy Outside, a następnie wywołaj jego metodę o nazwie g (odwołanie do obiektu nie jest tym samym nigdzie zapamiętywane). Efekt działania tego fragmentu kodu jest taki sam jak rezultat wykonania dwóch instrukcji: Outside out = new Outside(); out.g(); Ostatecznie w wyniku działania aplikacji na ekranie pojawi się widok przedstawiony na rysunku 5.26. Widać więc wyraźnie, że w klasie zewnętrznej można bez problemu odwoływać się do składowych klasy wewnętrznej. Rysunek 5.26. Odwołania do pól i metod klasy wewnętrznej Kilka klas wewnętrznych W jednej klasie zewnętrznej może istnieć dowolna liczba klas wewnętrznych, nie ma w tym względzie ograniczeń. Dostęp do nich odbywa się w taki sam sposób, jaki zaprezentowano wyżej. Przykładowa klasa Outside, w której zostały zdefiniowane dwie klasy wewnętrzne, została przedstawiona na listingu 5.49. Rozdział 5. Programowanie obiektowe. Część II 241 Listing 5.49. public class Outside { SecondInside secondIns = new SecondInside(); class FirstInside { public int liczba = 100; public void f() { System.out.println("FirstInside:f liczba = " + liczba); } } class SecondInside { public double liczba = 1.0; public void f() { System.out.println("SecondInside:f liczba = " + liczba); } } public void g() { FirstInside firstIns = new FirstInside(); firstIns.f(); firstIns.liczba = 200; firstIns.f(); secondIns.f(); secondIns.liczba = 2.5; secondIns.f(); } public static void main(String args[]) { new Outside().g(); } } Są tu trzy klasy, zewnętrzna Outside oraz dwie wewnętrzne: FirstInside i SecondInside. W klasie FirstInside znajduje się jedno pole typu int o nazwie liczba oraz jedna bezargumentowa metoda o nazwie f. Zadaniem tej metody jest wyświetlanie wartości pola liczba. Klasa SecondInside jest zbudowana w sposób analogiczny do FirstInside, zawiera jedno pole o nazwie liczba i typie double oraz jedną bezargumentową metodę o nazwie f, której zadaniem jest wyświetlanie wartości pola liczba. Klasa zewnętrzna, Outside, zawiera pole typu SecondInside o nazwie secondIns, do którego już w trakcie deklaracji jest przypisywany nowy obiekt klasy SecondInside. Do dyspozycji są również dwie metody: g oraz main. W g tworzymy zmienną typu FirstInside o nazwie firstIns i przypisujemy jej odniesienie do nowego obiektu klasy FirstInside, następnie wywołujemy metodę f tego obiektu, przypisujemy polu liczba wartość 200 oraz ponownie wywołujemy metodę f. W kolejnym kroku wywołujemy metodę f obiektu wskazywanego przez secondIns, przypisujemy polu liczba tego obiektu wartość 2.5 oraz ponownie wywołujemy jego metodę f. Nie musimy tworzyć obiektu secondIns, gdyż czynność ta została wykonana w trakcie deklaracji pola secondIns. Ostatecznie po uruchomieniu takiej aplikacji na ekranie pojawi się widok zaprezentowany na rysunku 5.27. 242 Java. Praktyczny kurs Rysunek 5.27. Wykorzystanie dwóch klas wewnętrznych Składowe klas wewnętrznych W dotychczasowych przykładach składowe (pola i metody) klas wewnętrznych były oznaczone specyfikatorem dostępu public. Nie jest to jednak obligatoryjne, do klas wewnętrznych mają zastosowanie takie same zasady ustalania dostępu do składowych jak w przypadku zwykłych klas. Dostęp może być więc publiczny, pakietowy, chroniony bądź prywatny. Załóżmy, że istnieje klasa wewnętrzna Inside podobna do przedstawionej na listingu 5.48, w której jednak pole liczba jest polem prywatnym, tak jak pokazano na listingu 5.50. Listing 5.50. public class Outside { class Inside { private int liczba = 100; public void f() { System.out.println("Inside:f liczba = " + liczba); } } public void g() { Inside ins = new Inside(); ins.f(); ins.liczba = 200; ins.f(); } public static void main(String args[]) { new Outside().g(); } } Zaskoczeniem może być, że przykład ten kompiluje się bez problemów, choć pole liczba w klasie Inside jest polem prywatnym! Otóż okazuje się, że klasa zewnętrzna ma pełny dostęp do składowych klasy wewnętrznej. Nie ma więc przeciwwskazań, aby w klasie Outside odwołać się do pola liczba z klasy Inside. Ta zasada dotyczy jednak tylko i wyłącznie klasy zewnętrznej. Pozostałe klasy będą w pełni respektowały modyfikatory dostępu. Przykład ilustrujący to zagadnienie został zaprezentowany na listingu 5.51. Listing 5.51. public class Outside { public Inside ins = new Inside(); class Inside { private int liczba = 100; Rozdział 5. Programowanie obiektowe. Część II 243 public void f() { System.out.println("Inside:f liczba = " + liczba); } } } public class Main { public static void main(String args[]) { Outside out = new Outside(); out.ins.f(); out.ins.liczba = 200; out.ins.f(); } } Klasa Outside ma konstrukcję bardzo podobną do wykorzystywanej we wcześniejszych przykładach. Zawiera klasę wewnętrzną Inside oraz publiczne pole (klasy Inside) o nazwie ins. Pole to jest inicjowane już w momencie jego deklaracji. Klasa wewnętrzna Inside zawiera jedno prywatne pole typu int o nazwie liczba, któremu podczas deklaracji jest przypisywana wartość 100, oraz jedną publiczną, bezargumentową metodę f wyświetlającą wartość pola liczba na ekranie. Ważne jest podkreślenie, że pole liczba jest prywatne. Obie klasy należy zapisać w pliku o nazwie Outside.java. W drugim pliku, o nazwie Main.java, należy umieścić kod klasy Main. Zawiera ona metodę main, od której rozpocznie się wykonywanie kodu. W metodzie tej tworzymy nowy obiekt klasy Outside, następnie odwołujemy się do jego pola ins, czyli do obiektu klasy Inside. Wywołujemy metodę f, po czym przypisujemy polu liczba wartość 100 i ponownie wywołujemy metodę f. Próba kompilacji spowoduje wystąpienie błędu widocznego na rysunku 5.28. Skoro bowiem pole liczba z klasy Inside jest polem prywatnym, można się do niego odwoływać jedynie z wnętrza klasy Inside lub klasy zewnętrznej dla Inside (czyli Outside). Problem zniknie, jeśli pole liczba będzie polem publicznym lub też dopiszemy metody publiczne operujące na nim. Rysunek 5.28. Próba odwołania się do prywatnego pola klasy wewnętrznej Umiejscowienie klasy wewnętrznej W przedstawionych dotychczas przykładach klasa wewnętrzna była umieszczana zaraz na początku kodu klasy zewnętrznej, ewentualnie za jej polami. Jest to najbardziej sensowne i najczęściej spotykane umiejscowienie klas wewnętrznych, ale to nie jedyna możliwość. Klasa wewnętrzna może się również znaleźć w dowolnym miejscu między polami, między metodami, a także… wewnątrz metod (będzie to wtedy lokalna klasa 244 Java. Praktyczny kurs wewnętrzna, ang. local inner class)8. Ta ostatnia sytuacja jest rzadziej spotykana, niemniej taka możliwość istnieje i warto wiedzieć, jak w praktyce traktować taki fragment kodu. Jak zatem umieścić klasę wewnętrzną w treści metody klasy zewnętrznej? Taka sytuacja została zaprezentowana na listingu 5.52. Listing 5.52. public class Outside { public void f() { class Inside { /*…pola klasy Inside…*/ public void g() { /*…instrukcje metody g…*/ } /*…dalsze metody klasy Inside…*/ } /*…dalsze instrukcje metody f…*/ } /*…dalsze metody klasy Outside…*/ } Są tu dwie klasy: Outside i Inside. Outside jest klasą zewnętrzną i zawiera bezargumentową metodę o nazwie f. W tej metodzie została zdefiniowana klasa wewnętrzna o nazwie Inside zawierająca bezargumentową metodę g. Trzeba zdawać sobie jednak sprawę, że w takiej sytuacji widoczność klasy Inside została ograniczona wyłącznie do metody f klasy Outside. Poza tą metodą jest ona nieznana i nie można się do niej odwoływać. Zostało to zilustrowane w przykładzie z listingu 5.53. Listing 5.53. public class Outside { //źle, w tym miejscu nie jest znana klasa Inside //Inside ins1 = new Inside(); public void f() { //źle, w tym miejscu nie jest znana klasa Inside //Inside ins2 = new Inside(); class Inside { public void g() { } } //dobrze, klasa Inside została zdefiniowana Inside ins3 = new Inside(); } //źle, w tym miejscu nie jest znana klasa Inside //Inside ins4 = new Inside(); } W komentarze zostały ujęte instrukcje, których wykonanie skończy się błędem kompilacji. Nie można utworzyć obiektów ins1, ins2 i ins4, gdyż w miejscach ich deklaracji nie jest widoczna klasa Inside. Jedyne miejsce, w którym można się do niej bezpo8 W rzeczywistości klasa wewnętrzna może się znaleźć w dowolnym bloku wyróżnionym za pomocą znaków nawiasu klamrowego, a więc również w pętli czy w instrukcji warunkowej. Rozdział 5. Programowanie obiektowe. Część II 245 średnio odwoływać, znajduje się w metodzie f, za jej (klasy) definicją. Próby odwołań poza tym zasięgiem nieuchronnie doprowadzą do powstania błędów kompilacji. Dostęp do klasy zewnętrznej Wiadomo już, że z klasy zewnętrznej jest pełny dostęp do składowych klasy wewnętrznej. Okazuje się jednak, że ta relacja obowiązuje również w drugą stronę, to znaczy z klasy wewnętrznej mamy dostęp do składowych klasy zewnętrznej. Co więcej, jest to dostęp pełny, czyli można się odwoływać także do składowych prywatnych. Przykład ilustrujący to zagadnienie został przedstawiony na listingu 5.54. Listing 5.54. public class Outside { private int liczba = 100; class Inside { private void g(int argument) liczba = argument; } } private void f(int argument) { Inside ins = new Inside(); System.out.println("liczba = ins.g(argument); System.out.println("liczba = } public static void main(String new Outside().f(200); } } { " + liczba); " + liczba); args[]) { Klasa zewnętrzna Outside zawiera prywatne pole o nazwie liczba, któremu w trakcie deklaracji jest przypisywana wartość 100. W tej klasie jest również zdefiniowana klasa wewnętrzna Inside, która zawiera prywatną metodę g przyjmującą jeden argument typu int. Wartość tego argumentu jest przypisywana, uwaga… prywatnemu polu liczba w klasie Outside. Jak widać, nie trzeba stosować żadnych specjalnych konstrukcji programistycznych, dostęp do pola klasy zewnętrznej jest bezpośredni. Klasa Outside zawiera też prywatną metodę f przyjmującą argument typu int. W tej klasie tworzony jest obiekt klasy Inside, który jest przypisywany zmiennej ins. Następnie na ekranie jest wyświetlana bieżąca wartość pola liczba oraz wywoływana jest metoda g obiektu ins. Metodzie tej zostaje przekazany argument otrzymany przez metodę f. Na zakończenie ponownie wyświetlana jest wartość pola liczba. W metodzie main, od której rozpocznie się wykonywanie kodu, jest tworzony nowy obiekt klasy Outside i wywoływana jest jego metoda f. Metodzie tej jest przekazywana wartość 200. Tym samym wartość ta zostaje ostatecznie przypisana (w metodzie g klasy Inside) polu liczba. Na ekranie pojawi się więc widok zaprezentowany na rysunku 5.29. 246 Java. Praktyczny kurs Rysunek 5.29. Ilustracja odwołania z klasy wewnętrznej do prywatnego pola klasy zewnętrznej Ćwiczenia do samodzielnego wykonania Ćwiczenie 28.1. Napisz przykładową klasę A, w której znajdzie się wewnętrzna klasa B, a w tej z kolei zostanie umieszczona wewnętrzna klasa C. Skompiluj otrzymany kod, zobacz, jak będą wyglądały nazwy plików wynikowych. Ćwiczenie 28.2. Zmodyfikuj kod klas z listingu 5.51 tak, aby pole liczba klasy Inside pozostało polem prywatnym, ale by była możliwa jego modyfikacja przez metody innych klas. Nie dokonuj zmian w klasie Outside. Ćwiczenie 28.3. Zmodyfikuj kod klas z listingu 5.51 tak, aby pole liczba klasy Inside pozostało polem prywatnym, ale by była możliwa jego modyfikacja. Nie dokonuj zmian w klasie Inside. Ćwiczenie 28.4. Umieść definicję klasy wewnętrznej w metodzie klasy zewnętrznej, w bloku instrukcji if. Lekcja 29. Rodzaje klas wewnętrznych i dziedziczenie Omówiliśmy już podstawy posługiwania się klasami wewnętrznymi, czyli takimi, które są zdefiniowane wewnątrz innych klas. Lekcja 29. pozwoli na pogłębienie wiedzy na ten temat. Przedstawione zostaną sposoby tworzenia i odwoływania się do obiektów klas wewnętrznych w ciele klas od nich niezależnych, istniejące rodzaje klas wewnętrznych, a także to, co się dzieje w przypadku dziedziczenia. Okaże się również, jak w tego typu konstrukcjach stosować modyfikatory dostępu. Obiekty klas wewnętrznych Wiadomo już, że obiekty klas wewnętrznych można tworzyć w klasach zewnętrznych — nic w tym dziwnego, skoro klasa zewnętrzna ma pełny dostęp do klasy wewnętrznej9. 9 Z wyjątkiem opisanej w poprzedniej lekcji sytuacji, kiedy dostęp do klasy wewnętrznej jest ograniczony do zasięgu, w którym została ona zdefiniowana. Rozdział 5. Programowanie obiektowe. Część II 247 Co jednak zrobić, aby móc operować na obiekcie klasy wewnętrznej poza klasą zewnętrzną? Przykładowo: co zrobić w sytuacji, kiedy klasą zewnętrzną jest Outside, wewnętrzną Inside, a chcemy mieć dostęp do obiektów klasy Inside z zupełnie niezależnej klasy Main? Możliwe są dwa rozwiązania tego problemu. Otóż w klasie zewnętrznej można umieścić metodę tworzącą i zwracającą obiekty klasy wewnętrznej lub też, za pomocą specjalnej składni, bezpośrednio powołać do życia obiekt klasy wewnętrznej. Zacznijmy od pierwszego sposobu. Na listingu 5.55 są widoczne przykładowe klasy Inside i Outside. Listing 5.55. public class Outside { class Inside { public void g() { System.out.println("Inside:g()"); } } public Inside getInside() { return new Inside(); } } Klasa zewnętrzna Outside zawiera metodę getInside, która zwraca odniesienie do obiektu klasy Inside. Obiekt ten jest tworzony wewnątrz tej metody, a referencja jest zwracana za pomocą standardowej instrukcji return. Klasa wewnętrzna Inside zawiera jedną metodę o nazwie g, wyświetlającą napis informujący o klasie, z której pochodzi. Aby utworzyć teraz klasę Main, która skorzysta z obiektu klasy Inside, można użyć sposobu przedstawionego na listingu 5.56. Listing 5.56. public class Main { public static void main(String args[]) { Outside out = new Outside(); out.getInside().g(); } } Tworzymy nowy obiekt klasy Outside i przypisujemy go zmiennej o nazwie out. Następnie wywołujemy metodę getInside tego obiektu, która zwraca odniesienie do obiektu klasy Inside, oraz wywołujemy metodę g tego obiektu. Po skompilowaniu i uruchomieniu klasy Main pojawi się widok zaprezentowany na rysunku 5.30. W klasie Main udało się więc wykonać bezpośrednią operację na obiekcie typu Inside. Rysunek 5.30. Wywołanie metody klasy wewnętrznej z klasy Main 248 Java. Praktyczny kurs Przedstawiony sposób dostępu do obiektu klasy wewnętrznej jest jak najbardziej poprawny, ale ma jedną wadę. Otóż obiekt klasy Inside można wykorzystać tylko raz, tylko do jednokrotnego wywołania metody g, gdyż nie jest przechowywana referencja do niego. Tego typu wywołania spotyka się w praktyce, jeśli jednak ma być zachowany dostęp do obiektu zwróconego przez metodę getInside, trzeba postąpić inaczej. Należy oczywiście zadeklarować zmienną, w której zostanie zapisana referencja. Pytanie brzmi: jakiego typu będzie ta zmienna? Odpowiedź, która nasuwa się w pierwszej chwili, brzmi: zmienna powinna być typu Inside. Jeśli jednak spróbujemy w klasie Main dokonać przykładowej deklaracji w postaci: Inside ins; nie osiągniemy zamierzonego celu. Kompilator nie zna osobnej klasy o nazwie Inside i zgłosi błąd. Klasa wewnętrzna nie może istnieć samodzielnie, bez zewnętrznej. Jest to również odzwierciedlone w deklaracji zmiennych klas wewnętrznych. Deklaracja taka powinna wyglądać następująco: klasa_zewnętrzna.klasa_wewnętrzna nazwa_zmiennej; Czyli prawidłowa deklaracja zmiennej klasy Inside w klasie Main powinna mieć postać: Outside.Inside ins; Przykładowa klasa Main wykorzystująca taką deklarację jest widoczna na listingu 5.57. Listing 5.57. public class Main { public static void main(String args[]) { Outside out = new Outside(); Outside.Inside ins; ins = out.getInside(); ins.g(); ins.g(); } } Tworzymy nowy obiekt klasy Outside i przypisujemy go zmiennej out. Następnie deklarujemy zmienną klasy Inside, wykorzystując przedstawioną przed chwilą konstrukcję. Tę zmienną inicjujemy, wywołując metodę getInside z klasy Outside, która zwraca obiekt klasy Inside. W ten sposób zachowujemy referencję do obiektu, którą można dalej dowolnie wykorzystywać. W tym przypadku dwukrotnie wywoływana jest metoda g. Pozostał do omówienia drugi sposób tworzenia obiektów klas wewnętrznych, to znaczy bezpośrednie powoływanie ich do życia w klasach niezależnych. Pytanie brzmi: co zrobić w sytuacji, kiedy klasa zewnętrzna nie udostępnia żadnej metody zwracającej nowy obiekt klasy wewnętrznej, tak jak zostało to przedstawione na listingu 5.58? Listing 5.58. public class Outside { class Inside { public void g() { Rozdział 5. Programowanie obiektowe. Część II 249 System.out.println("Inside:g()"); } } } Przede wszystkim trzeba sobie uświadomić, że obiekt klasy wewnętrznej nie może istnieć, o ile nie istnieje również obiekt klasy zewnętrznej10. Zawsze więc najpierw tworzymy obiekt klasy zewnętrznej i — dopiero korzystając z niego — możemy powołać do życia obiekt klasy wewnętrznej. Powinniśmy zastosować w takiej sytuacji ciąg instrukcji o następującej postaci: klasa_zewnętrzna obiekt_zew = new klasa_zewnętrzna(); klasa_zewnętrzna.klasa_wewnętrzna obiekt_wew = obiekt_zew.new klasa_wewnętrzna(); Przy założeniu, że klasy Outside i Inside są takie jak na listingu 5.58, instrukcje te wyglądałyby następująco: Outside out = new Outside(); Outside.Inside ins = out.new Inside(); Po ich wykonaniu zmienna ins będzie wskazywała na nowy obiekt klasy wewnętrznej Inside, na którym będzie można wykonywać wszelkie dozwolone operacje, np. wywoływać metody, tak jak zostało to przedstawione na listingu 5.59. Listing 5.59. public class Main { public static void main(String args[]) { Outside out = new Outside(); Outside.Inside ins = out.new Inside(); ins.g(); } } Rodzaje klas wewnętrznych Dotychczas zostało zaprezentowanych już kilkanaście przykładów z klasami wewnętrznymi. Łatwo zauważyć, że w ich definicjach nie pojawiały się żadne modyfikatory dostępu. Wynikałoby z tego, że wszystkie stosowane do tej pory klasy wewnętrzne były klasami pakietowymi. Tak jest w istocie. Brakowało jednak okazji, by się o tym przekonać, gdyż, jak wiadomo (lekcja 17.), dla klas znajdujących się w tym samym katalogu, które nie zostały formalnie umieszczone w żadnym pakiecie, jest tworzony pakiet domyślny, dzięki czemu mogą się one wzajemne do siebie odwoływać. Na tej zasadzie działały przykłady z listingów 5.55 – 5.59. Jeśli jednak definicje klasy wewnętrznej i zewnętrznej umieścimy w oddzielnym pakiecie, sytuacja ulegnie diametralnej zmianie. Zostało to zobrazowane w przykładzie widocznym na listingu 5.60. 10 Istnieje jednak specyficzny typ klas wewnętrznych, dla którego ta zasada nie obowiązuje. Zostanie on omówiony w lekcji 30. 250 Java. Praktyczny kurs Listing 5.60. package pakiet_klas; public class Outside { class Inside { public void g() { System.out.println("Inside:g()"); } } } import pakiet_klas.*; public class Main { public static void main(String args[]) { Outside out = new Outside(); Outside.Inside ins = out.new Inside(); ins.g(); } } Klasa Outside jest klasą publiczną, która została tym razem umieszczona w pakiecie o nazwie pakiet_klas. W pliku z klasą Main umieszczona została zatem dyrektywa import, dzięki której będzie można odwoływać się do klasy Outside. Nie ma więc żadnego problemu z utworzeniem obiektu klasy Outside, tak jak dzieje się to w pierwszej instrukcji metody main w klasie Main. Zwróćmy jednak uwagę, że klasa Inside, która jest wewnętrzna dla Outside, to klasa pakietowa, w związku z tym nie ma możliwości odwoływania się do niej z klasy Main. Dlatego też kolejna instrukcja metody main (tworząca obiekt klasy Inside) spowoduje powstanie błędów kompilacji. Gdyby zatem istniała konieczność odwołania się w klasie Main do klasy Inside, czyli uzyskania dostępu do klasy wewnętrznej z klasy niezależnej, klasa wewnętrzna musiałaby mieć dostęp publiczny. W takiej sytuacji klasa Inside powinna mieć postać: public class Inside { public void g() { System.out.println("Inside:g()"); } } W tym miejscu powraca problem klasyfikacji klas występujących w Javie. Z lekcji 17. wiemy, że klasy dzielą się na: publiczne, pakietowe, wewnętrzne. Jest to oczywiście podział poprawny. Ponieważ jednak klasy wewnętrzne są definiowane wewnątrz innych klas i można o nich myśleć jako o składowych tych klas, możliwe jest używanie w stosunku do nich modyfikatorów dostępu. W związku z tym klasy wewnętrzne mogą być: Rozdział 5. Programowanie obiektowe. Część II 251 pakietowe, publiczne, prywatne, chronione i należy je traktować tak jak składowe wymienionych typów. Zatem aby całkowicie zabronić dostępu do klasy wewnętrznej (a jest to stosunkowo często spotykana sytuacja), należy zrobić z niej klasę prywatną, tak jak zostało to przedstawione w przykładzie na listingu 5.61. Listing 5.61. public class Outside { private class Inside { public void g() { System.out.println("Inside:g()"); } } public Inside getInside() { return new Inside(); } } public class Main { public static void main(String args[]) { Outside out = new Outside(); //poniższe trzy zapisy są nieprawidłowe //w klasie Main nie ma dostępu do klasy Inside //Outside.Inside ins = out.new Inside(); //ins.g(); //out.getInside().g(); //prawidłowo, ale bez praktycznego znaczenia out.getInside(); } } Klasa wewnętrzna Inside jest teraz klasą prywatną. Oznacza to, że nie będzie miała do niej dostępu żadna inna klasa oprócz Outside. W klasie Outside została natomiast zdefiniowana metoda getInside zwracająca nowy obiekt klasy Inside. Co w takiej sytuacji dzieje się w klasie Main? Otóż można oczywiście utworzyć obiekt klasy Outside, gdyż jest ona klasą publiczną, wszelkie próby odwołania do klasy Inside skończą się natomiast niepowodzeniem. Nie uda się ani utworzyć obiektu Inside, ani pobrać referencji do niego przy użyciu metody getInside z jednoczesnym wywołaniem metody g. Istnieje natomiast możliwość wywołania samej metody getInside. Ponieważ jednak na tak zwróconej referencji nie można wykonać żadnej operacji, nie ma to praktycznego znaczenia11. 11 Chyba że konstruktor klasy Inside wykonywałby jakieś specjalne operacje, co jednak wydaje się sytuacją czysto hipotetyczną. 252 Java. Praktyczny kurs Warto zwrócić uwagę na jeszcze jedną ciekawą kwestię. Otóż metoda g z klasy Inside jest publiczna, teoretycznie dostępna dla wszystkich innych klas. Ponieważ jednak sama klasa Inside jest klasą wewnętrzną prywatną, metody g i tak nie można wywołać poza klasami Inside i Outside. Dziedziczenie po klasie zewnętrznej Kolejna kwestia, której znajomość jest konieczna przy wykorzystywaniu klas wewnętrznych, to sposób dziedziczenia. Otóż z klasy zewnętrznej można oczywiście bez żadnych problemów wyprowadzać klasy potomne. W klasie potomnej będzie też dostęp do składowych klasy wewnętrznej, o ile oczywiście nie jest to klasa prywatna. Przykład dziedziczenia po klasie zewnętrznej pokazano na listingu 5.62. Listing 5.62. public class Outside { public class Inside { public void g() { System.out.println("Inside:g()"); } } public Inside getInside() { return new Inside(); } } public class ExtendedOutside extends Outside { public void h() { System.out.println("ExtendedOutside:h()"); } } public class Main { public static void main(String args[]) { ExtendedOutside extOut = new ExtendedOutside(); extOut.h(); ExtendedOutside.Inside extIns1 = extOut.getInside(); extIns1.g(); ExtendedOutside.Inside extIns2 = extOut.new Inside(); extIns2.g(); } } Klasy Outside i Inside mają postać analogiczną do przedstawionej na listingu 5.61, z tą różnicą, że Inside jest teraz klasą publiczną. Z klasy Outside została wyprowadzona klasa potomna o nazwie ExtendedOutside, w której została zdefiniowana metoda o nazwie h. Oprócz tego jest jeszcze niezależna klasa Main operująca na obiektach wcześniej wymienionych klas. W tej klasie w metodzie main tworzymy obiekt extOut klasy ExtendedOutside, a następnie wywołujemy jego metodę h. Ten fragment z pewnością nie budzi żadnych wątpliwości, jest to typowa konstrukcja programistyczna. Rozdział 5. Programowanie obiektowe. Część II 253 W dalszej kolejności tworzymy pierwszy obiekt klasy Inside, wywołując metodę getInside obiektu extOut. Obiekt extOut jest klasy ExtendedOutside, zatem wykorzystywana jest tu metoda getInside odziedziczona po klasie Outside. Zwróćmy też uwagę na sposób deklaracji zmiennej extIns1: wykorzystujemy składnię ExtendedOutside.Inside, a nie — jak we wcześniejszych przykładach — Outside.Inside. Jest to możliwe dzięki temu, że klasa ExtendedOutside dziedziczy po Outside. Następnie wywołujemy metodę g obiektu extIns1. W kolejnym kroku tworzymy nowy obiekt klasy Inside, również wykorzystując obiekt extOut klasy ExtendedOutside. Stosujemy przedstawioną na początku bieżącej lekcji metodę wywołującą konstruktor klasy Inside poprzez obiekt klasy zewnętrznej. Zwróćmy uwagę, że jest to sytuacja, w której ExtendedOutside można traktować jako klasę zewnętrzną dla Inside, wszystko dzięki dziedziczeniu po klasie bazowej Outside. Dlatego też można zastosować powyższy sposób wywołania konstruktora, a także bez problemu wywołać metodę g obiektu extIns2. Ostatecznie po uruchomieniu kodu pojawi się widok zaprezentowany na rysunku 5.31. Rysunek 5.31. Ilustracja dostępu do klasy wewnętrznej z klasy pochodnej Przed chwilą padło stwierdzenie, że klasę potomną od klasy zewnętrznej można traktować jako klasę zewnętrzną dla klasy wewnętrznej. Brzmi to wyjątkowo zawile, ale chodzi o to, że klasa ExtendedOutside z listingu 5.62 jest traktowana jako zewnętrzna dla klasy Inside. Rzeczywiście tak jest, trzeba jednak wziąć w tym momencie pod uwagę reguły dziedziczenia. Jeśli bowiem klasa Inside będzie klasą pakietową, publiczną lub chronioną, bez problemu będzie można odwoływać się do niej w klasie ExtendedOutside. Gdyby jednak Inside była klasą prywatną, takie odwołania okazałyby się niestety niemożliwe. Zilustrowano to w przykładzie z listingu 5.63. Listing 5.63. public class Outside { private class Inside { } } public class ExtendedOutside extends Outside { //poniższe odwołanie jest nieprawidłowe, jeśli //klasa Inside jest klasą prywatną //Inside ins = new Inside(); } Klasa Inside jest w tym przykładzie klasą prywatną, czyli można ją traktować jako prywatną składową klasy Outside. W związku z tym klasa ExtendedOutside nie ma do niej dostępu. Jeśli usuniemy komentarz z linii: //Inside ins = new Inside(); przekonamy się o tym naocznie, gdyż kompilator zgłosi serię błędów. 254 Java. Praktyczny kurs Ćwiczenia do samodzielnego wykonania Ćwiczenie 29.1. Napisz klasę o nazwie A zawierającą wewnętrzną klasę o nazwie B. W klasie A umieść metodę zwracającą nowy obiekt klasy B. Ćwiczenie 29.2. Utwórz pakiet nowy_pakiet, w którym znajdą się dwie klasy: A, zewnętrzna o dostępie publicznym, i B, wewnętrzna o dostępie pakietowym. Klasa A powinna zawierać jedno prywatne pole klasy B o nazwie obiektB inicjowane w trakcie deklaracji, a B powinna zawierać pole liczba typu int. W klasie A dopisz metody pozwalające na zapis i odczyt pola liczba obiektu obiektB. Ćwiczenie 29.3. Napisz publiczną klasę o nazwie A zawierającą wewnętrzną klasę B o dostępie chronionym. Z klasy A wyprowadź klasę pochodną ExtendedA zawierającą pole klasy B. Ćwiczenie 29.4. Napisz klasę Outside zawierającą klasę wewnętrzną Inside o dostępie publicznym. Z Outside wyprowadź klasę ExtendedOutside zawierającą publiczną klasę wewnętrzną o nazwie ExtendedInside. Lekcja 30. Klasy anonimowe i zagnieżdżone Lekcja 30. kończy rozdział 5. poświęcony bardziej zaawansowanym technikom związanym z programowaniem obiektowym w Javie. Kontynuujemy w niej temat klas wewnętrznych. Tym razem okaże się, że oferują one kilka niespotykanych i wręcz zaskakujących możliwości, bo czy nie jest zaskakująca możliwość utworzenia klasy, która nie ma swojej nazwy, lub zadeklarowania klasy jako statycznej? Takie właśnie tematy zostaną omówione na kilku najbliższych stronach. Na początek rzutowanie obiektów na typy interfejsowe. Interfejs jako typ zwracany W lekcji 23. była mowa o technikach rzutowania obiektów na klasę bazową lub potomną. Okazuje się jednak, że rzutować można również na typy interfejsowe. Dokładniej: jeśli dana klasa implementuje pewien interfejs, to jej obiekt można rzutować na typ tego interfejsu. Oczywiście w takiej sytuacji programista będzie miał jedynie dostęp do składowych zdefiniowanych przez interfejs. Tak więc po schematycznej definicji klasy: class nazwa_klasy implements interfejs { /*…pola i składowe klasy…*/ } Rozdział 5. Programowanie obiektowe. Część II 255 można wykonać instrukcję: interfejs obiekt = new nazwa_klasy(); lub (stosując jawną konwersję typów): interfejs obiekt = (interfejs) new nazwa_klasy(); Powstanie wtedy obiekt typu nazwa_klasy, do którego będzie można odwoływać się przez zmienną obiekt. Trzeba jednak pamiętać, że przez tę zmienną możliwe będzie odwoływanie się jedynie do metod, które zostały wymuszone w deklaracji klasy poprzez implementację interfejsu (lekcja 26.). Zostało to zilustrowane w przykładzie widocznym na listingu 5.64. Listing 5.64. public interface PierwszyInterfejs { public int getLiczba(); public void setLiczba(int liczba); } public class MojaKlasa implements PierwszyInterfejs { private int liczba; public void setLiczba(int liczba) { this.liczba = liczba; } public int getLiczba() { return liczba; } public int setAndGet(int liczba) { int temp = this.liczba; this.liczba = liczba; return temp; } public static void main(String args[]) { MojaKlasa obj = new MojaKlasa(); obj.setLiczba(1); int temp1 = obj.getLiczba(); int temp2 = obj.setAndGet(2); PierwszyInterfejs pi = new MojaKlasa(); pi.setLiczba(3); int temp3 = pi.getLiczba(); //int temp4 = pi.getAndSet(4); } } W interfejsie PierwszyInterfejs zostały zadeklarowane dwie metody: getLiczba, bezargumentowa zwracająca wartość typu int, oraz setLiczba przyjmująca wartość typu int. Klasa MojaKlasa implementuje ten interfejs, zatem zawiera również definicje metod setLiczba i getLiczba. Metoda setLiczba służy do ustawiania wartości prywatnego pola o nazwie liczba, a getLiczba do pobierania wartości tego pola. Dodatkowo w klasie MojaKlasa została zdefiniowana metoda getAndSet, której zadaniem jest ustawianie wartości pola liczba oraz zwrócenie poprzedniej wartości tego pola. 256 Java. Praktyczny kurs Wykonywanie kodu rozpoczyna się od metody main. Jej kod zaczyna się od zadeklarowania zmiennej referencyjnej obj typu MojaKlasa i przypisania jej nowo utworzonego obiektu klasy MojaKlasa. Następnie wywołujemy metodę setLiczba tego obiektu, ustawiając wewnętrzne pole liczba na wartość 1. W kolejnym kroku za pomocą metody getLiczba pobieramy wartość tego pola i przypisujemy ją zmiennej temp1, po czym wywołujemy metodę setAndGet, przekazując w argumencie wartość 2. Wartość zwróconą przez tę metodę przypisujemy zmiennej temp2. Kolejny blok instrukcji metody main rozpoczynamy od zadeklarowania zmiennej pi typu PierwszyInterfejs i przypisania jej nowo utworzonego obiektu klasy MojaKlasa. Jak wiemy, jest to możliwe dzięki temu, że klasa MojaKlasa implementuje interfejs PierwszyInterfejs. Pamiętajmy tylko, że typem powstałego obiektu jest MojaKlasa, a jest on jedynie rzutowany na typ PierwszyInterfejs (podobna sytuacja miała miejsce podczas rzutowania obiektu danej klasy na klasę bazową lub pochodną, rzeczywisty typ obiektu nigdy nie ulegał zmianie). Ponieważ w interfejsie PierwszyInterfejs są zdefiniowane metody setLiczba oraz getLiczba, można je wywołać na rzecz obiektu pi. Instrukcje: pi.setLiczba(3); int temp3 = pi.getLiczba(); są zatem poprawne i działają zgodnie z założeniami. Nie można natomiast wykonać ujętej w komentarz instrukcji: int temp4 = pi.getAndSet(4); gdyż nie ma metody getAndSet w interfejsie PierwszyInterfejs. Usunięcie komentarza skończyłoby się błędem kompilacji. Rzutowanie na interfejs nie musi oczywiście oznaczać konieczności tworzenia nowego obiektu. Tak jak ma to miejsce w przypadku zwykłych typów klasowych, można również rzutować obiekt już istniejący, rzecz jasna o ile implementuje on dany interfejs. Jeśli więc ogólna postać klasy jest następująca: class nazwa_klasy implements interfejs { /*…definicja klasy…*/ } to prawidłowe będzie wykonanie poniższych instrukcji: nazwa_klasy nazwa_zmiennej_klasowej = new nazwa_klasy(); interfejs nazwa_zmiennej_interfejsowej = [(interfejs)] nazwa_zmiennej_klasowej; Nie musimy jawnie umieszczać nazwy interfejsu, na który rzutujemy, przed nazwą zmiennej (co zostało zaznaczone przez ujęcie jej w nawias klamrowy). Kompilator „wie”, na który typ ma nastąpić rzutowanie. Ze względu na przejrzystość kodu można jednak stosować rzutowanie jawne. W praktyce, przy założeniu, że dysponujemy zestawem klas takim jak przedstawiony na listingu 5.64, możemy wykonać instrukcje dla rzutowania jawnego: MojaKlasa obj = new MojaKlasa(); PierwszyInterfejs pi = (PierwszyInterfejs) obj; oraz dla rzutowania domyślnego: Rozdział 5. Programowanie obiektowe. Część II 257 MojaKlasa obj = new MojaKlasa(); PierwszyInterfejs pi = obj; Obie formy są prawidłowe i mają takie samo znaczenie. Klasy anonimowe Czy klasa może nie mieć nazwy? Wydaje się, że to niemożliwe, okazuje się jednak, że Java dopuszcza tego typu sytuacje. Takie klasy nazywamy anonimowymi i są one często stosowane w praktyce programistycznej. Można je wykorzystywać dzięki możliwości rzutowania na typy klasowe bądź interfejsowe12. Budowę klasy anonimowej najlepiej wyjaśnić na konkretnym przykładzie — został on zaprezentowany na listingu 5.65. Listing 5.65. public interface PierwszyInterfejs { public int getLiczba(); public void setLiczba(int liczba); } public class MojaKlasa { public static void main(String args[]) { PierwszyInterfejs pi; pi = new PierwszyInterfejs() { private int liczba; public void setLiczba(int liczba) { this.liczba = liczba; } public int getLiczba() { return liczba; } }; pi.setLiczba(100); int temp = pi.getLiczba(); System.out.println(temp); } } Przeanalizujmy, co tu się stało. Co właściwie zostało przypisane zmiennej pi? Już pierwsza część tego przypisania powinna wzbudzić zdziwienie, bo wygląda ona tak, jakby był wywoływany konstruktor klasy PierwszyInterfejs. Tymczasem Pierwszy Interfejs jest… interfejsem, z pewnością nie można więc tworzyć obiektów takiej klasy. Dalej jest jeszcze ciekawiej, ponieważ znajdują się tam deklaracje pól i metod. Cała ta konstrukcja oznacza stworzenie obiektu klasy anonimowej implementującej interfejs PierwszyInterfejs. Powstała więc klasa, która nie ma nazwy. Odniesienie do obiektu tej klasy zostało przypisane zmiennej pi. Dostęp do obiektu jest możliwy jedynie poprzez interfejs PierwszyInterfejs. Konsekwencją tego stanu rzeczy jest brak bezpośredniego dostępu do jakichkolwiek składowych tej klasy innych niż zdefiniowane w interfejsie. Nie ma więc 12 Co prawda interfejs także jest klasą, ale można wprowadzić takie rozróżnienie. 258 Java. Praktyczny kurs innej możliwości manipulacji wartością pola liczba jak tylko przez wykorzystanie metod setLiczba oraz getLiczba. Nic nie da nawet zmiana modyfikatora dostępu pola liczba z private na public. Należy zwrócić uwagę na średnik znajdujący się za definicją klasy anonimowej (formalnie kończący instrukcję przypisania). Jest on niezbędny do prawidłowej kompilacji kodu. Skoro utworzyliśmy obiekt takiej klasy i przypisaliśmy go zmiennej pi typu Pierwszy Interfejs, możemy za pośrednictwem metod setLiczba i getLiczba wykonywać operacje na polu liczba. Wywołujemy więc metodę setLiczba, przekazując jej w argumencie wartość 100, a następnie metodę getLiczba, przypisując zwrócony przez nią wynik zmiennej temp typu int. Ostatecznie wyświetlamy wartość zmiennej temp na ekranie, wykorzystując instrukcję System.out.println. Jeśli chcemy, aby klasa anonimowa została wyprowadzona z konkretnej klasy bazowej, musimy po operatorze new zamiast nazwy interfejsu użyć nazwy klasy. Taka sytuacja została przedstawiona na listingu 5.66. Listing 5.66. public class MojaKlasa { public void f() { System.out.println("MojaKlasa:f()"); } public static void main(String args[]) { MojaKlasa obj1 = new MojaKlasa() { public void f() { System.out.println("Anonim:f()"); } }; MojaKlasa obj2 = new MojaKlasa(); obj1.f(); obj2.f(); } } W tej chwili klasa anonimowa jest klasą pochodną od MojaKlasa, czyli dziedziczy po niej. Sama konstrukcja tworząca klasę anonimową jest analogiczna do przedstawionej w poprzednim przykładzie. Tym razem jednak możliwy jest dostęp do składowych tej klasy! Prześledźmy to nieco dokładniej. W klasie MojaKlasa znajduje się metoda f, której zadaniem jest wyświetlenie nazwy klasy, w jakiej została zdefiniowana. Podobnie jest w klasie anonimowej. W metodzie main tworzymy nowy obiekt klasy anonimowej i przypisujemy go (wykonywane jest tu niejawne rzutowanie w górę) zmiennej obj1. Następnie tworzymy nowy obiekt klasy MojaKlasa i przypisujemy go zmiennej obj2. W kolejnym kroku wywołujemy metody f obiektów wskazywanych przez obj1 i obj2. Rozdział 5. Programowanie obiektowe. Część II 259 Co zostanie wyświetlone na ekranie? Zmienna obj1 jest typu MojaKlasa, ale obiekt przez nią wskazywany jest obiektem klasy anonimowej dziedziczącej po MojaKlasa. Po przypomnieniu sobie materiału z lekcji 23. i 24. stanie się jasne, że wywołanie metody f będzie wywołaniem polimorficznym, czyli zostanie wykonana metoda f z klasy anonimowej. W drugim przypadku zmienna obj2 jest typu MojaKlasa i obiekt przez nią wskazywany jest również tego typu, zostanie więc oczywiście wykonana metoda f klasy MojaKlasa. Ostatecznie na ekranie pojawi się widok zaprezentowany na rysunku 5.32. Rysunek 5.32. Polimorficzne wywołanie metody klasy anonimowej Należy rozpatrzyć jeszcze jedną kwestię związaną z klasami anonimowymi, mianowicie problem konstruktorów. Co bowiem zrobić w sytuacji, kiedy klasa bazowa, po której dziedziczy klasa anonimowa, nie ma konstruktora domyślnego? Na szczęście nie trzeba tworzyć żadnych skomplikowanych konstrukcji. Wystarczy przekazać odpowiedni argument podczas tworzenia klasy anonimowej. Zostało to zobrazowane na listingu 5.67. Listing 5.67. public class MojaKlasa { private int liczba; public MojaKlasa(int liczba) { this.liczba = liczba; } /* pola i metody klasy bazowej */ public static void main(String args[]) { MojaKlasa obj1 = new MojaKlasa(1) { /* pola i metody klasy anonimowej */ }; /* dalsze instrukcje metody main… */ } } Klasa MojaKlasa przedstawiona na listingu 5.67 nie ma bezargumentowego konstruktora domyślnego, zawiera natomiast konstruktor przyjmujący jeden argument typu int. Klasa anonimowa dziedzicząca po MojaKlasa musi mieć możliwość wywołania tego konstruktora, trzeba jej więc przekazać argument typu int. Umieszcza się go w nawiasie okrągłym po nazwie klasy; należy zatem postępować tak, jakby wywoływany był konstruktor klasy MojaKlasa, mimo iż w dalszych instrukcjach znajduje się definicja klasy anonimowej. 260 Java. Praktyczny kurs Klasy statyczne Dzięki informacjom z lekcji 29. wiadomo, że obiekt klasy wewnętrznej nie może istnieć bez obiektu klasy zewnętrznej. Jednym z powodów jest to, że klasa wewnętrzna przechowuje odwołanie do zawierającego ją obiektu klasy zewnętrznej i dzięki temu ma dostęp do jego składowych (por. lekcja 28.). Okazuje się jednak, że istnieje pewien specjalny typ klas wewnętrznych, których obiekty mogą istnieć nawet wtedy, kiedy nie istnieje obiekt klasy zewnętrznej. Są to wewnętrzne klasy statyczne nazywane klasami zagnieżdżonymi (lub statycznymi klasami zagnieżdżonymi). Przykład takiej klasy jest widoczny na listingu 5.68. Listing 5.68. public class Zewnetrzna { public static class Wewnetrzna { public void f() { System.out.println("Wewnetrzna:f()"); } } public static void main(String args[]) { Wewnetrzna wewnetrzna = new Wewnetrzna(); wewnetrzna.f(); } } Klasa Zewnetrzna jest klasą zewnętrzną, natomiast Wewnetrzna to klasa wewnętrzna — utworzona jednak przy użyciu słowa kluczowego static. Oznacza to, że można utworzyć obiekt tej klasy, nawet jeśli nie istnieje obiekt klasy Zewnetrzna. Taka sytuacja ma miejsce w metodzie main. Powstaje tam obiekt klasy Wewnetrzna i jest przypisywany zmiennej wewnetrzna, mimo że nie istnieje obiekt klasy zewnętrznej. Widać też wyraźnie, że użyto zwykłej składni z wykorzystaniem operatora new, tak jak w przypadku typowych klas (pamiętajmy, że w przypadku klas wewnętrznych z lekcji 28. i 29. używana była składnia specjalna). Konsekwencją tego stanu (statyczności klasy wewnętrznej) jest brak powiązania klasy wewnętrznej z zewnętrzną. Nie można zatem odwoływać się do pól i metod klasy wewnętrznej z klasy zewnętrznej, nie jest też możliwa sytuacja odwrotna. Odwołanie może nastąpić wyłącznie poprzez obiekt, tak jak zostało to pokazane w przykładzie z listingu 5.68 (wywołanie metody f). Inne próby odwołań skończą się błędami kompilacji. Przekonamy się o tym, jeśli spróbujemy skompilować kod z listingu 5.69. Zamiast spodziewanych plików wynikowych pojawi się seria komunikatów o błędach (rysunek 5.33). Listing 5.69. public class Zewnetrzna { public int zewLiczba; public static class Wewnetrzna { public int wewLiczba; public void f() { //nieprawidłowo, nie ma dostępu do pola //zewLiczba klasy Zewnetrzna zewLiczba = 100; Rozdział 5. Programowanie obiektowe. Część II 261 wewLiczba = 100; System.out.println("Wewnetrzna:f()"); } } public void g() { zewLiczba = 100; //nieprawidłowo, nie ma dostępu do pola //wewLiczba klasy Wewnetrzna wewLiczba = 100; //nieprawidłowo, nie ma dostępu do metody //f klasy Wewnetrzna f(); } } Rysunek 5.33. Nieprawidłowy dostęp do składowych klas Interfejsy funkcjonalne i wyrażenia lambda W praktyce programistycznej spotyka się sytuacje, kiedy pewnej metodzie trzeba przekazać fragment kodu wykonujący jakieś zadanie. Często jest tak np. w przypadku funkcji bibliotecznych wykonujących sortowanie — potrzebują one informacji o tym, który z dwóch dowolnych elementów jest mniejszy, a który większy. Jeżeli procedura sortująca ma być uniwersalna, kod wykonujący takie porównanie musi być napisany przez programistę stosującego daną funkcję biblioteczną. Przyjrzyjmy się jednak całej koncepcji na nieco prostszym przykładzie. Załóżmy, że mamy klasę przechowującą długości przyprostokątnych trójkąta prostokątnego, która zawiera również metodę wyliczającą długość przeciwprostokątnej. Niech klasa nazywa się po prostu Przyprostokatne, ma dwa pola typu double, p1 i p2, a także konstruktor i metody setPrzyprostokatne oraz przeciwprostokatna. Ostatnia wymieniona metoda ma zwracać długość przeciwprostokątnej w postaci wartości typu double. Kod tak opisanej klasy Przyprostokatne mógłby wyglądać tak jak na listingu 5.70. 262 Java. Praktyczny kurs Listing 5.70. public class Przyprostokatne{ private double p1; private double p2; Przyprostokatne(double p1, double p2){ setPrzyprostokatne(p1, p2); } public void setPrzyprostokatne(double p1, double p2){ this.p1 = p1; this.p2 = p2; } public double przeciwprostokatna(){ return Math.sqrt(p1 * p1 + p2 * p2); } } Pola p1 i p2 są prywatne, a ich wartości mogą być zmieniane dzięki metodzie set Przyprostokatne. W klasie jest tylko jeden dwuargumentowy konstruktor, w którym ta metoda jest wywoływana. Dzięki temu tworzenie obiektów klasy Przyprostokatne będzie możliwe wyłącznie po podaniu długości obu przyprostokątnych — nie da się zatem zapomnieć o podaniu jednej z wartości. W metodzie przeciwprostokatna za pomocą powszechnie znanego wzoru (pierwiastek z sumy kwadratów przyprostokątnych daje przeciwprostokątną) wyliczana jest długość przeciwprostokątnej. Pierwiastek kwadratowy uzyskiwany jest dzięki wywołaniu statycznej metody sqrt z klasy Math (tak jak miało to miejsce w przykładach z rozdziału 2.), a zamiast podnoszenia do drugiej potęgi zostało użyte mnożenie. Działanie nowej klasy można wypróbować za pomocą programu przedstawionego na listingu 5.71. W metodzie main tworzony jest nowy obiekt klasy Przyprostokatne, a w konstruktorze przekazywane są wartości 3 i 4. Następnie wywoływana jest metoda przeciwprostokatna wykonująca obliczenie długości przeciwprostokątnej dla zadanych danych, a wynik jej działania jest wyświetlany na ekranie (łatwo przewidzieć, że będzie to wartość 5). Listing 5.71. public class Main { public static void main(String args[]) { Przyprostokatne p = new Przyprostokatne(3, 4); System.out.println(p.przeciwprostokatna()); } } W przygotowanym kodzie nie ma nic nadzwyczajnego, zastosowane zostały różne techniki omawiane do tej pory. Metodę przeciwprostokatna można byłoby jednak przygotować w taki sposób, aby wykonywała dodatkowe działania na przyprostokątnych przed wyliczeniem przeciwprostokątnej, przy czym to, jakie to będą działania, ma być ustalane przez programistę korzystającego z klasy Przyprostokatne, a nie przez twórcę tej klasy. Być może potrzebne będą wyniki dla przedłużonych albo skróconych boków trójkąta, może obie wielkości będzie trzeba pomnożyć albo podzielić. Nie chcemy żadnych ograniczeń. Rozdział 5. Programowanie obiektowe. Część II 263 A zatem metoda przeciwprostokatna ma być napisana tak, aby przy jej wywoływaniu dało się przekazać jej kod wykonywalny operujący na wartościach pól p1 i p2. Same wartości pól mają jednak pozostać niezmienione — nie chcemy zmieniać parametrów trójkąta, chcemy jedynie dowiedzieć się, co będzie się teoretycznie działo przy różnych zmianach długości boków. Jak jednak przekazać kod wykonywalny, czyli w rzeczywistości treść innej metody? Nie ma możliwości, by zrobić to bezpośrednio. Moglibyśmy jednak przekazać obiekt zawierający metodę wykonującą żądane przez nas obliczenia. Utwórzmy więc przeciążoną wersję metody przeciwprostokatna przyjmującą argument będący obiektem typu Przeksztalcenia, który zawiera metodę o nazwie przeksztalc. Ta metoda będzie przyjmowała jeden argument typu double i zwracała jego wartość po pewnym przekształceniu. Metoda przeciwprostokatna przyjmie wtedy postać przedstawioną na listingu 5.72. Listing 5.72. public double przeciwprostokatna(Przeksztalcenia prz){ double p1 = prz.przeksztalc(this.p1); double p2 = prz.przeksztalc(this.p2); return Math.sqrt(p1 * p1 + p2 * p2); } Teraz przed wykonaniem właściwego obliczenia wartości pól p1 i p2 są poddawane przekształceniu, o którego charakterze decyduje metoda przeksztalc obiektu typu Przeksztalcenia wskazywanego przez argument prz. Instrukcja return nie zmieniła się przy tym w stosunku do poprzedniego przykładu, jednak (na co warto zwrócić uwagę) operuje ona tym razem na zmiennych lokalnych metody przeciwprostokatna, a nie na polach obiektu klasy Przyprostokatne (jak miało to miejsce w poprzednim przypadku). Pierwotny stan obiektu nie zostanie zatem zmieniony. To jeszcze nie koniec działań. Programista korzystający z najnowszej wersji naszej klasy będzie musiał używać obiektów klasy Przeksztalcenia zawierających metodę przeksztalc o zadanej sygnaturze (deklaracji). Skąd ma wiedzieć, jak ta metoda ma wyglądać? Trzeba byłoby przygotować wzorcową wersję klasy. Nie powinna ona jednak zawierać implementacji metody przeksztalc, ale jedynie jej deklarację. Czyli można byłoby utworzyć klasę abstrakcyjną. Tylko czy aby na pewno chcemy wymuszać korzystanie z konkretnej klasy Przeksztalcenia, która będzie pełniła wyłącznie rolę pomocniczą (rolę opakowania dla metody przeksztalc)? Nie chodzi przecież o to, żeby dostarczany był obiekt konkretnej klasy (dokładniej: pochodny od pewnej klasy abstrakcyjnej), ale o to, by dostarczony został obiekt zawierający konkretną metodę (nie ma tak naprawdę znaczenia, jakiej będzie klasy, chodzi tylko o metodę). Istnienie metody w obiekcie można wymusić za pomocą interfejsu. A więc najlepiej przygotować po prostu interfejs Przeksztalcenia (lub o innej nazwie). Mógłby on mieć postać przedstawioną na listingu 5.73. Listing 5.73. public interface Przeksztalcenia{ public double przeksztalc(double arg); } 264 Java. Praktyczny kurs Taki interfejs, który zawiera deklarację tylko jednej metody abstrakcyjnej, nazywamy interfejsem funkcjonalnym (ang. functional interface). Powinno już być teraz widać, że praktyczne wykorzystanie klasy Przyprostokatne i metody przeciwprostokatna będzie wymagało użycia obiektu DOWOLNEJ klasy implementującej interfejs Przeksztalcenia (taka klasa mogłaby też implementować dowolnie wiele innych interfejsów). Może ona być publiczna, pakietowa, wewnętrzna. Zatem przykładowy program mógłby wyglądać tak jak na listingu 5.74. Listing 5.74. class Zwiekszaj implements Przeksztalcenia{ public double przeksztalc(double arg){ return arg + 5; } } public class Main { public static void main(String args[]) { Przyprostokatne p = new Przyprostokatne(3, 4); System.out.println(p.przeciwprostokatna(new Zwiekszaj())); } } Przygotowana została tu pakietowa klasa Zwiekszaj, która implementuje interfejs Przeksztalcenia i w związku z tym zawiera metodę przeksztalc. Ta metoda zwraca przekazany jej argument typu double powiększony o wartość 5. W programie (klasa Main, metoda main) tworzony jest nowy obiekt typu Przyprostokatne, a następnie wywoływana jest jego metoda przeciwprostokatna. To takie samo działanie jak w przypadku przykładu z listingu 5.71. Wykorzystana została jednak przeciążona wersja metody, która otrzymała w postaci argumentu nowy obiekt klasy Zwiekszaj. Tym samym przed obliczeniem długości przeciwprostokątnej wartości przyprostokątnych zostały zwiększone o 5 (co wynika z działania metody przeksztalc z klasy Zwiekszaj). Przy takim zapisie pojawia się jednak pewna niedogodność. Kod miał być przecież możliwie uniwersalny, tak aby przed obliczeniem przeciwprostokątnej można było łatwo zmieniać w różny sposób długości przyprostokątnych. Tymczasem użycie typowych konstrukcji wymaga dla każdego rodzaju obliczeń pisania osobnych klas (np. klasa zmniejszająca, mnożąca, dzieląca, dodająca itd.). To na pewno nie byłoby wygodne. Od czego jednak są klasy anonimowe. Przecież klasa jest tak naprawdę jedynie opakowaniem dla metody przeksztalc. W większość przypadków nie będzie więc potrzeby jej nazywania i tworzenia osobnych konstrukcji. Nic nie stoi na przeszkodzie, aby metodzie przeciwprostokatna przekazywać obiekty klasy anonimowej implementującej interfejs Przeksztalcenia. Wyglądałoby to wtedy tak jak na listingu 5.75. Listing 5.75. public class Main { public static void main(String args[]) { Przyprostokatne p = new Przyprostokatne(3, 4); System.out.println(p.przeciwprostokatna( new Przeksztalcenia(){ public double przeksztalc(double arg){ Rozdział 5. Programowanie obiektowe. Część II 265 return arg - 1; } }) ); } } Przedstawiony zapis może wydawać się niezbyt czytelny, ale to wyłącznie kwestia przyzwyczajenia. To rodzaj wywołania kaskadowego (charakterystycznego dla stylu programowania funkcyjnego), w którym unika się tworzenia zbędnych zmiennych pomocniczych (dodatkowo można byłoby pozbyć się zmiennej p, co może być dobrym ćwiczeniem do samodzielnego wykonania). Metoda println otrzymała w postaci argumentu wartość zwróconą przez wywołanie metody przeciwprostokatna obiektu wskazywanego przez zmienną p. Argumentem metody przeciwprostokatna jest z kolei nowy obiekt klasy anonimowej implementującej interfejs Przeksztalcenia. W związku z tym pojawi się definicja metody przeksztalc, która tym razem zwraca wartość przekazanego jej argumentu pomniejszoną o 1. Tym samym w efekcie na ekranie pojawi się długość przeciwprostokątnej dla wartości przyprostokątnych zmniejszonych o 1. Gdyby całość rozpisać z użyciem zmiennych pomocniczych, program wyglądałby tak jak na listingu 5.76 (dla początkujących programistów ta wersja będzie zapewne bardziej klarowna). Listing 5.76. public class Main { public static void main(String args[]) { Przyprostokatne p = new Przyprostokatne(3, 4); Przeksztalcenia prz = new Przeksztalcenia(){ public double przeksztalc(double arg){ return arg - 1; } }; double wynik = p.przeciwprostokatna(prz); System.out.println(wynik); } } Zwróćmy tu jednak ponownie uwagę na kwestię, która została już zasygnalizowana. Otóż jedynym celem, dla którego istnieje klasa bądź interfejs Przeksztalcenia, jest opakowanie metody znajdującej się wewnątrz. To ta metoda jest istotna. Niestety nie można przekazać metody jako argumentu, dlatego stosuje się przedstawione wyżej nadmiarowe konstrukcje programistyczne (trzeba napisać sporo dodatkowego kodu, który pełni tylko funkcję pomocniczą13). W Java 8, jeśli mamy do czynienia z interfejsem funkcjonalnym (jak w powyższych przykładach), zapis można uprościć, korzystając z tzw. 13 To skądinąd charakterystyczna cecha Javy, która jest często przez praktyków określana jako język „przegadany”. Dlatego też wiele zintegrowanych narzędzi programistycznych (NetBeans, Eclipse, IntelliJ itp.) umożliwia automatyczne generowanie wielu typowych struktur. 266 Java. Praktyczny kurs wyrażeń lambda (ang. lambda expressions). Takie wyrażenie można przedstawić w schematycznej postaci: (typ1 arg1, typ2 arg2, … , typN argN) -> {treść metody} i należy je umieścić zamiast całej definicji klasy anonimowej implementującej dany interfejs. Przykładowo dla wersji z listingu 5.75 wyrażenie wyglądałoby jak poniżej: (double arg) -> {return arg - 1;} Stąd wniosek, że cały program korzystający z tej uproszczonej składni miałby postać przedstawioną na listingu 5.77. Listing 5.77. public class Main { public static void main(String args[]) { Przyprostokatne p = new Przyprostokatne(3, 4); System.out.println(p.przeciwprostokatna( (double arg) -> { return arg - 1; } ); } } Ćwiczenia do samodzielnego wykonania Ćwiczenie 30.1. Do klasy MojaKlasa z listingu 5.64 dopisz metodę toString. Użyj jej do wyświetlania wartości pola liczba po każdej wykonanej operacji. Czy będzie można skorzystać z tej metody w przypadku obiektu pi? Ćwiczenie 30.2. Napisz klasę, która będzie zawierała metodę getI1 zwracającą nowy obiekt anonimowej klasy implementującej interfejs o nazwie I1. Ćwiczenie 30.3. Napisz klasę anonimową, która będzie klasą pochodną od klasy niemającej konstruktora domyślnego. Klasa bazowa powinna natomiast zawierać dwa konstruktory: jeden przyjmujący argument typu int oraz drugi przyjmujący argument typu double. Utwórz dwa obiekty klasy anonimowej, korzystając z obu konstruktorów. Ćwiczenie 30.4. Napisz statyczną klasę zagnieżdżoną zawierającą metodę statyczną o nazwie f wyświetlającą dowolny napis na ekranie. Wywołaj tę metodę, nie tworząc żadnego obiektu. Rozdział 5. Programowanie obiektowe. Część II 267 Ćwiczenie 30.5. Napisz taką wersję programu z listingu 5.75, aby nie została w niej użyta żadna zmienna. Dodatkowo wykonaj to ćwiczenie, stosując wyrażenie lambda. Ćwiczenie 30.6. Przygotuj taką wersję klasy Przyprostokatne, aby można było osobno decydować o przekształceniu wykonywanym na każdej z przyprostokątnych (np. pierwszą zwiększyć, a drugą zmniejszyć). Czy trzeba będzie wprowadzać zmiany do interfejsu Przeksztalcenia? Napisz program testujący nowe możliwości. 268 Java. Praktyczny kurs Rozdział 6. System wejścia-wyjścia Do pisania aplikacji w Javie niezbędna jest znajomość przynajmniej podstaw systemu wejścia-wyjścia. Tej tematyce jest poświęcony ten rozdział. W Javie operacje wejścia-wyjścia są wykonywane za pomocą strumieni. Strumień to abstrakcyjny ciąg danych, który działa — mówiąc w uproszczeniu — w taki sposób, że dane wprowadzone na jednym jego końcu pojawiają się na drugim końcu. Strumienie dzielą się na wejściowe i wyjściowe. Strumienie wyjściowe mają początek w aplikacji i koniec w innym urządzeniu, np. na ekranie czy w pliku, umożliwiają zatem wyprowadzanie danych z programu. Przykładowo standardowy strumień wyjściowy jest reprezentowany przez obiekt System.out, a wykonanie metody println tego obiektu powoduje, że dane domyślnie zostają wysłane na ekran. Strumienie wejściowe działają odwrotnie: ich początek znajduje się poza aplikacją (może to być np. klawiatura albo plik dyskowy), a koniec w aplikacji, umożliwiają zatem wprowadzanie danych. Co więcej, strumienie umożliwiają też komunikację między obiektami w obrębie jednej aplikacji. W tym rozdziale zostanie jednak omówiona tylko komunikacja aplikacji ze światem zewnętrznym. Lekcja 31. Standardowe wejście Opisy podstawowych operacji wyjściowych, czyli wyświetlania informacji na ekranie konsoli, pojawiły się już wielokrotnie; operacje te zostaną dokładniej przedstawione w ramach kolejnej lekcji. Omówienie systemu wejścia-wyjścia rozpoczniemy od wprowadzania danych i standardowego strumienia wejściowego reprezentowanego przez obiekt System.in. Tej tematyce zostanie poświęcona cała bieżąca lekcja. Zostaną w niej przedstawione informacje, jak odczytywać dane wprowadzane przez użytkownika z klawiatury. Standardowy strumień wejściowy Standardowy strumień wejściowy jest reprezentowany przez obiekt System.in, czyli obiekt in zawarty w klasie System. Jest to obiekt typu InputStream, klasy reprezentującej strumień wejściowy. Metody udostępniane przez tę klasę są zebrane w tabeli 6.1. Jak widać, nie jest to imponujący zestaw, niemniej jest to podstawowa klasa operująca na strumieniu wejściowym. 270 Java. Praktyczny kurs Tabela 6.1. Metody udostępniane przez klasę InputStream Deklaracja metody Opis int available() Zwraca przewidywaną liczbę bajtów, które mogą być pobrane ze strumienia przy najbliższym odczycie. void close() Zamyka strumień i zwalnia związane z nim zasoby. void mark(int readlimit) Zaznacza bieżącą pozycję w strumieniu. boolean markSupported() Sprawdza, czy strumień może obsługiwać metody mark i reset. abstract int read() Odczytuje kolejny bajt ze strumienia. int read(byte[] b) Odczytuje ze strumienia liczbę bajtów nie większą niż rozmiar tablicy b. Zwraca rzeczywiście odczytaną liczbę bajtów. int read(byte[] b, int off, int len) Odczytuje ze strumienia liczbę bajtów nie większą niż określona przez len i zapisuje je w tablicy b, począwszy od komórki wskazywanej przez off. Zwraca rzeczywiście przeczytaną liczbę bajtów. void reset() Wraca do pozycji strumienia wskazywanej przez wywołanie metody mark. long skip(long n) Pomija w strumieniu liczbę bajtów wskazywanych przez n. Zwraca rzeczywiście pominiętą liczbę bajtów. Widać wyraźnie, że odczyt bajtów ze strumienia można przeprowadzić za pomocą jednej z metod o nazwie read. Przyjrzyjmy się metodzie odczytującej tylko jeden bajt. Mimo że odczytuje ona bajt, zwraca wartość typu int. Jest tak dlatego, że zwracana wartość zawsze zawiera się w przedziale 0 – 255 (to tyle, ile może przyjmować jeden bajt). Tymczasem zmienna typu byte reprezentuje wartości „ze znakiem” w zakresie od –128 do +1271. Spróbujmy zatem napisać prosty program, który odczyta wprowadzony z klawiatury znak, a następnie wyświetli ten znak oraz jego kod ASCII na ekranie. Kod realizujący przedstawione zadanie jest widoczny na listingu 6.1. Listing 6.1. import java.io.*; public class Main { public static void main(String args[]) { System.out.print("Wprowadź dowolny znak z klawiatury: "); try{ char c = (char) System.in.read(); System.out.print("Wprowadzony znak to: "); System.out.println(c); System.out.print("Jego kod to: "); System.out.println((int) c); } catch(IOException e){ System.out.println("Błąd podczas odczytu strumienia."); } } } 1 Oczywiście reprezentowanie zakresu od 0 do 255 byłoby możliwe również za pomocą typów short i long. Rozdział 6. System wejścia-wyjścia 271 Pierwszym krokiem jest zaimportowanie pakietu java.io (lekcja 17.), znajduje się w nim bowiem definicja wyjątku IOException, który musimy przechwycić. W metodzie main za pomocą metody System.out.println wyświetlamy tekst z prośbą o wprowadzenie dowolnego znaku z klawiatury, a następnie wykonujemy instrukcję: char c = (char) System.in.read(); Wywołujemy zatem metodę read obiektu in zawartego w klasie System2. Obiekt ten jest klasy InputStream (typem obiektu jest InputStream) i reprezentuje standardowy strumień wejściowy. W typowym przypadku jest on powiązany z klawiaturą, więc z tego strumienia będą odczytywane dane wprowadzane przez użytkownika z klawiatury. Metoda read zwraca wartość typu int, dokonujemy zatem konwersji na typ char, tak aby wyświetlić na ekranie wprowadzony znak, i przypisujemy go zmiennej c. Następnie wyświetlamy zawartość tej zmiennej za pomocą instrukcji: System.out.println(c); Aby wyświetlić kod znaku, należy ponownie dokonać konwersji. Zmienną c typu char konwertujemy na typ int, tak aby metoda println potraktowała ją jako liczbę, a nie jako znak. Ponieważ metoda read może spowodować powstanie wyjątku IOException, wszystkie instrukcje zostały ujęte w blok try, który zapobiegnie niekontrolowanemu zakończeniu programu (por. lekcje z rozdziału 4.). Przykładowy efekt działania programu z listingu 6.1 jest widoczny na rysunku 6.1. Rysunek 6.1. Przykładowy efekt działania programu z listingu 6.1 Trzeba zwrócić uwagę, że w ten sposób tak naprawdę odczytywany jest ze strumienia nie jeden znak, ale jeden bajt. A te pojęcia nie są tożsame. Jeden znak może zawierać w zależności od standardu kodowania od jednego do kilku bajtów. Wczytywanie tekstu Wiadomo już, jak odczytać jeden bajt, co jednak zrobić, aby wprowadzić całą linię tekstu? Przecież taka sytuacja jest o wiele częstsza. Można oczywiście odczytywać pojedyncze znaki w pętli tak długo, aż zostanie osiągnięty znak końca linii, oraz połączyć je w obiekt typu String. Najlepiej byłoby wręcz wyprowadzić z klasy InputStream klasę pochodną, która zawierałaby metodę np. o nazwie readLine, wykonującą taką czynność. Trzeba byłoby przy tym pamiętać o odpowiedniej obsłudze standardów kodowania znaków. Na szczęście w JDK został zdefiniowany cały zestaw klas operujących na strumieniach wejściowych. Zamiast więc powtarzać coś, co zostało już zrobione, najlepiej po prostu z nich skorzystać. 2 Obiekt in jest polem finalnym i statycznym klasy System. 272 Java. Praktyczny kurs Zadanie to będzie wymagało użycia dwóch klas pośredniczących. Klasą udostępniającą metodę readLine, która jest w stanie prawidłowo zinterpretować znaki przychodzące ze strumienia, jest BufferedReader. Aby jednak można było utworzyć obiekt takiej klasy, musi powstać obiekt klasy Reader lub klasy od niej pochodnej — w tym przypadku najodpowiedniejszy będzie InputStreamReader. Ponieważ nie ma takiego obiektu w aplikacji (do dyspozycji mamy jedynie obiekt System.in typu InputStream), należy go utworzyć, wywołując odpowiedni konstruktor. Będzie to obiekt (strumień) pośredniczący w wymianie danych. Do jego utworzenia potrzeba z kolei obiektu klasy InputStream, a tym przecież dysponujemy. Dlatego też kod programu, który odczytuje linię tekstu ze standardowego wejścia, będzie wyglądać tak, jak zostało to przedstawione na listingu 6.2. Listing 6.2. import java.io.*; public class Main { public static void main(String args[]) { BufferedReader brIn = new BufferedReader( new InputStreamReader(System.in) ); System.out.println("Wprowadź linię tekstu zakończoną znakiem Enter:"); try{ String line = brIn.readLine(); System.out.print("Wprowadzona linia to: " + line); } catch(IOException e){ System.out.println("Błąd podczas odczytu strumienia."); } } } Pierwszym zadaniem jest utworzenie obiektu brIn klasy BufferedReader. Mamy tu do czynienia ze złożoną instrukcją, która najpierw tworzy obiekt typu InputStreamReader, wykorzystując obiekt System.in, i dopiero ten obiekt przekazuje konstruktorowi klasy BufferedReader. Zatem konstrukcję: BufferedReader brIn = new BufferedReader( new InputStreamReader(System.in) ); można również rozbić na dwie części: InputStreamReader isr = new InputStreamReader(System.in); BufferedReader brIn = new BufferedReader(isr); Znaczenie będzie takie samo, jednak w drugim przypadku zostanie utworzona zupełnie niepotrzebnie dodatkowa zmienna isr typu InputStreamReader. To, który ze sposobów jest czytelniejszy i zostanie zastosowany, zależy jednak od indywidualnych preferencji programisty. Z reguły stosuje się sposób pierwszy. Kiedy obiekt klasy BufferedReader jest już utworzony, można wykorzystać metodę readLine, która zwróci w postaci obiektu typu String (klasa String reprezentuje ciągi Rozdział 6. System wejścia-wyjścia 273 znaków) linię tekstu wprowadzoną przez użytkownika. Linia tekstu jest rozumiana jako ciąg znaków wprowadzony aż do naciśnięcia klawisza Enter (dokładniej: aż do osiągnięcia znaku końca wiersza w strumieniu). Wynika z tego, że instrukcja: String line = brIn.readLine(); utworzy zmienną line typu String i przypisze jej obiekt (referencję do obiektu) zwrócony przez metodę readLine obiektu brIn. Działanie programu będzie zatem następujące: po uruchomieniu zostanie wyświetlona prośba o wprowadzenie linii tekstu, następnie linia ta zostanie odczytana i przypisana zmiennej line, a potem zawartość tej zmiennej zostanie wyświetlona na ekranie. Przykład wykonania tego programu pokazano na rysunku 6.2. Rysunek 6.2. Wczytanie linii tekstu za pomocą obiektu klasy BufferedReader Skoro wiadomo już, jak wczytać wiersz tekstu z klawiatury, spróbujmy napisać program, którego zadaniem będzie wczytywanie kolejnych linii ze standardowego wejścia tak długo, aż zostanie odczytany ciąg znaków quit. Zadanie wydaje się banalne, wystarczy przecież do kodu z listingu 6.2 dodać pętlę while, która będzie sprawdzała, czy zmienna line zawiera napis quit. W pierwszym odruchu napiszemy zapewne pętlę, która będzie wyglądać następująco (przy założeniu, że wcześniej został prawidłowo zainicjowany obiekt brIn): String line = ""; while(line != "quit"){ line = brIn.readLine(); System.out.println("Wprowadzona linia to: " + line); } System.out.println("Koniec wczytywania danych."); Warto przeanalizować ten przykład. Na początku deklarujemy zmienną line i przypisujemy jej pusty ciąg znaków. W pętli while sprawdzamy warunek, czy zmienna line jest różna od ciągu znaków quit — jeśli tak, to pętla jest kontynuowana, a jeśli nie, to kończy działanie. Zatem znaczenie jest następujące: dopóki line jest różne od quit, wykonuj instrukcje z wnętrza pętli. Wewnątrz pętli przypisujemy natomiast zmiennej line wiersz tekstu odczytany ze standardowego strumienia wejściowego oraz wyświetlamy go na ekranie. Wszystko to wygląda bardzo poprawnie, ma tylko jedną wadę — nie zadziała zgodnie z założeniami. Aby się o tym przekonać, wystarczy uruchomić program z listingu 6.3, następnie wpisać kilka linii tekstu, a po nich ciąg znaków quit. Teoretycznie program powinien zakończyć działanie, tymczasem działa nadal, co jest widoczne na rysunku 6.3. 274 Java. Praktyczny kurs Rysunek 6.3. Błąd w pętli while uniemożliwia zakończenie programu Listing 6.3. import java.io.*; public class Main { public static void main(String args[]) { BufferedReader brIn = new BufferedReader( new InputStreamReader(System.in) ); System.out.println("Wprowadzaj linie tekstu. Aby zakończyć, wpisz quit."); String line = ""; try{ while(line != "quit"){ line = brIn.readLine(); System.out.println("Wprowadzona linia to: " + line); } System.out.println("Koniec wczytywania danych."); } catch(IOException e){ System.out.println("Błąd podczas odczytu strumienia."); } } } Co się dzieje, gdzie jest błąd? Podejrzenie powinno najpierw paść na warunek zakończenia pętli while. On rzeczywiście jest nieprawidłowy, choć wydaje się, że instrukcja ta jest poprawna. Zwróćmy jednak uwagę na to, co tak naprawdę jest w tym warunku porównywane. Miał być porównany ciąg znaków zapisany w zmiennej line z ciągiem znaków quit. Tymczasem po umieszczeniu w kodzie sekwencji "quit" powstał nienazwany obiekt klasy String faktycznie zawierający ciąg znaków quit, a w miejsce napisu "quit" została wstawiona referencja do tego obiektu. Jak wiadomo z wcześniejszych lekcji (lekcja 13.), zmienna line jest także referencją do obiektu klasy String. Instrukcja porównania line != "quit" porównuje zatem ze sobą dwie REFERENCJE, które w tym przypadku muszą być różne. W związku z tym nie dochodzi tu do porównania zawartości obiektów. Dlatego program nie może działać poprawnie i wpada w nieskończoną pętlę (aby zakończyć jego działanie, trzeba wcisnąć na klawiaturze kombinację klawiszy Ctrl+C). Koniecznie należy więc zapamiętać, że do porównywania obiektów nie używa się operatorów porównywania! Zamiast tego należy skorzystać z metody equals. Jest ona zadeklarowana w klasie Object i w związku z tym dziedziczona przez wszystkie klasy. Rozdział 6. System wejścia-wyjścia 275 Warto pamiętać o tym podczas tworzenia własnych klas, by w razie potrzeby dopisać własną wersję tej metody. Jak zastosować tę metodę w praktyce? Otóż zwraca ona wartość true, kiedy obiekty są takie same (czyli ich zawartość jest taka sama), lub false, kiedy obiekty są różne. W przypadku obiektów typu String wartość true zostanie zwrócona, jeśli ciąg znaków zapisany w jednym z nich jest taki sam jak ciąg znaków zapisany w drugim. Jeżeli np. istnieje zmienna zadeklarowana jako: String napis1 = "test"; to wynikiem operacji: napis1.equals("test"); będzie wartość true, a wynikiem operacji: napis1.equals("java"); będzie wartość false. Powróćmy teraz do pętli while. Jako że znane są nam już właściwości metody equals, w pierwszej chwili na pewno na myśl przyjdzie nam warunek: while(!line.equals("quit")){ /*instrukcje pętli while*/ } Jeśli wprowadzimy go do kodu z listingu 6.3, aplikacja zacznie poprawnie reagować na polecenie quit — będzie się wydawać, że całość nareszcie działa prawidłowo. W zasadzie można byłoby się zgodzić z tym stwierdzeniem, gdyż program rzeczywiście działa zgodnie z założeniami, tylko że teraz zawiera kolejny błąd, tym razem dużo trudniejszy do wykrycia. Ujawni się on dopiero przy próbie przerwania pracy aplikacji. Spróbujmy uruchomić program, a po wpisaniu testowej linii tekstu przerwijmy jego działanie (w większości systemów należy wcisnąć kombinację klawiszy Ctrl+C). Efekt jest widoczny na rysunku 6.4. Zapewne nie spodziewaliśmy się wyjątku NullPointerException… Rysunek 6.4. Ukryty błąd w programie spowodował wygenerowanie wyjątku Na przyczynę powstania tego błędu naprowadza linia tekstu poprzedzająca komunikat maszyny wirtualnej, mianowicie: Wprowadzona linia to: null. Oznacza to bowiem (spójrzmy na kod), że metoda readLine obiektu brIn zwróciła — zamiast ciągu znaków — wartość null. Jest to standardowe działanie, metoda ta zwraca wartość null, kiedy zostanie osiągnięty koniec strumienia, czyli kiedy nie ma w nim już żadnych danych do odczytania. Co się dzieje dalej? Otóż wartość null jest przypisywana zmiennej line, 276 Java. Praktyczny kurs a potem w warunku pętli while następuje próba wywołania metody equals obiektu wskazywanego przez line. Tymczasem line ma wartość null i nie wskazuje na żaden obiekt. W takiej sytuacji musi zostać wygenerowany wyjątek NullPointerException. Jak temu zapobiec? Możliwości są dwie: albo dodamy w pętli warunek sprawdzający, czy line jest równe null, a jeśli tak, to przerwiemy pętlę np. instrukcją break, albo też zmodyfikujemy sam warunek pętli. Ta druga możliwość jest lepsza, gdyż nie powoduje wykonywania dodatkowego kodu, a jednocześnie otrzymujemy ciekawą konstrukcję. Poprawiony warunek powinien wyglądać następująco: while(!"quit".equals(line)){ /*instrukcje pętli while*/ } Początkowo tego typu zapis budzi zdziwienie: jak można wywoływać jakąkolwiek metodę na rzecz ciągu znaków? Wystarczy jednak sobie przypomnieć stwierdzenie, które pojawiło się kilka akapitów wyżej, otóż literał3 "quit" w rzeczywistości powoduje powstanie obiektu klasy String i podstawianie w jego (literału) miejsce referencji do tego obiektu. Skoro tak, można wywołać metodę equals. To właśnie dzięki takiej konstrukcji unikamy wyjątku NullPointerException, gdyż nawet jeśli line będzie miało wartość null, to metoda equals po prostu zwróci wartość false, bo null na pewno jest różne od ciągu znaków quit. Ostatecznie pełny prawidłowy kod będzie miał postać przedstawioną na listingu 6.4. Przykład działania tego kodu został z kolei zaprezentowany na rysunku 6.5. Listing 6.4. import java.io.*; public class Main { public static void main(String args[]) { BufferedReader brIn = new BufferedReader( new InputStreamReader(System.in) ); System.out.println("Wprowadzaj linie tekstu. Aby zakończyć, wpisz quit."); String line = ""; try{ while(!"quit".equals(line)){ line = brIn.readLine(); System.out.println("Wprowadzona linia to: " + line); } System.out.println("Koniec wczytywania danych."); } catch(IOException e){ System.out.println("Błąd podczas odczytu strumienia."); } } } 3 Czyli inaczej stała znakowa (napisowa), stały ciąg znaków umieszczony w kodzie programu. Rozdział 6. System wejścia-wyjścia 277 Rysunek 6.5. Wprowadzanie w pętli kolejnych linii tekstu Wprowadzanie liczb Potrafimy już odczytywać w aplikacji wiersze tekstu wprowadzane z klawiatury, równie ważną umiejętnością jest wprowadzanie liczb. Jak to zrobić? Trzeba sobie uzmysłowić, że z klawiatury zawsze wprowadzany jest tekst. Jeśli próbujemy wprowadzić do aplikacji wartość 123, to w rzeczywistości wprowadzimy trzy znaki, "1", "2" i "3" o kodach ASCII 61, 62, 63. Mogą one zostać przedstawione w postaci ciągu "123", ale to dopiero aplikacja musi przetworzyć ten ciąg na wartość 123. Takiej konwersji w przypadku wartości całkowitej można dokonać np. za pomocą metody parseInt z klasy Integer. Jest to metoda statyczna, można ją więc wywołać, nie tworząc obiektu klasy Integer4. Przykładowe wywołanie może wyglądać następująco: int liczba = Integer.parseInt(ciąg_znaków); Zmiennej liczba zostanie przypisana wartość typu int zawarta w ciągu znaków ciąg_ znaków. Gdyby ciąg przekazany jako argument metody parseInt nie zawierał poprawnej wartości całkowitej, zostałby wygenerowany wyjątek NumberFormatException. Aby zatem wprowadzić do aplikacji wartość całkowitą, można odczytać wiersz tekstu, korzystając z metody readLine, a następnie wywołać metodę parseInt. Ten właśnie sposób został wykorzystany w programie z listingu 6.5. Zadaniem tego programu jest wczytanie liczby całkowitej oraz wyświetlenie wyniku mnożenia tej liczby przez wartość 2. Listing 6.5. import java.io.*; public class Main { public static void main(String args[]) { BufferedReader brIn = new BufferedReader( new InputStreamReader(System.in) ); System.out.print("Wprowadź liczbę całkowitą: "); String line = null; try{ line = brIn.readLine(); } 4 Jak widać, metody statyczne, które były przedmiotem lekcji 18., przydają się w praktyce. 278 Java. Praktyczny kurs catch(IOException e){ System.out.println("Błąd podczas odczytu strumienia."); return; } int liczba; try{ liczba = Integer.parseInt(line); } catch(NumberFormatException e){ System.out.print("Podana wartość nie jest liczbą całkowitą."); return; } System.out.print(liczba + " * 2 = " + liczba * 2); } } Kod zaczyna się od utworzenia obiektu brIn klasy BufferedReader powiązanego poprzez obiekt pośredniczący klasy InputStreamReader ze standardowym strumieniem wejściowym. Stosujemy technikę opisaną przy omawianiu przykładu z listingu 6.2. Następnie wyświetlamy prośbę o wprowadzenie liczby całkowitej oraz odczytujemy wiersz tekstu za pomocą metody readLine obiektu brIn, nie zapominając o przechwyceniu ewentualnego wyjątku IOException. W przypadku wystąpienia takiego wyjątku wyświetlamy stosowną informację na ekranie oraz kończymy wykonywanie programu. Warto zwrócić uwagę, że używana jest w tym celu instrukcja return bez żadnych parametrów, co oznacza wyjście z funkcji main (bez zwracania wartości), a tym samym zakończenie działania aplikacji. Jeśli odczyt wiersza tekstu się powiedzie, zostanie on zapisany w zmiennej line. Deklarujemy więc dalej zmienną liczba typu int oraz przypisujemy jej wynik działania metody parseInt z klasy Integer. Metodzie przekazujemy ciąg znaków zapisany w zmiennej line. Jeśli wprowadzony przez użytkownika ciąg znaków nie reprezentuje poprawnej wartości liczbowej, wygenerowany zostaje wyjątek NumberFormatException. W takim wypadku wyświetlamy komunikat o tym fakcie oraz kończymy działanie funkcji main, a tym samym programu, wywołując instrukcję return. Jeśli jednak konwersja tekstu na liczbę powiedzie się, odpowiednia wartość zostanie zapisana w zmiennej liczba, można zatem wykonać mnożenie liczba * 2 i wyświetlić wartość wynikającą z tego mnożenia na ekranie. Przykładowy wynik działania programu jest widoczny na rysunku 6.6. Rysunek 6.6. Przykładowy wynik działania programu z listingu 6.5 Gdyby miała być wczytana liczba zmiennoprzecinkowa, należałoby do konwersji zastosować metodę parseDouble z klasy Double lub parseFloat z klasy Float, co jest doskonałym ćwiczeniem do samodzielnego wykonania. Rozdział 6. System wejścia-wyjścia 279 Ćwiczenia do samodzielnego wykonania Ćwiczenie 31.1. Napisz klasę Main, która będzie zawierała metodę readLine. Zadaniem tej metody będzie zwrócenie wprowadzonej przez użytkownika linii tekstu. Nie używaj do odczytu danych innych metod niż System.in.read(). Przetestuj działanie własnej metody readLine. Ćwiczenie 31.2. Zmodyfikuj kod z listingu 6.3 tak, aby warunek pętli while miał postać while(!line. equals("quit")), ale by nie występował błąd NullPointerException, kiedy osiągnięty zostanie koniec strumienia (np. po wciśnięciu kombinacji klawiszy Ctrl+C). Ćwiczenie 31.3. Napisz program (analogiczny do przedstawionego na listingu 6.5), który będzie umożliwiał wprowadzenie liczby zmiennoprzecinkowej. Ćwiczenie 31.4. Napisz klasę pochodną od BufferedReader, która będzie zawierała metody getInt oraz getDouble odczytujące ze strumienia liczby całkowite oraz zmiennopozycyjne (zmiennoprzecinkowe). Ćwiczenie 31.5. Napisz klasę Main testującą działanie metod getInt oraz getDouble z ćwiczenia 31.4. Lekcja 32. Standardowe wejście i wyjście W lekcji 32. będą kontynuowane tematy, które pojawiły się wcześniej w tym rozdziale. Omówimy klasę StreamTokenizer, która potrafi dzielić dane ze strumienia wejściowego na jednostki leksykalne nazwane tokenami, co pozwala używać jej do wprowadzania danych do aplikacji. Zostanie też pokazane, jak zbudować program wykonujący działanie arytmetyczne oraz aplikację obliczającą pierwiastki równania kwadratowego o parametrach wprowadzanych z klawiatury. Przedstawione zostaną również bardziej nowoczesne klasy Scanner i Console. Na końcu lekcji znajdzie się rozwiązanie problemu obsługi polskich znaków diakrytycznych w aplikacjach korzystających z konsoli w systemie Windows. Wykorzystanie klasy StreamTokenizer W lekcji 31. zaprezentowano sposób, w jaki odczytuje się w aplikacji liczby wprowadzane ze standardowego strumienia wejściowego. Tym sposobem było wczytanie linii tekstu za pomocą metody readLine z klasy BufferedReader oraz dokonanie konwersji 280 Java. Praktyczny kurs z wykorzystaniem metod z klas Integer, Double lub Float5. Istnieje również inna metoda: użycie klasy StreamTokenizer. Klasa ta dzieli strumień wejściowy na jednostki leksykalne, czyli tokeny. Przykładowo jeśli strumień zawiera dane: 123 test 18 to można w nim wyróżnić trzy tokeny: 123, test i 18. Po przetworzeniu otrzymamy dwie liczby oraz ciąg znaków. To, jakie znaki będą separatorami oddzielającymi poszczególne jednostki leksykalne (w powyższym przykładzie są to spacje), można zdefiniować samodzielnie, jednak dla dalszych ćwiczeń i w wielu typowych zastosowaniach domyślne ustawienia są w zupełności wystarczające (to wszystkie tzw. białe znaki, czyli spacje, tabulatory, znaki końca wiersza itp.). Jak to jednak w praktyce wykorzystać do wprowadzania liczb? Otóż klasa Stream Tokenizer ma pole o nazwie nval, które zawiera wartość aktualnego tokena w postaci liczby typu double, oczywiście o ile ten token jest liczbą. Niezbędne jest zatem rozpoznawanie sytuacji, kiedy token nie reprezentuje wartości liczbowej. W tym celu wystarczy zbadać stan pola ttype lub wartość zwróconą przez metodę nextToken. Możliwe wartości są zebrane w tabeli 6.2. Tabela 6.2. Wartości pola ttype klasy StreamTokenizer Nazwa pola Znaczenie StreamTokenizer.TT_EOF Osiągnięty został koniec strumienia. StreamTokenizer.TT_EOL Osiągnięty został koniec linii. StreamTokenizer.TT_NUMBER Token jest liczbą. StreamTokenizer.TT_WORD Token jest słowem. Koniec jednak z teorią, czas sprawdzić, jak w praktyce wygląda wykorzystanie klasy StreamTokenizer. Napiszmy program, który ze standardowego wejścia wczyta liczbę całkowitą i wyświetli na ekranie jej wartość pomnożoną przez 2 (jest to zadanie podobne do realizowanych w poprzedniej lekcji). Odpowiedni kod jest widoczny na listingu 6.6. Listing 6.6. import java.io.*; public class Main { public static void main(String args[]) { StreamTokenizer strTok = new StreamTokenizer( new BufferedReader( new InputStreamReader(System.in) ) ); System.out.print("Wprowadź liczbę: "); try{ strTok.nextToken(); 5 W sposób analogiczny można wykorzystać metody klas Byte, Long, Short. Rozdział 6. System wejścia-wyjścia 281 } catch(IOException e){ System.out.print("Błąd podczas odczytu danych ze strumienia."); return; } if(strTok.ttype != StreamTokenizer.TT_NUMBER){ System.out.print("To nie jest prawidłowa liczba."); return; } double liczba = strTok.nval; System.out.print(liczba + " * 2 = " + liczba * 2); } } Tworzymy nowy obiekt klasy StreamTokenizer. W konstruktorze musimy przekazać obiekt klasy BufferedReader6, stosujemy więc konstrukcję kaskadową podobną do wykorzystywanej w przykładach z listingów 6.2 – 6.5. Zapis: StreamTokenizer strTok = new StreamTokenizer( new BufferedReader( new InputStreamReader(System.in) ) ); można byłoby również rozbić na ciąg instrukcji wykorzystujący dodatkowe zmienne: InputStreamReader isr = InputStreamReader(System.in); BufferedReader inbr = new BufferedReader(isr); StreamTokenizer strTok = new StreamTokenizer(inbr); Efekt będzie taki sam: utworzenie obiektu strTok klasy StreamTokenizer powiązanego ze standardowym strumieniem wejściowym. Kiedy obiekt zostanie utworzony, wyświetlamy na ekranie prośbę o podanie liczby oraz wywołujemy metodę nextToken. Odczytuje ona ze strumienia kolejną jednostkę leksykalną. Wywołanie umieszczamy w bloku try, aby przechwycić ewentualny wyjątek IOException, który może się pojawić podczas czytania ze strumienia. Po pobraniu tokena sprawdzamy, czy jest on wartością liczbową — informuje o tym stan pola ttype obiektu wskazywanego przez strTok. Jeśli ta wartość jest równa wartości TT_NUMBER zdefiniowanej w klasie StreamTokenizer7, token reprezentuje liczbę, w przeciwnym wypadku nie jest liczbą. Wykorzystujemy więc dalej instrukcję if do dokonania porównania. Jeśli wprowadzona została liczba, będzie się ona znajdowała w polu nval8 obiektu strTok, przypisujemy zatem jego wartość zmiennej typu double o nazwie liczba. 6 Dokładniej: obiekt klasy Reader lub pochodnej od Reader. Zdecydowanie nie należy stosować bezpośrednio obiektów klasy InputStream, mimo że taka wersja konstruktora również istnieje. 7 W rzeczywistości jest to pole statyczne typu int. 8 Gdyby odczytana jednostka leksykalna nie zawierała liczby, ale inny ciąg znaków, ciąg ten można by było odczytać z pola o nazwie sval. 282 Java. Praktyczny kurs Tej zmiennej używamy następnie do wykonania działania arytmetycznego, którego wynik wyświetlamy na ekranie. Przykładowy efekt działania programu jest widoczny na rysunku 6.7. Rysunek 6.7. Wczytanie wartości liczbowej przy użyciu klasy StreamTokenizer Zastanówmy się teraz, jak usprawnić tę aplikację. W obecnej sytuacji, kiedy użytkownik nie wprowadzi wartości liczbowej, np. pomyli się, aplikacja zakończy działanie. Nie jest to zbyt dobre zachowanie. Najlepiej byłoby ponownie poprosić o wprowadzenie prawidłowej liczby. Przyda się zatem pętla while, która będzie działać tak długo, aż podawane dane będą poprawne. Takie rozwiązanie zostało przedstawione na listingu 6.7. Listing 6.7. import java.io.*; public class Main { public static void main(String args[]) { StreamTokenizer strTok = new StreamTokenizer( new BufferedReader( new InputStreamReader(System.in) ) ); System.out.print("Wprowadź liczbę: "); try{ while(strTok.nextToken() != StreamTokenizer.TT_NUMBER){ System.out.println("To nie jest poprawna liczba."); System.out.print("Wprowadź liczbę: "); } } catch(IOException e){ System.out.print("Błąd podczas odczytu danych ze strumienia."); return; } double liczba = strTok.nval; System.out.print(liczba + " * 2 = " + liczba * 2); } } Obiekt przypisywany zmiennej strTok jest tworzony dokładnie tak samo jak w poprzednim przykładzie. Po wyświetleniu w bloku try prośby o wprowadzenie liczby rozpoczyna się jednak działanie pętli typu while. Działa ona tak długo, aż wartość zwrócona przez wywołanie metody nextToken będzie równa TT_NUMBER. Dopóki więc użytkownik nie poda prawidłowej liczby, będzie proszony o jej podanie i wywoływana będzie metoda nextToken. Kiedy poprawna wartość liczbowa zostanie wprowadzona, pętla zakończy Rozdział 6. System wejścia-wyjścia 283 swoje działanie i wartość ta zostanie przypisana zmiennej liczba. Na ekranie jest natomiast wyświetlany wynik mnożenia wartości zapisanej w zmiennej liczba przez 2. Wprowadzanie danych w rzeczywistych aplikacjach Wszystkie prezentowane dotychczas w tym rozdziale przykłady były raczej teoretyczne, nie wykonywały żadnych praktycznych zadań. Spróbujmy tym razem napisać dwie bardzo proste aplikacje, które będą realizowały konkretne zadania. Pierwsza z nich będzie wykonywała mnożenie dwóch liczb, druga to obiecany w lekcji 7. z rozdziału 2. program obliczający pierwiastki równania kwadratowego o zadanych parametrach wprowadzanych z klawiatury. Na pewno nie są to bardzo skomplikowane zadania programistyczne. Przykład numer jeden jest zilustrowany na listingu 6.8. Listing 6.8. import java.io.*; public class Main { public static void main(String args[]) { StreamTokenizer strTok = new StreamTokenizer( new BufferedReader( new InputStreamReader(System.in) ) ); System.out.print("Wprowadź pierwszą liczbę: "); try{ while(strTok.nextToken() != StreamTokenizer.TT_NUMBER){ System.out.println("To nie jest poprawna liczba."); System.out.print("Wprowadź pierwszą liczbę: "); } } catch(IOException e){ System.out.print("Błąd podczas odczytu danych ze strumienia."); return; } double pierwszaLiczba = strTok.nval; System.out.print("Wprowadź drugą liczbę: "); try{ while(strTok.nextToken() != StreamTokenizer.TT_NUMBER){ System.out.println("To nie jest poprawna liczba."); System.out.print("Wprowadź drugą liczbę: "); } } catch(IOException e){ System.out.print("Błąd podczas odczytu danych ze strumienia."); return; } double drugaLiczba = strTok.nval; double wynik = pierwszaLiczba * drugaLiczba; 284 Java. Praktyczny kurs System.out.println(pierwszaLiczba + " * " + drugaLiczba + " = " + wynik); } } Tworzymy obiekt strTok klasy StreamTokenizer sposobem opisanym kilka akapitów wyżej, a następnie wyświetlamy prośbę o wprowadzenie pierwszej liczby. W pętli while wywołujemy metodę nextToken tak długo, aż wprowadzony z klawiatury ciąg znaków będzie reprezentował wartość liczbową; wartość ta zostaje przypisana zmiennej typu double o nazwie pierwszaLiczba. Następnie wykonujemy identyczne czynności umożliwiające wprowadzenie drugiej liczby. Jedyną różnicą są komunikaty wyświetlane na ekranie oraz przypisanie odczytanej liczby zmiennej drugaLiczba. Po odczytaniu obu wartości pozostaje wykonanie mnożenia i wyświetlenie jego wyniku na ekranie. Przykładowy efekt działania programu jest widoczny na rysunku 6.8. Rysunek 6.8. Mnożenie dwóch liczb Drugi przykład dotyczy obliczania pierwiastków równania kwadratowego. Jak wykonać samo obliczenie, wiadomo już z lekcji 7., trzeba zatem dodać możliwość wprowadzania współczynników równania z klawiatury. Kod realizujący to zadanie jest przedstawiony na listingu 6.9. Listing 6.9. import java.io.*; public class Main { public double pobierzLiczbe(StreamTokenizer strTok) throws IOException { while(strTok.nextToken() != StreamTokenizer.TT_NUMBER){ System.out.println("To nie jest poprawna liczba."); System.out.print("Wprowadź poprawną liczbę: "); } return strTok.nval; } public void oblicz(double A, double B, double C) { System.out.println ("Parametry równania:\n"); System.out.println ("A: " + A + " B: " + B + " C: " + C + "\n"); if (A == 0){ System.out.println ("To nie jest równanie kwadratowe: A = 0!"); } else{ double delta = B * B - 4 * A * C; if (delta < 0){ System.out.println ("Delta < 0"); Rozdział 6. System wejścia-wyjścia 285 System.out.println ("To równanie nie ma rozwiązania w " + "zbiorze liczb rzeczywistych."); } else if(delta == 0){ double wynik = -B / (2 * A); System.out.println ("Rozwiązanie: x = " + wynik); } else if(delta > 0){ double wynik; wynik = (-B + Math.sqrt(delta)) / (2 * A); System.out.print ("Rozwiązanie: x1 = " + wynik); wynik = (-B - Math.sqrt(delta)) / (2 * A); System.out.println (", x2 = " + wynik); } } } public void start() { StreamTokenizer strTok = new StreamTokenizer( new BufferedReader( new InputStreamReader(System.in) ) ); double paramA = 0; double paramB = 0; double paramC = 0; try{ System.out.print("Podaj parametr paramA = pobierzLiczbe(strTok); System.out.print("Podaj parametr paramB = pobierzLiczbe(strTok); System.out.print("Podaj parametr paramC = pobierzLiczbe(strTok); System.out.println(""); } catch(IOException e){ System.out.println("Błąd podczas return; } oblicz(paramA, paramB, paramC); A: "); B: "); C: "); odczytu strumienia."); } public static void main (String args[]) { Main main = new Main(); main.start(); } } Jak widać, program został dosyć mocno rozbudowany w stosunku do pierwowzoru. Instrukcje wykonujące właściwe obliczenia pierwiastków zostały przeniesione do metody oblicz. Sama metoda obliczeń oczywiście się nie zmieniła, różnica jest taka, że metoda oblicz nie ma zapisanych na stałe parametrów A, B i C, tylko otrzymuje je w postaci argumentów. 286 Java. Praktyczny kurs Wprowadzanie danych odbywa się za pomocą klasy StreamTokenizer, tak jak we wcześniej prezentowanych przykładach. Tym razem odczyt pojedynczej liczby następuje jednak w metodzie pobierzLiczbe, nie ma bowiem sensu pisać tego samego kodu dla wprowadzania wartości każdego z parametrów. Trzeba byłoby trzy razy napisać praktycznie identyczną pętlę while. W przykładzie z listingu 6.8, gdzie wprowadzane były tylko dwie wartości, można było zdecydować się na taką metodę, ale tym razem zdecydowanie lepszym pomysłem jest właśnie zastosowanie dodatkowej metody odczytującej dane wejściowe. Pozostała jeszcze metoda start. Jej zadaniem jest utworzenie obiektu klasy Stream Tokenizer, który zostanie wykorzystany w metodzie pobierzLiczbe, przypisanie wartości zmiennym paramA, paramB i paramC oraz wywołanie metody oblicz. Oczywiście wymienione zmienne odzwierciedlają kolejne parametry równania kwadratowego. Wykonywanie kodu rozpocznie się tradycyjnie od metody main, w której tworzymy nowy obiekt klasy Main i wywołujemy metodę start. O tym, że program rzeczywiście rozwiązuje równania kwadratowe, powinien przekonać rysunek 6.9, który prezentuje rozwiązanie równania o parametrach A = 2, B = -2 i C = -4, czyli równania o postaci: 2x2 + 2x – 4 = 0. Rysunek 6.9. Efekt działania programu rozwiązującego równania kwadratowe Klasa Scanner W Java 5 pojawiła się nowa klasa, o nazwie Scanner, upraszczająca wiele operacji wczytywania i przetwarzania danych. Zawiera ona konstruktory, które mogą przyjmować obiekty klas: File, InputReader i String, a także obiekty implementujące interfejsy Readable lub ReadableByteChannel. Jest to więc zestaw pozwalający na obsługę bardzo wielu formatów wejściowych. Metod klasy Scanner jest bardzo wiele, nie ma potrzeby, aby je wszystkie omawiać. Warto jednak wiedzieć, że najbardziej dla nas interesujące rodziny next i hasNext są konstruowane na bardzo prostej zasadzie, którą schematycznie można przedstawić jako: hasNextNazwaTypuProstego i nextNazwaTypuProstego W pierwszej grupie istnieją więc metody hasNextInt, hasNextDouble, hasNextByte itd. Wszystkie one zwracają wartość true, o ile w powiązanym strumieniu danych kolejną jednostką leksykalną jest wartość danego typu prostego. Istnieje też metoda hasNext, która zwraca wartość true, jeżeli w strumieniu istnieje jakakolwiek kolejna jednostka Rozdział 6. System wejścia-wyjścia 287 leksykalna. Dodatkowo dostępna jest metoda hasNextLine, która określa, czy w strumieniu znajduje się linia tekstu. Metody z rodziny next, a więc nextInt, nextDouble, nextByte itd., zwracają natomiast kolejną jednostkę leksykalną w postaci wartości danego typu prostego. Metoda next zwraca zaś token w postaci ciągu znaków. Podobnie jak w przypadku opisanym w poprzednim akapicie, istnieje również metoda nextLine, która zwraca całą linię tekstu. Zobaczmy, jak wykorzystać to w praktyce: napiszmy program analogiczny do prezentowanego w poprzedniej części lekcji, ale korzystający z klasy Scanner. Będzie wczytywał ze standardowego strumienia wejściowego wartość całkowitą i wyświetli rezultat jej mnożenia przez 2. Kod realizujący takie zadanie został przedstawiony na listingu 6.10. Listing 6.10. import java.util.*; public class Main { public static void main(String args[]) { Scanner scanner = new Scanner(System.in); System.out.print("Wprowadź wartość całkowitą: "); while(!scanner.hasNextInt()){ System.out.print("To nie jest wartość całkowita: "); System.out.println(scanner.next()); System.out.print("Wprowadź wartość całkowitą: "); } int value = scanner.nextInt(); int result = value * 2; System.out.println(value + " * 2 = " + result); } } Na początku tworzymy nowy obiekt klasy Scanner i przypisujemy go zmiennej scanner. Ponieważ argumentem konstruktora może być obiekt klasy InputStream, wykorzystujemy bezpośrednio obiekt System.in: Scanner scanner = new Scanner(System.in); Następnie wyświetlamy prośbę o wprowadzenie wartości całkowitej i przechodzimy do pętli while: while(!scanner.hasNextInt()){ Jak widać, pętla zostanie zakończona, kiedy metoda hasNextInt obiektu zwróci wartość true, a więc wtedy, kiedy w strumieniu wejściowym pojawi się wartość, która będzie mogła być zinterpretowana jako całkowita. Oczywiście w sytuacji, gdy hasNextInt zwróci wartość false, wykonywane będzie wnętrze pętli, w którym wyświetlana jest informacja o tym, że wprowadzono niepoprawną wartość. Wartość ta będzie pobierana ze strumienia za pomocą metody next i w celach informacyjnych wyświetlana na ekranie: System.out.println(scanner.next()); 288 Java. Praktyczny kurs Z kolei kiedy w strumieniu pojawi się wartość typu int, pętla zostanie zakończona, a wczytana wartość zostanie pobrana za pomocą wywołania metody nextInt i zapisana w zmiennej value: int value = scanner.nextInt(); Następnie jest wykonywane mnożenie przez 2, a wynik tej operacji zostaje wyświetlony na ekranie. Przykładowy efekt działania programu widać na rysunku 6.10. Rysunek 6.10. Wczytywanie wartości za pomocą klasy Scanner Klasa Console Począwszy od Java 6 (1.6), w JDK dostępna jest klasa Console wyspecjalizowana w obsłudze konsoli (zawarta jest w pakiecie java.io). Dzięki niej możliwe jest pobranie odwołania do konsoli, na której operuje Java (o ile taka jest udostępniona; nie musi to być też konsola systemowa), i pobieranie oraz wyświetlanie danych. Jest to klasa finalna i dziedziczy bezpośrednio po Object. Aby uzyskać obiekt typu Console będący odwołaniem do bieżącej konsoli systemowej, nie wywołuje się bezpośrednio konstruktora, ale metodę statyczną console z klasy System. To wywołanie ma postać: System.console() W efekcie zostanie uzyskany obiekt konsoli, za którego pośrednictwem będzie można wykonywać różne operacje, lub też wartość null, o ile konsola nie jest dostępna. Wspomniane operacje mogą być realizowane za pomocą metod przedstawionych w tabeli 6.3. Prosty program pozwalający na wczytanie wiersza tekstu, a następnie wyświetlający uzyskane dane na ekranie został przedstawiony na listingu 6.11. Listing 6.11. import java.io.Console; public class Main { public static void main (String args[]) { Console con = System.console(); if(con == null){ System.out.println("Brak konsoli!"); } else{ String line = con.readLine("Wprowadź tekst: "); con.printf("Wprowadzony tekst: " + line); } } } Rozdział 6. System wejścia-wyjścia 289 Tabela 6.3. Metody zdefiniowane w klasie Console Deklaracja Opis void flush() Opróżnia bufor i wyświetla wszystkie znajdujące się w nim dane. Console format(String fmt, Object… args) Wyświetla dane sformatowane za pomocą ciągu formatującego fmt. Console printf(String format, Object… args) Wyświetla dane sformatowane za pomocą ciągu formatującego format. Reader reader() Zwraca powiązany z konsolą obiekt typu Reader (używany do odczytu danych). String readLine() Odczytuje wiersz tekstu. String readLine(String fmt, Object… args) Wyświetla tekst zachęty (prompt) sformatowany za pomocą ciągu fmt, a następnie wczytuje wiersz tekstu. char[] readPassword() Odczytuje wiersz tekstu (hasło), nie wyświetlając wprowadzanych z klawiatury znaków. char[] readPassword (String fmt, Object… args) Wyświetla tekst zachęty (prompt) sformatowany za pomocą ciągu fmt, a następnie wczytuje wiersz tekstu (hasło), nie wyświetlając wprowadzanych z klawiatury znaków. PrintWriter writer() Zwraca powiązany z konsolą obiekt typu PrintWriter (używany do zapisu danych). Ponieważ klasa Console znajduje się w pakiecie java.io, kod rozpoczyna się odpowiednią instrukcją import. Odwołanie do konsoli pobierane jest w sposób opisany powyżej i zapisywane w zmiennej con. Następnie badane jest, czy wartość con jest równa null (byłoby tak, gdyby aplikacja nie miała dostępu do konsoli). Jeśli jest równa null, za pomocą instrukcji System.out.println następuje próba podania komunikatu o błędzie i jest to koniec działania aplikacji. W przeciwnym razie (blok else) wyświetlany jest tekst z prośbą o wprowadzenie wiersza tekstu i wiersz ten jest wczytywany (wywołanie metody readLine). Po odczytaniu danych są one ponownie wyświetlane za pomocą metody printf. Program z listingu 6.11 można w łatwy sposób przerobić, tak aby odczytywał dane w pętli while aż do osiągnięcia pewnego ciągu znaków, np. quit. To jednak pozostanie ćwicze- niem do samodzielnego wykonania. W tej części lekcji przyjrzymy się jeszcze użytej na listingu metodzie printf. Pozwala ona na wyświetlanie danych w postaci sformatowanej według pewnego wzorca. Wystarczy w ciągu znaków będącym pierwszym argumentem umieścić znaczniki, które zostaną zamienione na wartości pobrane z kolejnych argumentów. Schematycznie wywołanie metody printf będzie miało zatem postać (przy założeniu, że obiekt con reprezentuje konsolę): con.printf("format", wartość1, wartość2, ..., wartośćN); O tym, że mamy do czynienia ze znacznikiem, informuje znak %. Możliwe do zastosowania znaczniki zostały przedstawione w tabeli 6.49. 9 W rzeczywistości ciąg formatujący może mieć dużo bardziej złożoną postać i określać np. precyzję, z jaką ma być wyświetlona wartość. Dokładne dane można znaleźć w dokumentacji technicznej JDK. 290 Java. Praktyczny kurs Tabela 6.4. Znaczniki formatujące dla metody printf Ciąg Znaczenie b lub B Argument będzie traktowany jak wartość boolowska. h lub H Ciąg będący wynikiem wykonania funkcji skrótu (hash code) argumentu. s lub S Argument będzie traktowany jak ciąg znaków. c lub C Argument będzie traktowany jak znak. d Argument będzie traktowany jak wartość całkowita dziesiętna. o Argument będzie traktowany jak wartość całkowita ósemkowa. x lub X Argument będzie traktowany jak wartość całkowita szesnastkowa. e lub E Argument będzie traktowany jak wartość rzeczywista w notacji naukowej. f Argument będzie traktowany jak wartość rzeczywista dziesiętna. g lub G Argument będzie traktowany jak wartość rzeczywista dziesiętna w notacji zwykłej lub naukowej w zależności od zastosowanej precyzji. a lub A Argument będzie traktowany jak wartość rzeczywista szesnastkowa. t lub T Argument będzie traktowany jak data i (lub) czas. Wymaga określenia dodatkowego formatowania. % Znak %. n Znak nowego wiersza. Jeżeli zatem w kodzie zostaną zdefiniowane przykładowe zmienne: boolean zmienna1 = true; int zmienna2 = 254; to ich wartości można wpleść w ciąg znaków, który zostanie wyświetlony na ekranie w następujący sposób: con.printf("zmienna1 = %b, zmienna2 = %d", zmienna1, zmienna2); Wtedy zamiast ciągu %b zostanie podstawiona wartość zmiennej zmienna1, a pod %d — wartość zmiennej zmienna2 (oczywiście zmienna con musi zawierać odwołanie do konsoli). Na listingu 6.12 został przedstawiony program wyświetlający jedną wartość (50) na kilka różnych sposobów: jako znak, jako wartość dziesiętną, ósemkową i szesnastkową. Listing 6.12. import java.io.Console; public class Main { public static void main (String args[]) { Console con = System.console(); if(con == null){ System.out.println("Brak konsoli!"); } else{ int liczba = 50; con.printf("Znak: %c%n", liczba); Rozdział 6. System wejścia-wyjścia 291 con.printf("Dziesiętnie: %d%n", liczba); con.printf("Ósemkowo: %o%n", liczba); con.printf("Szesnastkowo: %x%n", liczba); } } } Klasa PrintStream Obiekt out z klasy System, reprezentujący standardowy strumień wyjściowy, jest obiektem typu PrintStream. Używany był wielokrotnie przy wyprowadzaniu danych na ekran za pomocą metod println i print. Jak już wiemy, metody te występują w wielu przeciążonych wersjach i umożliwiają wyświetlanie różnorodnych typów danych. Wszystkie dostępne metody klasy PrintStream zostały zebrane w tabeli 6.5. Tabela 6.5. Metody klasy PrintStream Deklaracja Opis PrintStream append(char c) Dodaje znak do strumienia. PrintStream append(CharSequence csq) Dodaje do strumienia sekwencję znaków. PrintStream append(CharSequence csq, int start, int end) Dodaje do strumienia część sekwencji znaków wyznaczaną przez indeksy start i end. boolean checkError() Opróżnia bufor oraz sprawdza, czy nie wystąpił błąd. protected void clearError() Zeruje status błędu. void close() Zamyka strumień. void flush() Powoduje opróżnienie bufora. PrintStream format(Locale l, String format, Object… args) Zapisuje w strumieniu dane określone przez argumenty args w formacie zdefiniowanym przez format, zgodnie z ustawieniami narodowymi wskazanymi przez locale. PrintStream format(String format, Object… args) Zapisuje w strumieniu dane określone przez argumenty args w formacie określonym przez format. void print(boolean b) Wyświetla wartość typu boolean. void print(char c) Wyświetla znak. void print(char[] s) Wyświetla tablicę znaków. void print(double d) Wyświetla wartość typu double. void print(float f) Wyświetla wartość typu float. void print(int i) Wyświetla wartość typu int. void print(long l) Wyświetla wartość typu long. void print(Object obj) Wyświetla ciąg znaków uzyskany przez wywołanie metody toString obiektu obj. void print(String s) Wyświetla ciąg znaków s. 292 Java. Praktyczny kurs Tabela 6.5. Metody klasy PrintStream (ciąg dalszy) Deklaracja Opis PrintStream printf(Locale l, String format, Object… args) Zapisuje w strumieniu dane określone przez argumenty args w formacie zdefiniowanym przez format, zgodnie z ustawieniami narodowymi wskazanymi przez locale. PrintStream printf(String format, Object… args) Zapisuje w strumieniu dane określone przez argumenty args w formacie zdefiniowanym przez format. void println() Wyświetla znak końca linii (powoduje przejście do nowej linii). void println(boolean b) Wyświetla wartość typu boolean oraz znak końca linii. void println(char c) Wyświetla znak zapisany w c oraz znak końca linii. void println(char[] s) Wyświetla tablicę znaków oraz znak końca linii. void println(double d) Wyświetla wartość typu double oraz znak końca linii. void println(float f) Wyświetla wartość typu float oraz znak końca linii. void println(int i) Wyświetla wartość typu int oraz znak końca linii. void println(long l) Wyświetla wartość typu long oraz znak końca linii. void println(Object obj) Wyświetla ciąg znaków uzyskany przez wywołanie metody toString obiektu obj oraz znak końca linii. void println(String s) Wyświetla ciąg znaków s oraz znak końca linii. protected void setError() Ustawia strumień w stan błędu. void write(byte[] buf, int off, int len) Zapisuje do strumienia liczbę bajtów wskazywaną przez len, poczynając od miejsca tablicy buf wskazywanego przez off. void write(int b) Zapisuje bajt b do strumienia. Polskie znaki w systemie Windows Powróćmy teraz do problemu wyświetlania polskich znaków na konsoli w środowisku Windows. Wiemy już z rozdziału 2., z lekcji 5., oraz z własnego doświadczenia, że w niektórych wersjach systemów (np. w Windows XP i Java 7) polskie znaki nie będą wyświetlane poprawnie, jeśli nie wykona się dodatkowych czynności, takich jak zmiana systemu kodowania znaków na konsoli oraz stosowanej czcionki10. Sposób ten został opisany dokładnie we wspomnianej lekcji. Warto jednak znać rozwiązanie ogólniejszego problemu — jak spowodować, aby program w Javie wyświetlał na konsoli (standardowym wyjściu) znaki w określonym standardzie kodowania? Najpierw trzeba sobie przypomnieć, skąd bierze się problem wyświetlania polskich znaków. Otóż typowa sytuacja jest następująca: w systemie Windows w napisanym przez nas kodzie źródłowym polskie znaki są kodowane w standardzie CP1250 (Windows 1250)11, Java natomiast przechowuje je w standardzie Unicode i tak też są one 10 W niektórych wersjach JDK 1.5 znaki na konsoli były standardowo wyświetlane zgodnie z kodowaniem CP852, nie było więc potrzeby wykonywania opisywanych czynności. 11 Chyba że został użyty edytor pozwalający na zapis w innym kodowaniu, np. UTF-8, i to ono zostało wybrane jako standard zapisu. Rozdział 6. System wejścia-wyjścia 293 reprezentowane w b-kodzie, z kolei na konsoli są wyświetlane w standardzie CP85212. Z przekodowaniem znaków z CP1250 na standard Unicode na szczęście kompilator (środowisko JDK) radzi sobie sam (można to regulować za pomocą opcji encoding kompilatora javac). Cały problem sprowadza się więc do wyświetlenia ich przy użyciu wybranej strony kodowej — w tym przypadku 852. Pomogą nam w tym klasy o nazwach OutputStreamWriter oraz PrintWriter. Klasa OutputStreamWriter oferuje dwa konstruktory: OutputStreamWriter(OutputStream out) OutputStreamWriter(OutputStream out, String enc) Pierwszy z nich używa domyślnej strony kodowej, nie będzie więc w tym przypadku przydatny. Drugi13 przyjmuje argument, który określa stronę kodową, w jakiej mają być kodowane znaki w strumieniu. Co w związku z tym należy zrobić? Utworzyć obiekt klasy OutputStreamWriter (powiązany ze standardowym strumieniem wyjściowym) korzystający ze strony kodowej 852. W tym celu należy zastosować konstrukcję: new OutputStreamWriter(System.out, "Cp852"); Niezbędne jest w tym miejscu również przechwycenie wyjątku UnsupportedEncoding Exception, który pojawia się, kiedy nie jest możliwe zastosowanie wskazanej strony kodowej. To jednak nie wszystko, klasa OutputStreamWriter oferuje bowiem jedynie trzy metody write, które pozwalają na wysłanie do strumienia znaku, tablicy znaków oraz łańcucha znaków w postaci obiektu klasy String. Nie jest więc odpowiednikiem klasy PrintStream. Na szczęście używając obiektu klasy OutputStreamWriter, można zbudować obiekt klasy PrintWriter, który zawiera wszystkie potrzebne metody (metody print oraz println z tabeli 6.5). Spójrzmy na listing 6.13, został na nim zobrazowany sposób utworzenia i wykorzystania obiektu klasy PrintWriter wyświetlającego znaki przy użyciu strony kodowej 852. Listing 6.13. import java.io.*; public class Main { public static void main (String args[]) { PrintWriter outp = null; try{ outp = new PrintWriter( new OutputStreamWriter(System.out, "Cp852"), true ); } catch(UnsupportedEncodingException e){ System.out.println("Nie można ustawić strony kodowej Cp852."); outp = new PrintWriter(new OutputStreamWriter(System.out), true); } System.out.println("Test polskich znaków, obiekt System.out:"); System.out.println("Małe litery: ąćęłńóśźż"); System.out.println("Duże litery: ĄĆĘŁŃÓŚŹŻ"); 12 W polskiej wersji systemu Windows. 13 Dostępny, począwszy od JDK 1.4. 294 Java. Praktyczny kurs System.out.println(); outp.println("Test polskich znaków, obiekt outp:"); outp.println("Małe litery: ąćęłńóśźż"); outp.println("Duże litery: ĄĆĘŁŃÓŚŹŻ"); } } Podstawową operacją jest utworzenie obiektu outp klasy PrintWriter. Używamy go zamiast tradycyjnego System.out. Deklarujemy zatem najpierw zmienną outp i przypisujemy jej wartość null, a następnie w bloku try tworzymy sam obiekt. Pierwszym parametrem konstruktora klasy PrintWriter jest nowo utworzony obiekt klasy OutputStreamWriter powiązany ze standardowym strumieniem wyjściowym reprezentowanym przez System.out i wykorzystujący stronę kodową 852. Należy zwrócić uwagę na drugi parametr równy true, który będzie wymuszać wypchnięcie danych do strumienia po wykonaniu każdej instrukcji print lub println. Niezastosowanie go będzie skutkowało przetrzymywaniem danych w buforze i nie będą się one prawidłowo pojawiały na ekranie14. Warto zwrócić również uwagę na to, co się dzieje, kiedy wystąpi wyjątek Unsupported EncodingException. Otóż w takiej sytuacji nie można pozwolić na to, aby zmienna outp nie została zainicjowana, gdyż doprowadziłoby to do błędów w dalszej części programu (niemożność wykorzystania obiektu outp). W związku z tym ponownie konstruujemy obiekt klasy PrintWriter, tym razem nie wymuszając zastosowania strony kodowej 852. Kiedy utworzymy już obiekt outp, można używać go zamiast standardowego System.out15, tak jak jest to widoczne w kolejnych instrukcjach, w których dokonujemy porównania obu sposobów wyświetlania polskich znaków. Efekt działania programu jest widoczny na rysunku 6.11. Rysunek 6.11. Efekt użycia klas PrintWriter i OutputStreamWriter do wyświetlenia polskich znaków 14 W praktyce może się jednak okazać konieczne wywoływanie metody flush po każdej instrukcji print lub println (patrz również rozwiązanie ćwiczenia 32.4). 15 Oczywiście outp korzysta ze strumienia System.out, wcześniej jednak dokonuje przekodowania znaków zgodnego z wybraną stroną kodową. Rozdział 6. System wejścia-wyjścia 295 Ćwiczenia do samodzielnego wykonania Ćwiczenie 32.1. Zmodyfikuj program z listingu 6.7 tak, aby poprawnie reagował (tzn. nie wyświetlał kolejnej prośby o wprowadzenie liczby) na zakończenie pracy aplikacji przez naciśnięcie klawiszy Ctrl+C (jest to sytuacja równoznaczna z osiągnięciem końca strumienia). Ćwiczenie 32.2. Napisz program, który będzie wczytywał ze standardowego wejścia tekst. Po wprowadzeniu każdej linii program powinien wyświetlić oddzielone od siebie słowa, z których się ona składa. Koniec pracy ma nastąpić, kiedy w tekście pojawi się słowo quit lub zostanie wciśnięta kombinacja klawiszy Ctrl+C. Ćwiczenie 32.3. Napisz program umożliwiający użytkownikowi wykonywanie na dwóch liczbach czterech podstawowych działań arytmetycznych: dodawania, odejmowania, mnożenia i dzielenia. Ćwiczenie 32.4. Napisz program, który za pomocą klasy Console będzie wczytywał wiersze tekstu i od razu wyświetlał wczytywane dane na ekranie wraz z numerem wprowadzonego wiersza. Odczyt danych ma się zakończyć po wprowadzeniu ciągu quit. Ćwiczenie 32.5. Zmodyfikuj program z listingu 6.9 w taki sposób, aby wyświetlał znaki na ekranie przy użyciu strony kodowej 852. Lekcja 33. System plików Lekcja 33. jest poświęcona technikom pozwalającym operować na systemie plików. Zawarte są w niej informacje na temat tego, jak tworzyć i usuwać pliki oraz katalogi. Zaprezentowana zostanie bliżej klasa File i metody przez nią udostępniane, jak również to, jak pobrać zawartość katalogu oraz jak usunąć katalog wraz z całą jego zawartością, co będzie wymagać zastosowania techniki rekurencyjnej. Po przedstawieniu tych tematów będzie można omówić metody zapisu i odczytu plików, tym jednak zajmiemy się dopiero w kolejnej lekcji. Klasa File Klasa File pozwala na wykonywanie podstawowych operacji na plikach i katalogach, takich jak ich tworzenie i usuwanie, operacje na nazwach czy pobieranie parametrów, np. czasu utworzenia bądź modyfikacji plików lub katalogów. Nie jest to jednak klasa, która umożliwiałaby modyfikację zawartości pliku. Metody przez nią udostępniane zostały zebrane w tabeli 6.6. Będziemy je wykorzystywać w dalszej części lekcji. 296 Java. Praktyczny kurs Tabela 6.6. Metody udostępniane przez klasę File Deklaracja metody Opis Wersja JDK boolean canExecute() Zwraca true, jeśli aplikacja może uruchomić dany plik. 1.6 boolean canRead() Zwraca true, jeśli aplikacja może odczytywać dany plik. 1.0 boolean canWrite() Zwraca true, jeśli aplikacja może zapisywać dany plik. 1.0 int compareTo(File pathname) Porównuje ścieżki dostępu do plików. 1.2 boolean createNewFile() Jeśli nie istnieje plik określony przez bieżący obiekt File, tworzy go. 1.2 static File create TempFile(String prefix, String suffix) Tworzy pusty plik tymczasowy w systemowym katalogu przeznaczonym dla plików tymczasowych. Nazwa powstaje przy wykorzystaniu ciągów przekazanych w parametrach prefix i suffix. 1.2 static File createTempFile(String prefix, String suffix, File directory) Podobnie jak dwie poprzednie metody, tworzy plik tymczasowy, zostaje on jednak umieszczony w katalogu wskazywanym przez argument directory. 1.2 boolean delete() Usuwa plik lub katalog (jeśli polecenie dotyczy katalogu, musi on być pusty). 1.0 void deleteOnExit() Zaznacza, że plik ma zostać usunięty, kiedy maszyna wirtualna będzie kończyć pracę. 1.2 boolean equals(Object obj) Zwraca true, jeśli obiekt obj reprezentuje taką samą ścieżkę dostępu. 1.0 boolean exists() Zwraca true, jeśli plik lub katalog istnieje. 1.0 File getAbsoluteFile() Zwraca obiekt zawierający bezwzględną nazwę pliku lub katalogu (wraz z pełną ścieżką dostępu). 1.2 String getAbsolutePath() Zwraca bezwzględną ścieżkę dostępu do pliku lub katalogu (bez nazwy pliku). 1.0 File getCanonicalFile() Zwraca obiekt zawierający kanoniczną postać nazwy pliku lub katalogu (wraz z pełną ścieżką dostępu). 1.2 String getCanonicalPath() Zwraca kanoniczną postać ścieżki dostępu do pliku lub katalogu. 1.1 long getFreeSpace() Zwraca ilość wolnego miejsca na partycji, na której znajduje się plik lub katalog. 1.6 String getName() Zwraca nazwę pliku (bez ścieżki dostępu). 1.0 String getParent() Zwraca nazwę katalogu nadrzędnego. 1.0 File getParentFile() Zwraca obiekt typu File wskazujący na katalog nadrzędny. 1.2 String getPath() Zwraca nazwę bieżącego katalogu lub pliku w postaci obiektu typu String. 1.0 long getTotalSpace() Zwraca całkowitą ilość miejsca na partycji, na której znajduje się plik lub katalog. 1.6 long getUsableSpace() Zwraca dostępną dla danej maszyny wirtualnej ilość miejsca na partycji, na której znajduje się plik lub katalog. 1.6 int hashCode() Oblicza wartość funkcji skrótu dla danej ścieżki dostępu. 1.0 Rozdział 6. System wejścia-wyjścia 297 Tabela 6.6. Metody udostępniane przez klasę File (ciąg dalszy) Deklaracja metody Opis Wersja JDK boolean isAbsolute() Zwraca true, jeśli dana ścieżka dostępu jest ścieżką bezwzględną. 1.0 boolean isDirectory() Zwraca true, jeśli ścieżka dostępu wskazuje na katalog. 1.0 boolean isFile() Zwraca true, jeśli ścieżka dostępu wskazuje na plik. 1.0 boolean isHidden() Zwraca true, jeśli ścieżka dostępu wskazuje na ukryty katalog lub plik. 1.2 long lastModified() Zwraca czas ostatniej modyfikacji pliku lub katalogu. 1.0 long length() Zwraca wielkość pliku w bajtach. 1.0 String[] list() Zwraca zawartość katalogu w postaci tablicy obiektów typu String. 1.0 String[] list (FilenameFilter filter) Zwraca listę plików i podkatalogów spełniających kryteria wskazane przez argument filter. 1.0 File[] listFiles() Zwraca zawartość katalogu w postaci tablicy obiektów typu File. 1.2 File[] listFiles (FileFilter filter) Zwraca w postaci tablicy obiektów typu File zawartość katalogu spełniającą kryteria wskazane przez filter. 1.2 File[] listFiles (FilenameFilter filter) Zwraca w postaci tablicy obiektów typu File zawartość katalogu spełniającą kryteria wskazane przez filter. 1.2 static File[] listRoots() Wyświetla wszystkie „korzenie” (węzły główne) systemu plików. 1.2 boolean mkdir() Tworzy nowy katalog. 1.0 boolean mkdirs() Tworzy nowy katalog z uwzględnieniem nieistniejących katalogów nadrzędnych. 1.0 boolean renameTo (File dest) Zmienia nazwę pliku lub katalogu na wskazywaną przez argument dest. 1.0 boolean setExecutable (boolean executable) Ustawia prawo (atrybut) wykonywalności (executable) katalogu lub pliku (dla właściciela obiektu). 1.6 boolean setExecutable (boolean executable, boolean ownerOnly) Ustawia prawo (atrybut) wykonywalności (executable) pliku lub katalogu dla właściciela lub wszystkich użytkowników. 1.6 boolean setLastModified (long time) Ustawia datę ostatniej modyfikacji pliku lub katalogu. 1.2 boolean setReadable (boolean readable) Ustawia prawo (atrybut) do odczytu (readable) katalogu lub pliku (dla właściciela obiektu). 1.6 boolean setReadable (boolean readable, boolean ownerOnly) Ustawia prawo (atrybut) do odczytu (readable) pliku lub katalogu dla właściciela lub wszystkich użytkowników. 1.6 boolean setReadOnly() Ustawia atrybut ReadOnly pliku lub katalogu. 1.2 boolean setWriteable (boolean readable) Ustawia prawo (atrybut) do odczytu (writeable) katalogu lub pliku (dla właściciela obiektu). 1.6 298 Java. Praktyczny kurs Tabela 6.6. Metody udostępniane przez klasę File (ciąg dalszy) Deklaracja metody Opis Wersja JDK boolean setWriteable (boolean readable, boolean ownerOnly) Ustawia prawo (atrybut) do zapisu (writeable) do pliku lub katalogu dla właściciela lub wszystkich użytkowników. 1.6 String toString() Zwraca ścieżkę dostępu w postaci obiektu klasy String. 1.0 URI toURI() Konwertuje ścieżkę dostępu reprezentowaną przez obiekt File na zunifikowany identyfikator zasobów — obiekt typu URI. 1.4 URL toURL() Konwertuje ścieżkę dostępu reprezentowaną przez obiekt File na obiekt klasy URL. Metoda przestarzała, o ile to możliwe, nie należy z niej korzystać. 1.2 Pobranie zawartości katalogu W celu poznania zawartości danego katalogu można skorzystać z metody list klasy File. Zwraca ona tablicę obiektów typu String zawierającą nazwy plików i katalogów, zatem napisanie programu, którego zadaniem będzie wyświetlenie zawartości jakiegoś katalogu, dla nikogo nie powinno stanowić problemu. Taki przykładowy kod obrazujący wykorzystanie metody list jest widoczny na listingu 6.14. Listing 6.14. import java.io.*; public class Main { public static void main (String args[]) { File file = new File("."); String[] dirList = file.list(); for(int i = 0; i < dirList.length; i++){ System.out.println(dirList[i]); } } } Konstruujemy obiekt file klasy File, podając w konstruktorze ścieżkę dostępu do katalogu, którego zawartość chcemy wylistować — jest to katalog bieżący oznaczony jako ., można też użyć zapisu ./. Format, w jakim podaje się nazwę ścieżki dostępu, jest zależny od konwencji stosowanej w danym systemie operacyjnym, klasa File sama radzi sobie z rozkodowaniem takiego zapisu. Następnie deklarujemy zmienną dirList i przypisujemy jej obiekt zwrócony przez wywołanie metody list. Ponieważ ten obiekt to w rzeczywistości tablica obiektów klasy String, pozostaje zastosowanie pętli for do odczytania jego zawartości i wyświetlenia jej na ekranie. Przykładowy efekt wykonania kodu z listingu 6.14 jest widoczny na rysunku 6.12. Proste wyświetlenie zawartości katalogu z pewnością nie było zbyt skomplikowanym zadaniem, zauważmy jednak, że klasa File udostępnia również drugą, przeciążoną metodę list, która daje większe możliwości. Pozwala ona bowiem na pobranie nazw tylko tych plików i katalogów, które pasują do określonego wzorca. Przyjmuje parametr typu FilenameFilter określający, które nazwy należy zaakceptować, a które odrzucić. Rozdział 6. System wejścia-wyjścia 299 Rysunek 6.12. Wyświetlenie zawartości katalogu przy użyciu metody list klasy File W rzeczywistości FilenameFilter to interfejs, w którym zadeklarowano tylko jedną metodę: accept. Aby zobaczyć, jak jego zastosowanie wygląda w praktyce, napiszmy program, który będzie wyświetlał zawartość dowolnego katalogu pasującą do wzorca określonego za pomocą wyrażenia regularnego16. Nazwa katalogu oraz wzorzec będą wczytywane z wiersza poleceń podczas uruchamiania aplikacji. Spójrzmy na kod przedstawiony na listingu 6.15. Listing 6.15. import java.io.*; import java.util.regex.*; public class Main { public static void main (String args[]) { if(args.length < 1){ System.out.println("Wywołanie programu: Main katalog maska"); return; } File file = new File(args[0]); String[] dirList; if(args.length < 2){ dirList = file.list(); } else{ try{ dirList = file.list((FilenameFilter) new MyFilenameFilter(args[1])); } catch(PatternSyntaxException e){ System.out.println("Nieprawidłowe wyrażenie regularne: " + args[1]); return; } } for(int i = 0; i < dirList.length; i++){ System.out.println(dirList[i]); } } } class MyFilenameFilter implements FilenameFilter { Pattern pattern; public MyFilenameFilter(String mask) { pattern = Pattern.compile(mask); } 16 Omówienie tematu wyrażeń regularnych wykracza poza ramy niniejszej książki. Osoby nieznające tej tematyki powinny zapoznać się z publikacjami ogólnodostępnymi w internecie. 300 Java. Praktyczny kurs public boolean accept(File dir, String name) { Matcher matcher = pattern.matcher(name); return matcher.matches(); } } Początkowo może wydawać się to trochę skomplikowane, w szczególności zastosowanie dodatkowej klasy MyFilenameFilter17, przeanalizujmy jednak wszystko po kolei, począwszy od kodu klasy Main. Zaczynamy od sprawdzenia, czy podczas wywołania został podany przynajmniej jeden argument (por. lekcja 14.). Jeśli nie, należy poinformować o tym użytkownika, wyświetlając informację na ekranie, i zakończyć działanie aplikacji. Jeśli jednak został podany przynajmniej jeden argument, zakładamy, że zawiera on nazwę katalogu, którego zawartość ma zostać wyświetlona, wykorzystujemy go zatem jako argument konstruktora klasy File: File file = new File(args[0]); Przygotowujemy również zmienną dirList, której zostanie przypisana wynikowa tablica obiektów klasy String. Sprawdzamy następnie, czy aplikacji został przekazany drugi argument wiersza poleceń — jeśli go nie ma (args.length < 2), wywołujemy znaną nam już bezargumentową wersję metody list i przypisujemy wynik jej wykonania zmiennej dirList. Jeżeli jednak drugi argument wiersza poleceń istnieje, to oznacza, że jest to wzorzec, z którym będą porównywane pliki, wykorzystujemy go więc jako argument konstruktora klasy MyFilenameFilter. Zapis: dirList = file.list( (FilenameFilter) new MyFileNameFilter(args[1]) ); powoduje wykonanie następujących operacji: utworzenie nowego obiektu klasy MyFile nameFilter, rzutowanie tego obiektu na typ interfejsowy FilenameFilter (to rzutowanie formalnie nie jest konieczne; jak wiadomo z wcześniejszych lekcji, w miejscu, gdzie oczekiwany jest obiekt danej klasy, można użyć obiektu klasy pochodnej), przekazanie go metodzie list oraz przypisanie obiektu zwróconego przez list zmiennej dirList. Instrukcja ta została ujęta w blok try, gdyż jeśli drugi argument wywołania nie będzie prawidłowym wyrażeniem regularnym, zostanie zgłoszony wyjątek Pattern SyntaxException. Jeśli jednak wyrażenie jest poprawne, zmiennej dirList zostanie przypisana tablica zawierająca listę plików z danego katalogu, których nazwa jest zgodna z wzorcem przekazanym aplikacji. Niezależnie zatem od tego, czy wzorzec został przekazany, czy nie, zmienna dirList będzie zawierała listę plików i katalogów znajdujących się we wskazanym katalogu. Ostatecznie wyświetlamy ją na ekranie, wykorzystując klasyczną pętlę for. Przyjrzyjmy się teraz konstrukcji klasy MyFilenameFilter. Po co się ją tworzy? Otóż użyta metoda list wymaga argumentu będącego obiektem klasy implementującej interfejs 17 Jest to klasa pakietowa, zatem cały listing należy zapisać w jednym pliku — Main.java. Rozdział 6. System wejścia-wyjścia 301 FilenameFilter. Skoro bowiem typem argumentu jest FilenameFilter, a Filename Filter to klasa interfejsowa, której obiektów nie można tworzyć, typem argumentu musi być klasa implementująca ten interfejs (lekcje 26. i 27.)18. Dlatego też została napisana klasa MyFilenameFilter implementująca interfejs FilenameFilter. Implementacja tego interfejsu jest równoznaczna z koniecznością zdefiniowania metody o nazwie accept przyjmującej dwa argumenty: pierwszy z nich to obiekt typu File, drugi to sama nazwa pliku (lub katalogu). Jak to będzie działać? Otóż metoda list dla każdego znalezionego pliku będzie wywoływała metodę accept obiektu przekazanego jej (metodzie list) jako argument. W naszym przypadku będzie to metoda accept z klasy MyFilenameFilter. Metoda ta powinna zwrócić wartość true, o ile dany plik lub katalog ma być uwzględniony w listingu, lub false w przeciwnym przypadku. Trzeba zatem tak skonstruować tę metodę, aby sprawdzała, czy nazwa pliku pasuje do wyrażenia regularnego. Pomogą nam w tym klasy z pakietu java.util.regex, stworzone specjalnie do operowania na wyrażeniach regularnych. Klasa Pattern reprezentuje wyrażenie regularne, w klasie MyFilenameFilter umieściliśmy zatem pole tego typu. Obiekt klasy Pattern jest tworzony w konstruktorze poprzez wykorzystanie statycznej metody compile. Zwraca ona obiekt klasy Pattern obsługujący wyrażenie przekazane jako argument. W metodzie accept trzeba porównać ciąg znaków (nazwę pliku lub katalogu) przekazany w argumencie name z wyrażeniem reprezentowanym przez pattern. W tym celu należy powołać do życia dodatkowy obiekt klasy Matcher. Wywołujemy więc metodę matcher obiektu pattern, przekazując jej parametr name: Matcher matcher = pattern.matcher(name); w wyniku czego otrzymamy obiekt klasy Matcher. Zostaje on przypisany zmiennej matcher. Jeśli teraz wywołamy metodę matches obiektu matcher, uzyskamy odpowiedź, czy argument name jest zgodny z wyrażeniem regularnym reprezentowanym przez pattern. Tak więc wynik zwrócony przez wywołanie: matcher.matches() staje się jednocześnie wynikiem całej funkcji. Jeśli jest to wartość true, oznacza to, że name jest zgodne z wyrażeniem regularnym; jeśli jest to wartość false, zgodności nie ma. Zauważmy też, że w rzeczywistości można byłoby również całą treść metody accept zapisać w jednej linii, nie ma bowiem potrzeby tworzenia dodatkowej zmiennej matcher. Równie dobrze sprawdzi się kod w postaci19: return pattern.matcher(name).matches(); 18 Można również użyć obiektu klasy anonimowej. 19 Przykład mógłby być znacznie prostszy, gdyby zastosować metodę matches z klasy Pattern w połączeniu z klasą anonimową. Wtedy jednak kompilacja wyrażenia regularnego odbywałaby się przy każdym wywołaniu metody accept, co zgodnie z dokumentacją JDK jest metodą wolniejszą i powinno być stosowane tylko przy pojedynczych porównaniach. 302 Java. Praktyczny kurs Przykładowy efekt działania tego programu został przedstawiony na rysunku 6.13. Pierwszy znak . określa katalog bieżący, a użyte wyrażenie regularne ^M.*\.java$ oznacza ciągi znaków (nazwy plików) zaczynające się (^) od dużej litery M, po której następuje dowolna liczba dowolnych znaków (.*), a nazwa jest zakończona ($) ciągiem .java20. Program działa zatem zgodnie z założeniami, choć zawiera pewien błąd. Jego odnalezienie i poprawienie pozostanie jednak ćwiczeniem do samodzielnego wykonania (podpowiedź: co się stanie, jeśli w wywołaniu zostanie użyta nazwa nieistniejącego katalogu?). Rysunek 6.13. Efekt działania programu listującego zawartość katalogu Tworzenie plików i katalogów Klasa File zawiera metodę o nazwie createNewFile umożliwiającą utworzenie pliku21. Co prawda pliki można również tworzyć w inny sposób, korzystając z klas strumieniowych, czym zajmiemy się w lekcji 34., niemniej warto znać i ten sposób. Aby skorzystać z createNewFile, należy najpierw utworzyć obiekt klasy File, podając w konstruktorze nazwę pliku, a następnie wywołać metodę createNewFile. Przykładowy program wykonujący takie zadanie został przedstawiony na listingu 6.16. Listing 6.16. import java.io.*; public class Main { public static void main(String args[]) { if(args.length < 1){ System.out.println("Wywołanie programu: Main nazwa_pliku"); return; } File file = new File(args[0]); try{ if(file.createNewFile()){ System.out.println("Utworzony został plik: " + args[0]); } else{ System.out.println("Nie mogę utworzyć pliku: " + args[0]); } } catch(IOException e){ System.out.println("Błąd wejścia-wyjścia: " + e); 20 Dokładniejszy opis wyrażeń regularnych można znaleźć m.in. w publikacji JavaScript. Praktyczny kurs (http://helion.pl/ksiazki/jscpk.htm). 21 Metoda ta pojawiła się w JDK 1.2, wcześniejsze wersje JDK jej nie zawierają. Rozdział 6. System wejścia-wyjścia 303 return; } } } Nazwa pliku do utworzenia będzie podawana w wierszu poleceń przy wywoływaniu programu, np.: java Main test.txt Pierwszą instrukcją jest zatem sprawdzenie, czy nazwa ta została rzeczywiście podana. Jeśli nie, pozostaje wyświetlić stosowny komunikat i zakończyć pracę, wykorzystując instrukcję return. Jeśli tak, należy utworzyć obiekt file klasy File poprzez przekazanie konstruktorowi pierwszego argumentu z wiersza poleceń oraz wywołać metodę createNewFile. Jeśli plik nie istniał i został poprawnie utworzony, metoda ta zwraca wartość true, natomiast jeżeli plik o wskazanej nazwie istniał wcześniej, zwracana jest wartość false, a próba utworzenia nie jest podejmowana. Jeśli plik nie istniał, ale próba jego utworzenia się nie powiodła, generowany jest wyjątek IOException. Może tak się stać np. w sytuacji, kiedy podamy nieistniejącą ścieżkę dostępu. Równie ważne jak tworzenie plików jest tworzenie katalogów, w których te pierwsze mogą być umieszczane. Umożliwiają to dwie metody klasy File: mkdir oraz mkdirs. Różnica między nimi jest taka, że w przypadku metody mkdir musi istnieć pełna ścieżka dostępu, natomiast mkdirs potrafi utworzyć niezbędne katalogi nadrzędne. To znaczy, że jeśli istnieje przykładowy katalog /java/test/, to próba utworzenia katalogu /java/test/files/source przy użyciu mkdir zakończy się niepowodzeniem (nie istnieje bowiem ścieżka /java/test/files/), natomiast przy wykorzystaniu mkdirs zakończy się sukcesem (zostanie bowiem utworzony również brakujący podkatalog files). Obie metody zwracają wartość true, gdy próba utworzenia katalogu zakończyła się powodzeniem, oraz false w przeciwnym wypadku. Oznacza to, że w sytuacji, kiedy katalog istnieje i podjęta zostanie próba jego utworzenia, w wyniku otrzymamy wartość false (mimo że katalog jest na dysku). Warto więc przed wywołaniem mkdir lub mkdirs sprawdzać, czy katalog istnieje, za pomocą metody exists. Ten sposób został zastosowany w programie przedstawionym na listingu 6.17. Listing 6.17. import java.io.*; public class mkdir { public static void main(String args[]) { if(args.length < 1){ System.out.println("Wywołanie programu: mkdir nazwa_katalogu"); return; } File file = new File(args[0]); if(file.exists()){ System.out.println("Katalog o wskazanej nazwie już istnieje."); return; } if(file.mkdirs()){ 304 Java. Praktyczny kurs System.out.println("Utworzony został katalog: " + args[0]); } else{ System.out.println("Nie mogę utworzyć katalogu: " + args[0]); } } } Usuwanie plików i katalogów Skoro wiadomo już, jak tworzyć pliki i katalogi, należałoby także wiedzieć, w jaki sposób je usuwać. Funkcja wykonująca to zadanie nazywa się, jak łatwo sprawdzić w tabeli 6.6, po prostu delete. Usuwa ona plik lub katalog i zwraca wartość true, jeśli operacja ta zakończy się sukcesem, lub false w przeciwnym wypadku. Wartość false zostanie zwrócona również wtedy, gdy wskazany obiekt nie istnieje. Prosty przykład zastosowania metody delete został zaprezentowany na listingu 6.18. Listing 6.18. import java.io.*; public class delete { public static void main (String args[]) { if(args.length < 1){ System.out.println("Wywołanie programu: delete nazwa"); return; } File file = new File(args[0]); if(!file.exists()){ System.out.println("Nie ma takiego pliku lub katalogu."); return; } if(!file.delete()){ System.out.println("Plik/katalog nie został usunięty."); } else{ System.out.println("Plik/katalog został usunięty."); } } } Nazwa pliku lub katalogu do usunięcia jest przekazywana aplikacji w wierszu poleceń jako pierwszy parametr wywołania. Zostaje ona użyta w konstruktorze obiektu klasy File. Sprawdzamy następnie, czy wskazany plik lub katalog istnieje (if(!file.exists()){), jeśli nie, kończymy pracę programu (przez wywołanie instrukcji return). Jeśli jednak istnieje, wykonujemy metodę delete i w zależności od tego, jaka wartość zostanie przez nią zwrócona, wyświetlamy komunikat o powodzeniu lub niepowodzeniu operacji. Trzeba jednak wiedzieć, że jeśli spróbujemy usunąć katalog, który ma jakąś zawartość (nie jest pusty), operacja taka nie zostanie wykonana (metoda delete zwróci wartość false). Program usuwający katalog wraz z całą zawartością będzie musiał być skonstruowany inaczej. Można zastosować w tym celu technikę rekurencji, czyli wywoływania Rozdział 6. System wejścia-wyjścia 305 funkcji przez samą siebie22. Kod realizujący takie zadanie został przedstawiony na listingu 6.19. Listing 6.19. import java.io.*; public class Delete { public void delete(String name) { File file = new File(name); File[] dirList = file.listFiles(); for(int i = 0; i < dirList.length; i++){ if(dirList[i].isDirectory()){ delete(dirList[i].getPath()); } dirList[i].delete(); } file.delete(); } public static void main (String args[]) { if(args.length < 1){ System.out.println("Wywołanie programu: Delete katalog"); return; } if(!new File(args[0]).exists()){ System.out.println("Nie ma takiego katalogu."); return; } Delete delete = new Delete(); delete.delete(args[0]); } } Tym razem funkcja main zajmuje się jedynie sprawdzeniem, czy aplikacji został przekazany przynajmniej jeden argument, utworzeniem obiektu klasy Delete oraz wywołaniem jego metody delete. Właściwa funkcjonalność, czyli procedury usuwające pliki i katalogi, zawiera się właśnie w metodzie delete. Jako argument otrzymuje ona nazwę katalogu, który ma zostać usunięty. Wewnątrz tworzymy nowy obiekt klasy File i pobieramy listę obiektów znajdujących się w tym katalogu. Tym razem, inaczej niż we wcześniejszych przykładach, wykorzystujemy metodę listFiles, która zwraca tablicę obiektów klasy File. W pętli for sprawdzamy po kolei obiekty zawarte w tablicy dirList. Jeżeli dany obiekt jest katalogiem, wywołujemy rekurencyjnie funkcję delete, przekazując jej jako argument jego nazwę (uzyskaną dzięki wywołaniu metody getPath, patrz też tabela 6.6), czyli usuwamy ten podkatalog wraz z jego zawartością (delete(dirList[i].getPath())). Niezależnie jednak od tego, czy mamy do czynienia z plikiem, czy z katalogiem, wy22 Osoby nieznające technik rekurencyjnych powinny zapoznać się z dowolną książką opisującą podstawy algorytmów. 306 Java. Praktyczny kurs wołujemy usuwającą go funkcję delete (dirList[i].delete();). Na zakończenie usuwamy katalog bieżący (file.delete();). Ćwiczenia do samodzielnego wykonania Ćwiczenie 33.1. Napisz program, który wyświetli listę plików i podkatalogów katalogu o nazwie przekazanej w postaci argumentu w taki sposób, że najpierw pojawi się ponumerowana lista plików, a następnie ponumerowana lista katalogów. Pliki i katalogi powinny być numerowane osobno. Ćwiczenie 33.2. Zmodyfikuj kod klas z listingu 6.15 w taki sposób, aby w przypadku, kiedy podane w wywołaniu wyrażenie regularne nie jest prawidłowe, program nie zgłaszał błędu, ale też nie wyświetlał żadnych plików. W klasie Main nie może występować instrukcja try przechwytująca wyjątek PatternSyntaxException. Ćwiczenie 33.3. Zmodyfikuj program z listingu 6.19 tak, aby po wykonaniu operacji usuwania wyświetlił liczbę faktycznie usuniętych plików oraz katalogów. Ćwiczenie 33.4. Zmodyfikuj program z listingu 6.19 tak, by usuwał jedynie pliki, pozostawiając nienaruszoną strukturę katalogów. Ćwiczenie 33.5. Zmodyfikuj program z listingu 6.19 tak, aby nie było konieczności tworzenia obiektu klasy Delete. Lekcja 34. Operacje na plikach W poprzedniej lekcji został omówiony sposób wykonywania na plikach i katalogach operacji takich jak ich tworzenie i usuwanie. Niezbędna jest jednak również wiedza o sposobach zapisu i odczytu danych w plikach. Tej właśnie tematyce jest poświęcona niniejsza lekcja. Przedstawiona zostanie w tym miejscu dokładniej klasa RandomAccess File oraz jej metody pozwalające na wykonywanie tego typu operacji, nie zostaną również pominięte informacje o klasach strumieniowych, takich jak FileInputStream i FileOutputStream. Okaże się także, jaka jest różnica między operacjami buforowanymi i niebuforowanymi. Rozdział 6. System wejścia-wyjścia 307 Klasa RandomAccessFile Klasa RandomAccessFile daje możliwość wykonywania wszelkich operacji na plikach o dostępie swobodnym. Pozwala na odczytywanie i zapisywanie danych w pliku oraz przemieszczanie się po pliku. Jest dostępna we wszystkich JDK, począwszy od JDK 1.0. Metody udostępniane przez RandomAccessFile są zebrane w tabeli 6.7. Tabela 6.7. Metody klasy RandomAccessFile Deklaracja metody Opis Od JDK void close() Zamyka strumień oraz zwalnia wszystkie związane z nim zasoby. 1.0 FileChannel getChannel() Zwraca powiązany z plikiem unikalny obiekt typu FileChannel. 1.4 FileDescriptor getFD() Zwraca deskryptor pliku powiązanego ze strumieniem. 1.0 long getFilePointer() Zwraca aktualną pozycję w pliku. 1.0 long length() Zwraca długość pliku. 1.0 int read() Odczytuje kolejny bajt danych z pliku. 1.0 int read(byte[] b) Odczytuje z pliku liczbę bajtów nie większą niż rozmiar tablicy b i umieszcza je w tej tablicy. Zwraca rzeczywiście odczytaną liczbę bajtów. 1.0 int read(byte[] b, int off, int len) Odczytuje z pliku liczbę bajtów nie większą niż wskazywana przez len i zapisuje je w tablicy b, począwszy od komórki wskazywanej przez off. Zwraca faktycznie przeczytaną liczbę bajtów. 1.0 boolean readBoolean() Odczytuje wartość typu boolean. 1.0 byte readByte() Odczytuje wartość typu byte. 1.0 char readChar() Odczytuje wartość typu char. 1.0 double readDouble() Odczytuje wartość typu double. 1.0 float readFloat() Odczytuje wartość typu float. 1.0 void readFully(byte[] b) Odczytuje z pliku liczbę bajtów równą wielkości tablicy b. 1.0 void readFully(byte[] b, int off, int len) Odczytuje z pliku liczbę bajtów wskazywaną przez len i zapisuje 1.0 je w tablicy b, począwszy od komórki wskazywanej przez off. int readInt Odczytuje wartość typu int. 1.0 String readLine Odczytuje linię tekstu. 1.0 long readLong Odczytuje wartość typu long. 1.0 short readShort Odczytuje wartość typu short. 1.0 int readUnsignedByte Odczytuje wartość typu unsigned byte. 1.0 int readUnsignedShort Odczytuje wartość typu unsigned short. 1.0 String readUTF Odczytuje tekst w kodowaniu UTF-8. 1.0 void seek(long pos) Zmienia wskaźnik pozycji w pliku na pos. 1.0 void setLength (long newLength) Ustawia rozmiar pliku na newLength. 1.2 int skipBytes(int n) Pomija n bajtów. 1.0 308 Java. Praktyczny kurs Tabela 6.7. Metody klasy RandomAccessFile (ciąg dalszy) Deklaracja metody Opis Od JDK void write(byte[] b) Zapisuje tablicę bajtów b w pliku. 1.0 void write(byte[] b, int off, int len) Zapisuje w pliku len bajtów z tablicy b, począwszy od komórki wskazywanej przez off. 1.0 void write(int b) Zapisuje bajt b w pliku. 1.0 void writeBoolean (boolean v) Zapisuje w pliku wartość boolean w postaci jednego bajtu. 1.0 void writeByte(int v) Zapisuje w pliku bajt v. 1.0 void writeBytes(String s) Zapisuje w pliku ciąg znaków s w postaci sekwencji bajtów. 1.0 void writeChar(int v) Zapisuje w pliku wartość typu char w postaci dwóch bajtów. 1.0 void writeChars(String s) Zapisuje w pliku ciąg wskazywany przez s w postaci sekwencji znaków. 1.0 void writeDouble(double v) Konwertuje wartość v na typ long, korzystając z metody doubleToLongBits z klasy Double, i tak powstałą wartość zapisuje do pliku (8 bajtów). 1.0 void writeFloat(float v) Konwertuje wartość v na typ int, korzystając z metody floatToLongBits z klasy Float, i tak powstałą wartość zapisuje w pliku (4 bajty). 1.0 void writeInt(int v) Zapisuje w pliku wartość typu int w postaci czterech bajtów. 1.0 void writeLong(long v) Zapisuje w pliku wartość typu long w postaci ośmiu bajtów. 1.0 void writeShort(int v) Zapisuje w pliku wartość typu short w postaci dwóch bajtów. 1.0 void writeUTF(String str) Zapisuje w pliku ciąg znaków s w kodowaniu UTF-8. 1.0 Aby wykonywać operacje na plikach przy użyciu klasy RandomAccessFile, należy utworzyć jej instancję, podając w konstruktorze ścieżkę dostępu do pliku oraz tryb jego otwarcia. Ścieżka dostępu może być przedstawiona w postaci ciągu znaków String lub jako obiekt klasy File. Do dyspozycji są więc dwa konstruktory: RandomAccessFile(File file, String mode) RandomAccessFile(String name, String mode) Oba tworzą powiązany z plikiem wskazywanym przez file lub name strumień służący do odczytu i (lub) zapisu danych. Jeśli zostanie użyty parametr mode równy r, plik musi wcześniej istnieć na dysku, inaczej zostanie zgłoszony wyjątek FileNotFoundException. Parametr mode może przyjmować jedną z czterech wartości: r — otwarcie tylko do odczytu, próba wykonania jakiejkolwiek operacji zapisu spowoduje wygenerowanie wyjątku IOException; rw — otwarcie do odczytu i zapisu, jeżeli plik nie istnieje, zostanie podjęta próba jego utworzenia; rws — otwarcie do odczytu i zapisu wymuszające synchroniczny zapis w rzeczywistym urządzeniu (np. na dysku twardym) przy każdej modyfikacji danych lub metadanych pliku; Rozdział 6. System wejścia-wyjścia 309 rwd — otwarcie do odczytu i zapisu wymuszające synchroniczny zapis w rzeczywistym urządzeniu (np. na dysku twardym) przy każdej modyfikacji danych w pliku. Tryby rws i rwd oznaczają, że przy każdej modyfikacji danych mają one być od razu, z pominięciem buforowania, zapisywane w urządzeniu. Dokładny opis tych trybów wykracza poza ramy niniejszej publikacji. Odczyt danych z pliku Skoro wiadomo już, jakie funkcje udostępnia klasa RandomAccessFile, można przystąpić do wykonywania operacji na plikach. Na początek odczyt danych. Do dyspozycji jest wiele różnych metod operujących na różnych typach danych, nie ma jednak potrzeby, aby wszystkie dokładnie omawiać, ich zastosowanie wydaje się jasne. Pojawi się za to przykład pokazujący odczyt z pliku tekstowego. Będzie to program odczytujący kolejne wiersze tekstu z pliku, którego nazwa została podana jako pierwszy argument wywołania, i wyświetlający je na ekranie. Kod realizujący takie zadanie został zaprezentowany na listingu 6.20. Listing 6.20. import java.io.*; public class Main { public static void main (String args[]) { if(args.length < 1){ System.out.println("Wywołanie programu: Main nazwa_pliku"); return; } File file = new File(args[0]); } } if(!file.exists()){ System.out.println("Nie ma takiego pliku."); return; } RandomAccessFile raf = null; try{ raf = new RandomAccessFile(file, "r"); } catch(FileNotFoundException e){ System.out.println("Nie ma takiego pliku."); return; } String line = ""; try{ while((line = raf.readLine()) != null){ System.out.println(line); } raf.close(); } catch(IOException e){ System.out.println("Błąd wejścia-wyjścia."); } 310 Java. Praktyczny kurs Na początku sprawdzamy, czy istnieje przynajmniej jeden argument wywołania — jeśli nie, wyświetlamy odpowiednią informację na ekranie i za pomocą instrukcji return kończymy działanie funkcji main, a tym samym całego programu. Jeśli argument jest obecny, tworzymy zmienną file i przypisujemy jej referencję do nowego obiektu klasy File. Konstruktorowi tego obiektu przekazujemy argument zapisany w tablicy args pod indeksem 0. Sprawdzamy dalej, czy plik istnieje na dysku, wywołując metodę exists — jeśli nie, kończymy działanie aplikacji. Tym sposobem dotarliśmy do miejsca deklaracji zmiennej klasy RandomAccessFile i utworzenia obiektu tej klasy. Konstruktorowi przekazujemy dwa argumenty: pierwszy to obiekt klasy File, drugi — ciąg znaków określający tryb dostępu do pliku. Znak r określa oczywiście tryb tylko do odczytu (zgodnie z opisem trybów podanym wyżej). Jeśli plik wskazywany przez file nie istnieje, konstruktor zgłosi wyjątek FileNotFoundException, dlatego też został ujęty w blok try. Kwestią do samodzielnego rozważenia pozostanie tu następujący problem: czy wobec tego konieczne było wcześniejsze sprawdzenie istnienia pliku za pomocą metody exists obiektu file, czy też był to tylko nadmiar ostrożności? Skoro obiekt raf został utworzony, można przystąpić do odczytu pliku. Deklarujemy zmienną line typu String, która będzie tymczasowo przechowywała kolejne linie tekstu. Odczyt odbywa się w pętli while. Warunek tej pętli jest instrukcją złożoną: line = raf.readLine()) != null. W każdym przebiegu najpierw jest wykonywana metoda readLine obiektu wskazywanego przez raf, następnie wynik jej działania zostaje przypisany zmiennej line i ostatecznie zostaje wykonane sprawdzenie, czy line jest różne od null. Porównanie wartości zwróconej przez readLine z null jest konieczne do stwierdzenia, że został osiągnięty koniec pliku. W środku pętli znajduje się tylko jedna instrukcja, wyświetlająca wartość przypisaną line na ekranie. Po zakończeniu pętli zamykamy strumień, wywołując metodę close. Całość jest ujęta w blok try, gdyż podczas odczytu może wystąpić wyjątek IOException. Zapis danych do pliku Klasa RandomAccessFile udostępnia również wiele funkcji pozwalających na zapis różnego typu danych w plikach. W rzeczywistości funkcje te zapisują jednak zwykłe ciągi bajtów. Przykładowo funkcja writeInt nie zapisuje abstrakcyjnej wartości typu int, ale po prostu cztery bajty danych reprezentujących wartość typu int. Z kolei funkcja readInt odczytuje ze strumienia cztery bajty i składa z nich wartość typu int. To, jak odczytywać dane z pliku, wiadomo już z poprzedniej części lekcji. Czas więc poznać sposób, w jaki dokonywany jest zapis. Napiszemy program, który będzie odczytywał ze standardowego wejścia dane wprowadzane przez użytkownika i zapisywał je w pliku tekstowym. Nazwę pliku będzie można podać w wywołaniu programu lub też wprowadzić w trakcie jego wykonania. Kod realizujący przedstawione zadanie jest widoczny na listingu 6.21. Listing 6.21. import java.io.*; public class Main { Rozdział 6. System wejścia-wyjścia public static void main (String args[]) { BufferedReader brIn = new BufferedReader( new InputStreamReader(System.in) ); String fileName = ""; if(args.length < 1){ System.out.print("Podaj nazwę pliku:"); try{ fileName = brIn.readLine(); } catch(IOException e){ System.out.print("\nBłąd wejścia-wyjścia."); return; } } else{ fileName = args[0]; } File file = new File(fileName); if(file.exists()){ System.out.println("Plik o tej nazwie już istnieje."); return; } RandomAccessFile raf = null; try{ raf = new RandomAccessFile(file, "rw"); } catch(FileNotFoundException e){ System.out.println("Nie można utworzyć pliku."); return; } String line = ""; try{ while(true){ line = brIn.readLine(); if("quit".equals(line) || line == null){ break; } raf.writeBytes(line + "\n"); } raf.close(); } catch(IOException e){ System.out.print("\nBłąd wejścia-wyjścia."); return; } } } 311 312 Java. Praktyczny kurs Tworzymy obiekt brIn klasy BufferedReader powiązany ze standardowym strumieniem wejściowym, w dokładnie taki sam sposób jak w przypadku przykładów z lekcji 31. i 32. Sprawdzamy dalej, czy w linii wywołania został podany przynajmniej jeden argument. Jeśli tak, przypisujemy go zmiennej fileName. Jeżeli argument nie został podany, wyświetlamy na ekranie prośbę o wprowadzenie nazwy pliku, a odczytaną linię również przypisujemy zmiennej fileName. Następnie tworzymy obiekt klasy File, przekazując konstruktorowi wartość zapisaną w fileName, oraz sprawdzamy, czy taki plik istnieje. Jeśli istnieje, kończymy wykonywanie programu (instrukcja return). Jeżeli jednak wskazanego pliku nie ma, tworzymy nowy obiekt klasy RandomAccessFile, ustawiając parametr mode na rw, czyli w trybie do zapisu i odczytu. Instrukcje te trzeba ująć w blok try, ponieważ gdy nie uda się utworzyć pliku, zostanie zgłoszony wyjątek FileNotFoundException. Kiedy obiekt raf klasy RandomAccessFile zostanie utworzony, można przystąpić do zasadniczej części zadania, czyli odczytu danych ze standardowego wejścia i zapisu do pliku. Rozpoczynamy więc pętlę while, w której odczytujemy wprowadzane linie tekstu za pomocą metody readLine obiektu brIn. Każda odczytana linia zostaje przypisana zmiennej line. Następnie sprawdzamy, czy w line jest ciąg znaków quit, co by oznaczało, że użytkownik zakończył wprowadzanie danych, oraz czy line jest równe null, co z kolei znaczyłoby, że nastąpił koniec strumienia. Jeżeli zachodzi którakolwiek z tych sytuacji, kończymy pętlę. Jeśli jednak line jest różne i od quit, i od null, zapisujemy odczytaną linię tekstu w pliku przy użyciu metody writeBytes obiektu raf. Do każdej linii dodawany jest znak końca wiersza \n. To konieczne, gdyż dane odczytane za pomocą metody readLine takiego znaku nie zawierają (a więc bez tej czynności tekst w pliku nie byłby podzielony na wiersze). Należy zwrócić uwagę, że zastosowany został znak końca wiersza charakterystyczny dla systemów uniksowych. W Windowsie powinny to być dwa znaki: \r\n. Po zakończeniu pętli zamykamy strumień (plik), wykorzystując metodę close. Kopiowanie plików Skoro potrafimy już odczytywać i zapisywać pliki, nie powinno sprawić nam trudności ich kopiowanie. Wystarczy utworzyć jeden obiekt RandomAccessFile dla pliku źródłowego, drugi dla pliku docelowego, a następnie w pętli odczytywać zawartość pierwszego i zapisywać w drugim. Wykonajmy zatem taki przykład. Nazwy plików źródłowego i docelowego będą podawane przy wywołaniu programu. Klasę realizującą to zadanie nazwijmy Kopiuj; jej kod jest widoczny na listingu 6.22. Listing 6.22. import java.io.*; public class Kopiuj { public static void main (String args[]) { if(args.length < 2){ System.out.println("Wywołanie programu: Kopiuj plik_źródłowy plik_docelowy"); return; } Rozdział 6. System wejścia-wyjścia 313 File sourceFile = new File(args[0]); File destFile = new File(args[1]); if(!sourceFile.exists()){ System.out.println("Plik return; } if(destFile.exists()){ System.out.println("Plik return; } RandomAccessFile rafSource RandomAccessFile rafDest = źródłowy nie istnieje."); docelowy istnieje."); = null; null; try{ rafSource = new RandomAccessFile(sourceFile, "r"); rafDest = new RandomAccessFile(destFile, "rw"); } catch(FileNotFoundException e){ System.out.println("Błąd podczas otwierania plików: " + e); return; } int b; try{ while((b = rafSource.read()) != -1){ rafDest.write(b); } rafSource.close(); rafDest.close(); } catch(IOException e){ System.out.print("\nBłąd wejścia-wyjścia podczas kopiowania pliku."); return; } } } Tworzymy obiekty sourceFile oraz destfile, wykorzystując parametry przekazane aplikacji. Obiekt reprezentowany przez sourceFile będzie powiązany z plikiem źródłowym, natomiast ten wskazywany przez destfile — z plikiem docelowym. Sprawdzamy następnie, czy istnieje plik źródłowy — jeśli nie, kończymy działanie programu. Sprawdzamy też, czy istnieje plik docelowy — jeśli tak, również kończymy działanie programu (tak, aby pliku nie nadpisać). Jeżeli jednak istnieje plik źródłowy oraz nie istnieje plik docelowy, tworzymy obiekty rafSource i rafDest, które będą użyte podczas właściwej operacji kopiowania. Obiekt rafSource jest tworzony w trybie tylko do odczytu, natomiast rafDest w trybie do odczytu i zapisu. Kopiowanie odbywa się przez cykliczne wywoływanie metody read obiektu źródłowego oraz metody write obiektu docelowego. Deklarujemy zmienną b typu int, która będzie pośredniczyła w wymianie danych. Dlaczego ta zmienna jest typu int? Po zajrzeniu do tabeli 6.7 okaże się, że metoda read, odczytując bajt, nie zwraca wartości typu byte, ale int. Dokładniej, jest to wartość typu int z zakresu 0 – 255. Dzięki temu można łatwo 314 Java. Praktyczny kurs zasygnalizować koniec pliku, metoda read zwraca wtedy wartość –1. Gdyby wartość zwracana była typu byte, nie byłoby to możliwe i koniec pliku musiałby być sygnalizowany wystąpieniem wyjątku bądź w inny, mniej wygodny sposób. Po tych wyjaśnieniach znaczenie wyrażenia warunkowego pętli while powinno być jasne. Jest to wyrażenie złożone, podobnie jak w przypadku przykładu z listingu 6.20. Instrukcja ta odczytuje kolejne bajty z pliku źródłowego i przypisuje je zmiennej b tak długo, aż metoda read zwróci wartość –1, czyli zostanie osiągnięty koniec pliku. Wewnątrz pętli while za pomocą metody write zapisuje się każdy odczytany bajt w docelowym pliku wskazywanym przez rafDest. Po wykonaniu kopiowania należy zamknąć oba pliki (czy też dokładniej: strumienie powiązane z plikami), wywołując metodę close. Przedstawiona metoda kopiowania jest najprostsza w realizacji, jednak nadaje się jedynie do kopiowania plików o niewielkich rozmiarach. Ponieważ klasa RandomAccessFile tworzy niebuforowany strumień operujący na pliku, czytanie i zapis bajt po bajcie jest metodą bardzo nieefektywną (łatwo się o tym przekonać, próbując kopiować za pomocą przedstawionego programu pliki o dużych rozmiarach). Rozwiązaniem jest wykorzystanie buforowanych klas strumieniowych (zostanie to pokazane w dalszej części lekcji) lub samodzielna obsługa buforowania. Wystarczy przecież umieścić w utworzonej klasie niewielki bufor i skorzystać z metod czytających naraz większą ilość danych. Jak wynika z danych zawartych w tabeli 6.7, takie metody są dostępne w klasie RandomAccess File. Program wykonujący kopiowanie plików, korzystający z własnego bufora, został przedstawiony na listingu 6.23. Listing 6.23. import java.io.*; public class Kopiuj { public static void main (String args[]) { /* tutaj początek kodu z listingu 6.22 aż do instrukcji tworzących obiekty rafSource i rafDest */ int count = 0; int buffSize = 10000; byte[] buff = new byte[buffSize]; try{ while((count = rafSource.read(buff)) != -1){ rafDest.write(buff, 0, count); } rafDest.close(); rafSource.close(); } catch(IOException e){ System.out.print("\nBłąd wejścia-wyjścia podczas kopiowania pliku."); return; } } } Rozdział 6. System wejścia-wyjścia 315 Początek kodu jest taki sam jak w przykładzie z listingu 6.20, nie ma zatem potrzeby, aby go powtórnie omawiać. Zmienia się natomiast metoda kopiowania danych. Przede wszystkim zostają zadeklarowane dodatkowe zmienne: count — przechowująca liczbę odczytanych bajtów; buffSize — przechowująca wielkość bufora; buff — tablica typu byte będąca buforem danych. W pętli while za pomocą metody read jest odczytywana porcja danych. Zostaną one zapisane w tablicy buff. Liczba odczytanych bajtów zostanie zwrócona przez tę metodę i zapisana w zmiennej count. Jeżeli będzie to wartość –1, będzie to oznaczało osiągnięcie końca pliku. Po każdym odczytaniu dane są zapisywane w pliku docelowym za pomocą metody write. Metodzie tej przekazuje się trzy parametry, pierwszy to bufor (czyli tablica bajtów), drugi to indeks komórki tablicy, od której rozpocznie się zapis, trzeci to liczba bajtów do zapisania. W celach testowych dobrze byłoby teraz uruchomić obie wersje programu (z listingu 6.22 i 6.23) i wykonać kopiowanie jakiegoś dużego pliku (rzędu kilku – kilkunastu megabajtów). Różnica w czasie wykonania będzie aż nadto widoczna. Ciekawym eksperymentem jest również zmienianie wielkości bufora i obserwowanie czasu wykonania programu. W tym miejscu warto jeszcze zwrócić uwagę na sposób działania wykorzystanej w programie metody read. Liczba odczytanych bajtów może wahać się od jednego do wielkości tablicy będącej buforem, nigdy nie wolno zatem zakładać, że bufor jest za każdym razem zapełniany do końca. Nie jest więc możliwe zastosowanie konstrukcji: while(rafSource.read(buff) != -1){ rafDest.write(buff, 0, buff.length); } W tym przypadku błąd jest raczej oczywisty. Przecież plik nie musi mieć rozmiaru będącego wielokrotnością rozmiaru bufora, a takie założenie zostało w tym fragmencie uczynione. Błąd zostanie też szybko wykryty, gdyż plik źródłowy i docelowy praktycznie w każdym wypadku będą się różnić. Dużo groźniejsze jest natomiast poprawienie tego błędu w sposób następujący: while((count = rafSource.read(buff)) != -1){ rafDest.write(buff, 0, count); if(count != buffSize) break; } lub: while(true){ count = rafSource.read(buff); if(count != -1) rafDest.write(buff, 0, count); if(count != buffSize) break; } Oba powyższe przykłady są niepoprawne, gdyż opierają się na założeniu, że liczba odczytanych bajtów jest zawsze równa wielkości bufora, a jedynie przy ostatnim odczycie (przy samym końcu) może być mniejsza (ze względu na to, że rozmiar pliku nie jest 316 Java. Praktyczny kurs wielokrotnością rozmiaru bufora). W większości wypadków (szczególnie kiedy operuje się na plikach dyskowych) tak właśnie się dzieje i może się wydawać, że program działa poprawnie. Nie wolno jednak czynić takiego założenia! Powtórzmy: zastosowana wersja funkcji read ma pełne prawo odczytać ze strumienia dowolną liczbę bajtów z przedziału od jednego do rozmiaru tablicy. To, że w większości przypadków odczytuje pełny rozmiar bufora, nie oznacza, że zawsze tak będzie. Trzeba więc za każdym razem kontrolować liczbę odczytanych bajtów. Strumieniowe operacje na plikach Tytuł może być nieco mylący, gdyż dotychczas opisywana klasa RandomAccessFile również tworzy strumień, niemniej jest ona niezależna od hierarchii klas pochodzących od InputStream i OutputStream, które pojawiły się przy omówieniu standardowego wejścia i wyjścia. Co więcej, zawiera metody odczytujące i zapisujące dane w plikach, podczas gdy tradycyjne operacje strumieniowe są rozdzielone. Inne klasy obsługują strumienie wejściowe, a inne wyjściowe. Spróbujmy wykonać kilka przykładów operujących na klasycznych strumieniach. Odczyt danych Do dyspozycji są dwie podstawowe klasy FileReader oraz FileInputStream. Pierwsza z nich powinna być stosowana, kiedy chce się skorzystać ze strumienia znakowego, czyli raczej dla plików tekstowych, druga — kiedy chce się skorzystać ze strumienia binarnego, czyli raczej dla plików binarnych. Obie klasy mają po trzy przeciążone konstruktory. Argumentami tych konstruktorów mogą być: ciąg znaków zawierający nazwę pliku; obiekt klasy FileDescriptor; obiekt klasy File. Klasa FileInputStream udostępnia trzy metody odczytujące dane (te metody zebrano w tabeli 6.8). Klasa FileReader udostępnia jedynie dwie metody odczytujące dane, obie odziedziczone po klasie InputStreamReader (te metody są zawarte w tabeli 6.9). Tabela 6.8. Metody odczytujące dane w klasie FileInputStream Deklaracja metody Opis int read() Wersja JDK Odczytuje jeden bajt danych. 1.0 int read(byte[] b) Odczytuje liczbę bajtów nie większą od długości tablicy b i zapisuje je w tablicy b. Zwraca rzeczywiście odczytaną liczbę bajtów. 1.0 int read(byte[] b, int off, int len) Odczytuje liczbę bajtów nie większą niż wskazywana przez len i zapisuje je w tablicy b, począwszy od komórki wskazywanej przez off. Zwraca rzeczywiście przeczytaną liczbę bajtów. 1.0 Rozdział 6. System wejścia-wyjścia 317 Tabela 6.9. Metody odczytujące dane w klasie FileReader Deklaracja metody Opis Wersja JDK int read() Odczytuje pojedynczy znak. 1.0 int read(char[] cbuf, int offset, int length) Odczytuje liczbę znaków nie większą niż wskazywana przez length i zapisuje je w tablicy cbuf, począwszy od komórki wskazywanej przez offset. Zwraca rzeczywiście przeczytaną liczbę znaków. 1.0 Metod tych można używać bezpośrednio lub też wykorzystać obiekty klas FileReader i FileInputStream jako argumenty dla konstruktorów klas dających większą funkcjonalność, np. BufferedInputStream lub BufferedReader. Ta technika jest już znana z przykładów obrazujących obsługę standardowego strumienia wejściowego. Napiszmy więc przykładowy program, który za pomocą wymienionych klas będzie odczytywał linie tekstu z pliku i wypisywał je na ekranie. Tak działający program przedstawiono na listingu 6.24. Listing 6.24. import java.io.*; public class Main { public static void main (String args[]) { if(args.length < 1){ System.out.print("Wywołanie programu: Main nazwa_pliku"); return; } BufferedReader brIn = null; try{ brIn = new BufferedReader( new FileReader(args[0]) ); } catch(FileNotFoundException e){ System.out.print("Nie mogę odnaleźć wskazanego pliku."); return; } String line = ""; try{ while((line = brIn.readLine()) != null){ System.out.println(line); } brIn.close(); } catch(IOException e){ System.out.print("\nBłąd wejścia-wyjścia."); return; } } } Zasada działania aplikacji jest bardzo podobna do tej przedstawionej w przykładzie z listingu 6.20, wykorzystany zostaje tylko obiekt innej klasy — BufferedReader zamiast 318 Java. Praktyczny kurs RandomAccessFile. Obiekt brIn jest tworzony przez kaskadowe (podobnie jak w przykładach z lekcji 31.) wywołanie konstruktorów klasy BufferedReader oraz FileReader. Konstruktorowi klasy FileReader jest przekazywany argument odczytany z wiersza poleceń. Jeśli plik o przekazanej nazwie nie istnieje, zostanie wygenerowany wyjątek FileNot FoundException, dlatego też cała konstrukcja jest ujęta w blok try. Odczyt danych odbywa się w pętli while, instrukcja readLine obiektu brIn jest wywoływana tak długo, aż zwróci wartości null, co oznaczać będzie osiągnięcie końca pliku. Każda odczytana linia jest zapisywana w zmiennej line, a następnie wyświetlana na ekranie za pomocą instrukcji System.out.println. Zapis do pliku Dane zapisuje się w plikach, wykorzystując klasy obsługujące strumienie wyjściowe. Podobnie jak w przypadku odczytu, do dyspozycji są dwie podstawowe klasy: FileWriter oraz FileOutputStream. Pierwsza z nich powinna być wykorzystywana do zapisu plików tekstowych, druga do zapisu strumieni binarnych. Klasa FileOutputStream oferuje trzy metody służące do zapisu danych (te metody zebrano w tabeli 6.10). Klasa File Writer dysponuje również trzema metodami zapisującymi. Wszystkie one zostały odziedziczone po klasie nadrzędnej, OutputStreamWriter (zestawiono je w tabeli 6.11). Tabela 6.10. Metody zapisujące dane klasy FileOutputStream Deklaracja metody Opis Wersja JDK void write(byte[] b) Zapisuje w strumieniu tablicę bajtów b. 1.0 void write(byte[] b, int off, int len) Zapisuje w strumieniu len bajtów z tablicy b, począwszy od komórki wskazywanej przez off. 1.0 void write(int b) Zapisuje w strumieniu bajt b. 1.0 Tabela 6.11. Metody zapisujące dane klasy FileWriter Deklaracja metody Opis Wersja JDK void write(char[] cbuf, int off, int len) Zapisuje w strumieniu len znaków z tablicy cbuf, począwszy od komórki wskazywanej przez off. 1.0 void write(int c) Zapisuje w strumieniu znak c. 1.0 void write(String str, int off, int len) Zapisuje w strumieniu len znaków z obiektu str, począwszy od znaku o indeksie wskazywanym przez off. 1.0 Obie klasy mają po pięć przeciążonych konstruktorów. Trzy z nich są jednoargumentowe i mogą przyjmować następujące argumenty: ciąg znaków zawierający nazwę pliku; obiekt klasy FileDescriptor; obiekt klasy File. Rozdział 6. System wejścia-wyjścia 319 Pozostałe konstruktory są dwuargumentowe. Pierwszy przyjmuje ciąg znaków oraz wartość boolean, drugi obiekt klasy File i wartość boolean. W obu przypadkach drugi argument ustawiony na true oznacza, że dane mają być dopisywane na końcu pliku, a ustawiony na false — że mają być zapisywane od początku pliku (przy nadpisywaniu jego wcześniejszej zawartości). Jeżeli podany plik istnieje, zostanie otwarty, jeśli nie istnieje, zostanie podjęta próba jego utworzenia. Wszystkie konstruktory generują wyjątek FileNotFoundException w następujących sytuacjach: podana nazwa wskazuje na katalog, a nie na plik; podany plik nie istnieje i nie można go również utworzyć; nie można z jakiegoś powodu otworzyć istniejącego pliku. Powyższe wiadomości pozwalają już bez problemu napisać program, który będzie wczytywał dane ze standardowego wejścia i zapisywał je do pliku. Taki program jest przedstawiony na listingu 6.25. Listing 6.25. import java.io.*; public class Main { public static void main (String args[]) { if(args.length < 1){ System.out.print("Wywołanie programu: Main nazwa_pliku"); return; } BufferedReader brIn = new BufferedReader( new InputStreamReader(System.in) ); FileWriter fileWriter = null; try{ fileWriter = new FileWriter(args[0], true); } catch(IOException e){ System.out.println("Nie mogę otworzyć wskazanego pliku."); return; } String line = ""; try{ while(true){ line = brIn.readLine(); if("quit".equals(line) || line == null){ break; } line += "\n"; fileWriter.write(line, 0, line.length()); } fileWriter.close(); 320 Java. Praktyczny kurs } catch(IOException e){ System.out.print("\nBłąd wejścia-wyjścia."); return; } } } Za odczyt danych odpowiada obiekt wskazywany przez zmienną brIn klasy Buffered Reader, za zapis — obiekt fileWriter klasy FileWriter. Obiekt zapisujący dane jest tworzony w taki sposób, że jeśli plik istnieje, dane będą dopisywane na jego końcu (drugi argument konstruktora równy true). W pętli while za pomocą metody readLine obiektu brIn odczytujemy ze standardowego wejścia kolejne linie tekstu. Sprawdzamy, czy zwrócona wartość jest równa quit, co by oznaczało, że użytkownik zakończył wprowadzanie danych, oraz czy jest ona równa null, co wskazuje na osiągnięcie końca strumienia (if("quit".equals(line) || line == null)). Osiągnięcie końca strumienia w przypadku standardowego strumienia wejściowego oznacza zwykle sytuację nieprawidłową, np. niespodziewane przerwanie działania programu przez użytkownika. Odczytany tekst znajdzie się w zmiennej line. Dodajemy do niej znak końca linii (znak ten jest usuwany przez metodę readLine podczas odczytu, nie ma go zatem w zmiennej line) oraz wywołujemy metodę write obiektu fileWriter. Tym samym zapisujemy odczytaną linię tekstu w pliku. Metoda write wymaga podania parametrów określających, jaką część ciągu znaków chcemy zapisać. Ponieważ zapisana ma być cała zawartość zmiennej line, pierwszym parametrem jest 0 (indeks pierwszego znaku, który ma znaleźć się w strumieniu), natomiast drugim — długość ciągu pobierana przez wywołanie metody length (liczba znaków, które mają znaleźć się w strumieniu). Kopiowanie plików Skoro wiadomo już, jak zapisywać i odczytywać pliki, można napisać strumieniową wersję programu wykonującego operację kopiowania. Tym razem zarówno strumień wejściowy, jak i wyjściowy będą strumieniami buforowanymi. Wykorzystamy klasy operujące na strumieniach binarnych BufferedOutputStream oraz BufferedInputStream. Kod realizujący to zadanie jest widoczny na listingu 6.26. Listing 6.26. import java.io.*; public class Kopiuj { public static void main (String args[]) { if(args.length < 2){ System.out.println("Wywołanie programu: Kopiuj plik_źródłowy plik_docelowy"); return; } File sourceFile = new File(args[0]); File destFile = new File(args[1]); if(!sourceFile.exists()){ Rozdział 6. System wejścia-wyjścia 321 System.out.println("Plik źródłowy nie istnieje."); return; } if(destFile.exists()){ System.out.println("Plik docelowy istnieje."); return; } BufferedInputStream source = null; BufferedOutputStream dest = null; try{ source = new BufferedInputStream( new FileInputStream(sourceFile) ); dest = new BufferedOutputStream( new FileOutputStream(destFile) ); } catch(FileNotFoundException e){ System.out.println("Błąd podczas tworzenia strumieni: " + e); return; } int b; try{ while((b = source.read()) != -1){ dest.write(b); } source.close(); dest.close(); } catch(IOException e){ System.out.print("\nBłąd wejścia-wyjścia podczas kopiowania pliku."); return; } } } Zasada działania tego programu jest analogiczna do tej z przykładu przedstawionego na listingu 6.22, z tą różnicą, że operuje się na innych klasach. Obiektem źródłowym jest teraz obiekt source klasy BufferedInputStream, natomiast docelowym obiekt dest klasy BufferedOutputStream. Tym razem można sobie pozwolić na kopiowanie bajt po bajcie, gdyż wymienione klasy same „dbają” o buforowanie danych, każda z nich zawiera w sobie wewnętrzny bufor. Warto porównać szybkość działania tej aplikacji z kodem z listingu 6.22, różnica z pewnością będzie widoczna, mimo że w obu przypadkach wykorzystuje się metody odczytujące i zapisujące po jednym bajcie danych. Oczywiście fakt, że klasy BufferedInputStream i BufferedOutputStream wykorzystują swoje własne mechanizmy buforowania, nie wyklucza zastosowania dodatkowego bufora w aplikacji, tak jak to miało miejsce w przypadku kodu z listingu 6.23. Pytanie brzmi tylko: czy zastosowanie takiego dodatkowego bufora przyspieszyłoby działanie powyższej wersji programu? Warto samodzielnie wykonać takie ćwiczenie. 322 Java. Praktyczny kurs Ćwiczenia do samodzielnego wykonania Ćwiczenie 34.1. Napisz program realizujący takie samo zadanie jak kod z listingu 6.20, nie korzystaj jednak z klasy File. Ćwiczenie 34.2. Zmodyfikuj program z listingu 6.23 w taki sposób, aby w sytuacji, kiedy docelowy plik istnieje, pytał, czy może skasować jego zawartość. Ćwiczenie 34.3. Zmodyfikuj program z listingu 6.26 tak, by występowało w nim wewnętrzne buforowanie danych, tak jak w przykładzie z listingu 6.23. Sprawdź, czy pojawią się znaczące różnice w prędkości działania programów. Ćwiczenie 34.4. Napisz program, który wyświetli zawartość pliku tekstowego w taki sposób, że poszczególne wiersze będą ponumerowane. Numeracja powinna zaczynać się od 1 lub od wartości przekazanej jako parametr wywołania programu. Ćwiczenie 34.5. Napisz program, który będzie sumował liczby zapisane w pliku tekstowym i wyświetlał wynik na ekranie. Nazwa pliku z danymi do sumowania powinna być podana w wierszu poleceń jako argument wywołania aplikacji. Rozdział 7. Kontenery i typy uogólnione W każdym programie trzeba przechowywać najrozmaitsze dane, wykorzystując w tym celu różne struktury, takie jak tablice, listy czy stosy. Java wspomaga programistę w realizacji tego typu zadań, oferując cały zestaw klas użytkowych mogących służyć za kontenery na dane. Rozdział 7. jest poświęcony właśnie podstawom kontenerów oraz typów uogólnionych. O ile kontenery, omówione w lekcji 35., dostępne są w Javie praktycznie od jej początków, o tyle typy uogólnione, którym poświęcona jest lekcja 36., są stosunkowo nową funkcjonalnością wprowadzoną w Java 5 (1.5). To dosyć rozległy i bardziej zaawansowany temat niż dotychczas omawiane, jednak każdy, kto poważnie myśli o programowaniu w Javie, musi znać przynajmniej jego podstawy. Lekcja 35. Kontenery Klasy kontenerowe, czyli takie, które umożliwiają tworzenie obiektów przechowujących inne obiekty, są dostępne w Javie od samego początku, choć większość z nich została wprowadzona w Java 2 (wersja 1.2), a część dopiero w Java 5 (1.5). W lekcji 35. zostaną omówione podstawy kontenerów, zobaczymy, jak można napisać własną klasę dynamicznie przechowującą dane, oraz przeanalizujemy przykłady wykorzystania dwóch klas kontenerowych dostępnych w JDK służących do utworzenia struktury dynamicznej tablicy oraz stosu. Przechowywanie wielu danych W lekcji 11. zostały przedstawione tablice, czyli struktury przechowujące dane różnych typów. Jednym z ich głównych ograniczeń była konieczność jawnej deklaracji ich wielkości. Zwykła tablica może przechowywać tylko tyle elementów, ile określono podczas jej tworzenia. W trakcie programowania często nie da się stwierdzić z góry, ile liczb czy obiektów będzie faktycznie potrzebnych, stąd konieczne są struktury danych, które pozwalają na dynamiczne zwiększanie swojej wielkości, a tym samym możliwości przechowywania danych. Jak poradzić sobie z takim problemem? Można 324 Java. Praktyczny kurs napisać własną klasę symulującą zachowanie dynamicznej tablicy. Spróbujmy wykonać to zadanie. Klasę nazwijmy np. TablicaInt — będzie ona umożliwiała przechowywanie dowolnej wartości typu int. Dostęp do danych będzie realizowany za pomocą metod get oraz set. Trzeba też ustalić, w jaki sposób przechowywać dane wewnątrz tej klasy. Użyjemy do tego zwykłej tablicy typu int. Przykładowy kod został zaprezentowany na listingu 7.1. Listing 7.1. public class TablicaInt{ private int tab[]; TablicaInt(int size){ tab = new int[size]; } public int get(int index){ if(index >= tab.length || index < 0){ throw new ArrayIndexOutOfBoundsException("index = " + index); } else{ return tab[index]; } } public void set(int index, int value){ if(index < 0){ throw new ArrayIndexOutOfBoundsException("index = " + index); } if(index >= tab.length){ resize(index + 1); } tab[index] = value; } protected void resize(int size){ int newTab[] = new int[size]; for(int i = 0; i < tab.length; i++){ newTab[i] = tab[i]; } tab = newTab; } public int size(){ return tab.length; } } Klasa zawiera jedno prywatne pole tab, któremu w konstruktorze jest przypisywana nowo utworzona tablica liczb typu int. Rozmiar tej tablicy jest określony przez argument konstruktora. Do pobierania danych służy metoda get, która przyjmuje jeden argument — index — określający indeks wartości, jaka ma zostać zwrócona. Indeks pobieranego elementu nie może być w tym przypadku większy niż całkowity rozmiar wewnętrznej tablicy pomniejszony o 1 (jak pamiętamy, elementy tablicy są indeksowane od 0) ani też mniejszy od 0. Jeśli zatem indeks znajduje się poza zakresem, generowany jest wyjątek ArrayIndexOutOfBoundsException: throw new ArrayIndexOutOfBoundsException("index = " + index); Rozdział 7. Kontenery i typy uogólnione 325 Jeśli zaś argument przekazany metodzie jest poprawny, zwrócona zostaje wartość odczytana spod wskazanego indeksu tablicy: return tab[index]; Metoda set przyjmuje dwa argumenty. Pierwszy — index — określa indeks elementu, który ma zostać zapisany, drugi — value — wartość, która ma się znaleźć pod tym indeksem. W tym przypadku na początku sprawdzamy, czy argument index jest mniejszy od 0, a jeśli tak, zgłaszamy wyjątek ArrayIndexOutOfBoundsException. Jest to jasne działanie, nie można bowiem zapisywać ujemnych indeksów (chociaż ciekawym rozwiązaniem byłoby wprowadzenie takiej możliwości, co będzie dobrym ćwiczeniem do samodzielnego wykonania). Inaczej będzie w przypadku, gdy okaże się, że indeks przekracza aktualny rozmiar tablicy. Skoro tablica ma dynamicznie zwiększać swoją wielkość w zależności od potrzeb, taka sytuacja jest w pełni poprawna. Zwiększamy wtedy tablicę, wywołując metodę resize1. Na końcu metody set przypisujemy wartość wskazaną przez value komórce określonej przez index: tab[index] = value; Pozostało więc przyjrzeć się metodzie resize. Nie wykonuje ona żadnych skomplikowanych czynności. Skoro nie można zwiększyć rozmiaru już raz utworzonej tablicy, metoda ta tworzy nową tablicę o rozmiarze wskazanym przez argument size: int newTab[] = new int[size]; Po wykonaniu tej czynności niezbędne jest oczywiście przepisanie zawartości starej tablicy do nowej, co odbywa się w pętli for, a następnie przypisanie nowej tablicy polu tab: tab = newTab; W kodzie klasy znajduje się również metoda size, która zwraca aktualny rozmiar tablicy. O tym, że nowa klasa działa prawidłowo, można się przekonać, dodając do niej metodę main, testującą obiekt typu TablicaInt. Przykładowy kod takiej metody został przed- stawiony na listingu 7.2, a efekt jego działania — na rysunku 7.1. Listing 7.2. public static void main(String args[]){ TablicaInt tab = new TablicaInt(2); tab.set(0, 1); tab.set(1, 2); tab.set(2, 3); for(int i = 0; i < tab.size(); i++){ System.out.println("tab[" + i + "] = " + tab.get(i) + " "); } tab.get(3); } 1 Warto zauważyć, że w przedstawionym przykładzie tablica jest powiększana tylko do rozmiaru wynikającego ze zwiększenia największego indeksu o 1. Przy wstawianiu dużej liczby danych do tablicy o małym rozmiarze początkowym odbije się to niekorzystnie na wydajności. Warto samodzielnie się zastanowić, jak temu zapobiec (patrz też podpunkt „Ćwiczenia do samodzielnego wykonania”). 326 Java. Praktyczny kurs Rysunek 7.1. Efekt działania kodu testującego nowy typ tablicy Na początku tworzony jest nowy obiekt klasy TablicaInt o rozmiarze dwóch elementów, a następnie trzykrotnie wywoływana jest metoda set. Pierwsze dwa wywołania ustawiają indeksy 0 i 1, zatem operują na istniejących od początku komórkach. Trzecie wywołanie powoduje ustawienie elementu o indeksie 2. Pierwotnie takiej komórki nie było w tablicy. Ponieważ jednak metoda set potrafi dynamicznie zwiększać rozmiar tablicy, również trzecia instrukcja tab.set jest wykonywana poprawnie. Zawartość obiektu tab jest następnie wyświetlana na ekranie za pomocą pętli for. Ostatnia instrukcja programu to próba pobrania przy użyciu metody get elementu o indeksie 3. Ponieważ taki element nie istnieje, instrukcja ta powoduje wygenerowanie wyjątku. Klasa TablicaInt może przechowywać tylko wartości typu int, czyli liczby całkowite. Co zrobić, gdy trzeba zapisywać wartości innych typów? Można np. napisać kolejną klasę. Co jednak w sytuacji, gdy przechowywane mają być wartości różnych typów? Jest to jak najbardziej możliwe. Jak pamiętamy, każdy typ obiektowy dziedziczy bezpośrednio lub pośrednio po klasie Object. Wiemy też już wiele o dziedziczeniu, rzutowaniu typów i polimorfizmie. A zatem wystarczy, aby nasza klasa Tablica przechowywała referencje do typu Object. Wtedy będą mogły być w niej zapisywane obiekty dowolnych typów. Spójrzmy na listing 7.3. Listing 7.3. public class Tablica { private Object tab[]; Tablica(int size){ tab = new Object[size]; } public Object get(int index){ if(index >= tab.length || index < 0){ throw new ArrayIndexOutOfBoundsException("index = " + index); } else{ return tab[index]; } } public void set(int index, Object value){ if(index < 0){ throw new ArrayIndexOutOfBoundsException("index = " + index); } Rozdział 7. Kontenery i typy uogólnione 327 if(index >= tab.length){ resize(index + 1); } tab[index] = value; } protected void resize(int size){ Object newTab[] = new Object[size]; for(int i = 0; i < tab.length; i++){ newTab[i] = tab[i]; } tab = newTab; } public int size(){ return tab.length; } public static void main(String args[]){ Tablica tab = new Tablica(2); tab.set(0, 1); tab.set(1, 2); tab.set(2, new Object()); for(int i = 0; i < tab.size(); i++){ System.out.println("tab[" + i + "] = " + tab.get(i) + " "); } tab.get(3); } } Struktura tego kodu jest bardzo podobna do przedstawionej na listingach 7.1 i 7.2, bo bardzo podobna jest też zasada działania. Metody get, set, size i resize wykonują analogiczne czynności, zmienił się natomiast typ przechowywanych danych, którym teraz jest Object. Tak więc metoda get, pobierająca dane, przyjmuje wartość typu int określającą indeks żądanego elementu i zwraca wartość typu Object, a metoda set przyjmuje dwa argumenty — pierwszy typu int określający indeks komórki do zmiany i drugi typu Object określający nową wartość komórki. Taka klasa będzie pracowała ze wszystkimi typami danych, nawet typami prostymi. Widać to wyraźnie w metodzie main. Dwa pierwsze wywołania tab.set powodują zapisanie w komórkach o indeksach 0 i 1 wartości 1 i 2. Jest to możliwe, mimo że metoda set oczekuje wartości typu Object. W takiej sytuacji następuje po prostu „zapakowanie” typu prostego w obiekt klasy opakowującej. W tym przypadku będzie to klasa Integer (każdy typ prosty ma właściwą sobie klasę opakowującą, dzięki czemu jego wartość może być używana wszędzie tam, gdzie jest oczekiwany typ Object). Trzecia metoda set powoduje umieszczenie w tablicy obiektu typu Object. Dalsze instrukcje działają tak samo jak w poprzednim przykładzie. Po kompilacji i uruchomieniu zobaczymy więc widok podobny do przedstawionego na rysunku 7.2. To dowód na to, że obiekt klasy Tablica potrafi nie tylko dynamicznie zwiększać swoją pojemność, ale też przechowywać dane różnych typów. 328 Java. Praktyczny kurs Rysunek 7.2. Obiekt klasy Tablica przechowuje dane różnych typów Klasy kontenerowe W poprzedniej części lekcji zostały pokazane proste przykłady prezentujące, w jaki sposób można tworzyć obiekty przechowujące dane, dynamicznie zwiększające swoją pojemność w miarę potrzeby. Na szczęście wcale nie trzeba tworzyć samodzielnie nowych klas tego typu — wraz z JDK w pakiecie java.util został dostarczony cały ich zestaw. Określa się je jako klasy kontenerowe lub kolekcje. Odzwierciedlają one najrozmaitsze struktury danych, takie jak tablice, tablice asocjacyjne, zbiory, stosy, kolejki, drzewa itp. Oczywiście wszystkie pozwalają na dynamiczne dodawanie i usuwanie danych bez ograniczeń wielkości. Nie ma tu niestety miejsca na dokładne omówienie wszystkich klas kontenerowych występujących w Javie, przyjrzyjmy się jednak sposobom wykorzystania dynamicznej tablicy oraz stosu. Klasa ArrayList Klasę ArrayList można potraktować jako dynamiczny wektor elementów (dynamiczną tablicę)2. Wybrane metody tej klasy zostały przedstawione w tabeli 7.1. Pewne zdziwienie może budzić użycie w przypadku niektórych metod nazwy E jako typu danych. Należy go jednak na razie potraktować jako dowolny typ obiektowy; kwestia ta zostanie dokładniej wyjaśniona w kolejnej lekcji. Oprócz wybranych metod do dyspozycji są także konstruktory: ArrayList(), ArrayList(int initialCapacity). Pierwszy z nich tworzy listę o pierwotnej wielkości równej dziesięciu elementom, drugi pozwala na samodzielne ustalenie tej wartości3. Jeśli więc wiadomo, że lista będzie przechowywała dużo większą liczbę elementów, należy skorzystać z drugiej wersji konstruktora, aby uniknąć zbędnych operacji powiększania pojemności w trakcie dodawania danych. 2 W rzeczywistości jest to implementacja interfejsu List, która wewnętrznie do przechowywania danych wykorzystuje tablicę, w odróżnieniu np. od LinkedList, gdzie do przechowywania danych jest używana lista dwukierunkowa. 3 Istnieje także trzeci konstruktor pozwalający na skopiowanie elementów z innego obiektu implementującego interfejs Collection. Rozdział 7. Kontenery i typy uogólnione 329 Tabela 7.1. Wybrane metody klasy ArrayList Deklaracja metody Opis boolean add(E e) Dodaje element na końcu listy. void add(int index, E element) Dodaje element w konkretnym miejscu listy (wyznaczonym przez argument index). void clear() Usuwa wszystkie elementy listy. boolean contains(Object o) Sprawdza, czy lista zawiera określony element. void ensureCapacity (int minCapacity) Zwiększa pojemność listy. E get(int index) Pobiera element znajdujący się na pozycji wskazywanej przez argument index. int indexOf(Object o) Zwraca indeks pierwszego wystąpienia na liście obiektu o lub –1, jeśli ten obiekt na liście nie występuje. boolean isEmpty() Zwraca true, jeśli lista nie zawiera żadnych elementów, bądź false w przeciwnym wypadku. int lastIndexOf(Object o) Zwraca indeks ostatniego wystąpienia na liście obiektu o lub –1, jeśli ten obiekt na liście nie występuje. E remove(int index) Usuwa z listy element znajdujący się na pozycji wskazanej przez argument index. boolean remove(Object o) Usuwa z listy pierwsze wystąpienie elementu o. protected void removeRange (int fromIndex, int toIndex) Usuwa z listy elementy z zakresu od fromIndex (włącznie) do toIndex (wyłącznie). E set(int index, E element) Zamienia element znajdujący się na pozycji index na określony przez argument element. int size() Zwraca aktualną liczbę elementów listy. Object[] toArray() Zwraca wszystkie elementy listy w postaci tablicy typu Object. Na listingu 7.4 został zaprezentowany prosty program, który wykorzystuje obiekt Array List do przechowywania danych wprowadzanych przez użytkownika z klawiatury, a następnie wyświetla je na ekranie. Listing 7.4. import java.io.*; import java.util.*; public class Main { public static void main(String args[]) { ArrayList list = new ArrayList(); BufferedReader brIn = new BufferedReader( new InputStreamReader(System.in) ); System.out.println("Wprowadzaj linie tekstu. Aby zakończyć, wpisz quit."); String line = ""; try{ while(!"quit".equals(line)){ line = brIn.readLine(); 330 Java. Praktyczny kurs list.add(line); } System.out.println("Koniec wczytywania danych."); } catch(IOException e){ System.out.println("Błąd podczas odczytu strumienia."); } System.out.println("Wprowadzone zostały następujące dane: "); int size = list.size(); for(int i = 0; i < size; i++){ line = list.get(i).toString(); System.out.println("[" + i + "] = " + line); } } } Najpierw za pomocą pierwszego typu konstruktora tworzony jest obiekt list klasy ArrayList, a następnie obiekt brIn klasy BufferedReader służący do odczytywania danych ze standardowego strumienia wejściowego. Pobieranie danych odbywa się tak samo jak w przykładach z lekcji 31. — przy użyciu pętli while. Każda odczytana linia jest dodawana do listy za pomocą metody add: list.add(line); Proces ten kończy się wraz z wprowadzeniem z klawiatury ciągu znaków quit (ten ciąg również jest dodawany do listy). Zebrane dane są następnie ponownie wyświetlane na ekranie. Odpowiada za to pętla for. Liczba wprowadzonych wierszy tekstu, czyli liczba elementów listy list, jest pobierana za pomocą wywołania metody size i zapisywana w zmiennej size. Ta właśnie zmienna jest wykorzystywana w wyrażeniu warunkowym pętli. Poszczególne elementy listy (to obiekty typu String) są odczytywane przy użyciu metody get oraz konwertowane na typ String i zapisywane w pomocniczej zmiennej line: line = list.get(i).toString(); Ta zmienna jest następnie wykorzystywana jako jeden z argumentów instrukcji System. out.println. Po dokonaniu kompilacji przedstawionego programu w Java 5 (1.5) lub wyższej wersji na ekranie pojawi się informacja o potencjalnym braku kontroli typów, tak jak zostało to przedstawione na rysunku 7.3. Na razie nie trzeba się tym przejmować — ta kwestia zostanie wyjaśniona w lekcji 36. Po uruchomieniu aplikacji można się przekonać, że wszystko działa zgodnie z założeniami (rysunek 7.4), a zastosowana dynamiczna lista pozwala na wprowadzenie praktycznie dowolnej ilości danych. Rysunek 7.3. Kompilator informuje o braku kontroli typów Rozdział 7. Kontenery i typy uogólnione 331 Rysunek 7.4. Przykładowy efekt działania programu z listingu 7.4 Klasa Stack Klasa Stack to implementacja struktury danych nazywanej stosem. Funkcjonuje ona w taki sposób, że dane umieszcza się na stosie za pomocą metody push, a zdejmuje z niego przy użyciu metody pop (metody klasy Stack zostały zebrane w tabeli 7.2). Dostęp mamy jednak tylko do ostatnio dodanego elementu. Tak więc dane wprowadzone na stos w pewnej kolejności będą z niego pobierane zawsze w kolejności odwrotnej. Klasa Stack udostępnia tylko jeden bezargumentowy konstruktor, a jak w praktyce wygląda jej wykorzystanie, pokazano w kodzie z listingu 7.5. Tabela 7.2. Metody klasy Stack Deklaracja metody Opis boolean empty() Zwraca true, jeśli stos jest pusty, bądź false w przeciwnym wypadku. E peek() Zwraca obiekt znajdujący się na szczycie stosu, nie zdejmując go ze stosu. E pop() Zdejmuje obiekt ze szczytu stosu i zwraca go jako rezultat działania. E push(E item) Umieszcza obiekt na stosie. int search(Object o) Zwraca pozycję, w której obiekt o znajduje się na stosie, lub –1, kiedy obiektu na stosie nie ma. Listing 7.5. import java.io.*; import java.util.*; public class Main { public static void main(String args[]) { Stack stack = new Stack(); BufferedReader brIn = new BufferedReader( new InputStreamReader(System.in) ); System.out.println("Wprowadzaj linie tekstu. Aby zakończyć, wpisz quit."); String line = ""; try{ while(!"quit".equals(line)){ line = brIn.readLine(); stack.push(line); } System.out.println("Koniec wczytywania danych."); 332 Java. Praktyczny kurs } catch(IOException e){ System.out.println("Błąd podczas odczytu strumienia."); } System.out.println("Wprowadzono następujące dane: "); while(!stack.empty()){ line = stack.pop().toString(); System.out.println(line); } } } Kod jest pod względem struktury podobny do przedstawionego w poprzednim przykładzie. Najpierw jest tworzony obiekt, który będzie przechowywał dane — tym razem jest nim stack: Stack stack = new Stack(); a następnie obiekt brIn obsługujący strumień wejściowy. Dane wprowadzane z klawiatury są odczytywane w pętli while oraz umieszczane na stosie za pomocą metody push obiektu stack: stack.push(line); Po zakończeniu tej procedury następuje odczyt zawartości stosu — odbywa się to w pętli while. Pętla ta zakończy działanie, kiedy stos będzie pusty, co jest sprawdzane za pomocą metody empty (metoda zwraca wartość true, gdy stos jest pusty). Wewnątrz pętli jest wykonywana metoda pop, która zdejmuje obiekt ze stosu i zwraca go jako wynik swojego działania. Na rzecz tego obiektu jest następnie wywoływana metoda toString, a wynik całej operacji zostaje przypisany zmiennej line: line = stack.pop().toString(); Przykładowy efekt działania programu został zaprezentowany na rysunku 7.5. Widać więc wyraźnie, że obiekty umieszczane na stosie są z niego rzeczywiście zdejmowane w odwrotnej kolejności. Rysunek 7.5. Przykładowy efekt działania aplikacji wykorzystującej klasę Stack Rozdział 7. Kontenery i typy uogólnione 333 Przeglądanie kontenerów W przykładach zamieszczonych w ramach tej lekcji do przeglądania i odczytywania danych z obiektów klas kontenerowych (ArrayList, Stack) wykorzystywane były dotąd pętle for i while. Jak jednak wiadomo z lekcji 7., istnieje także rozszerzona wersja pętli for, zwana również pętlą foreach. Ten właśnie rodzaj pętli doskonale nadaje się do przeglądania kolekcji obiektów. Spójrzmy na listing 7.6. Listing 7.6. import java.util.*; public class Main { public static void main(String args[]) { ArrayList list = new ArrayList(); list.add(1); list.add(2); list.add(3); for(Object o: list){ System.out.println(o.toString()); } } } Powstał tu obiekt list typu ArrayList, do którego za pomocą metody add zostały dodane trzy elementy. Elementy te zostały następnie wyświetlone na ekranie przy użyciu rozszerzonej pętli for oraz instrukcji System.out.println. Jak wiadomo z lekcji 7., w każdym przebiegu tej pętli pod zmienną o będzie podstawiany kolejny element listy (ogólnie — kolejny element kolekcji obiektów). Zauważmy, że jest to bardzo wygodne w sytuacji, kiedy niezbędne okazuje się przejrzenie i odczytanie wszystkich obiektów, nie trzeba bowiem ani znać rozmiaru kolekcji (por. listing 7.4), ani też testować, czy odczytano już wszystkie elementy (por. listing 7.5). Dociekliwy programista zada jednak w tym miejscu pytanie, skąd właściwie rozszerzona pętla for „wie”, jak pobierać kolejne elementy, skoro można ją stosować do wielu różnych klas kontenerowych, a każda z tych klas może inaczej przechowywać dane. Odpowiedź jest następująca: rozszerzona pętla for potrafi przeglądać elementy każdego obiektu implementującego interfejs Iterable, a większość klas kontenerowych ten interfejs implementuje. Interfejs Iterable wymusza definiowanie tylko jednej metody — iterator. Jej zadaniem jest zwrócenie tzw. obiektu iteratora, tj. obiektu implementującego interfejs Iterator i pozwalającego na przeglądanie kolekcji. W interfejsie Iterator zdefiniowane są z kolei trzy metody przedstawione w tabeli 7.3. Powróćmy więc do klasy Tablica, której kod został napisany na początku tej lekcji (listing 7.3), i sprawmy, aby mogła korzystać z dobrodziejstw rozszerzonej pętli for. Odpowiedni kod został zaprezentowany na listingu 7.7. 334 Java. Praktyczny kurs Tabela 7.3. Metody zdefiniowane w interfejsie Iterable Deklaracja metody Opis boolean hasNext() Zwraca true, jeśli istnieje kolejny obiekt, lub false w przeciwnym wypadku. E next() Zwraca kolejny obiekt kolekcji. void remove() Usuwa ostatni obiekt zwrócony przez iterator (operacja opcjonalna). Listing 7.7. import java.util.*; public class Tablica implements Iterable{ private Object tab[]; Tablica(int size){ tab = new Object[size]; } public Object get(int index){ if(index >= tab.length || index < 0){ throw new ArrayIndexOutOfBoundsException("index = " + index); } else{ return tab[index]; } } public void set(int index, Object value){ if(index < 0){ throw new ArrayIndexOutOfBoundsException("index = " + index); } if(index >= tab.length){ resize(index + 1); } tab[index] = value; } protected void resize(int size){ Object newTab[] = new Object[size]; for(int i = 0; i < tab.length; i++){ newTab[i] = tab[i]; } tab = newTab; } public int size(){ return tab.length; } public Iterator iterator(){ return new Iterator(){ int position = 0; public boolean hasNext(){ return position < tab.length; } public Object next(){ return tab[position++]; } public void remove(){ throw new UnsupportedOperationException(); } }; } Rozdział 7. Kontenery i typy uogólnione 335 public static void main(String args[]){ Tablica tab = new Tablica(2); tab.set(0, 1); tab.set(1, 2); tab.set(2, new Object()); for(Object o: tab){ System.out.println(o); } } } Klasa Tablica implementuje teraz interfejs Iterable, a więc ma też zdefiniowaną metodę iterator. Jedynym zadaniem tej metody jest zwrócenie obiektu implementującego interfejs Iterator. W tym przypadku jest to obiekt wewnętrznej klasy anonimowej utworzonej w sposób prezentowany już na stronach tej książki (lekcja 30.). Ponieważ jest to klasa wewnętrzna, ma dostęp do składowych klasy zewnętrznej (czyli Tablica), tak więc jej metody mają dostęp do danych przechowywanych w obiekcie tab. Zadanie obiektu iteratora jest bardzo proste — ma on pozwolić na przejrzenie wszystkich elementów przechowywanych przez obiekt klasy Tablica. Dlatego też w klasie anonimowej zostało zdefiniowane pole position, które będzie zawierało indeks aktualnie przeglądanego elementu oraz wymuszone przez interfejs Iterator metody hasNext i next. Metoda hasNext ma zwrócić wartość true, jeśli istnieje kolejny element w kolekcji, lub false, jeśli więcej elementów nie ma. Tak więc wartość false będzie zwracana albo przy pierwszym wywołaniu hasNext, kiedy w kolekcji (tablicy) nie ma żadnych obiektów, albo też jeśli w kolekcji są obiekty, ale wcześniejsze wywołanie metody next zwróciło ostatni obiekt. W naszym przypadku sprawdzenie, czy został osiągnięty koniec kolekcji, odbywa się za pomocą warunku position < tab.length. Gdy jest on prawdziwy, istnieją dalsze elementy, gdy jest fałszywy — nie istnieją. Odpowiednia wartość jest zwracana przy użyciu instrukcji: return position < tab.length; W pierwszej chwili może się to wydawać nie w pełni czytelne, ale jest to odpowiednik instrukcji warunkowej if w postaci: if(position < tab.length) return true; else return false; Metoda next ma za zadanie zwrócić kolejny obiekt kolekcji, przy pierwszym wywołaniu pierwszy obiekt, przy drugim — drugi itd. W tym przypadku ma to być obiekt o indeksie wskazywanym przez pole position, jednocześnie niezbędne jest zwiększenie wartości position o 1 (tak by wskazywała kolejny element kolekcji). Wszystko to odbywa się w jednej instrukcji: return tab[position++] Metoda remove nie jest nam do niczego potrzebna i nie będzie używana, dlatego też jedyną czynnością w jej wnętrzu jest zgłoszenie wyjątku UnsupportedOperationException. 336 Java. Praktyczny kurs Pozostałe elementy klasy Main działają w taki sam sposób jak w przykładzie z listingu 7.3. Z kolei metoda main testuje zachowanie obiektów wraz z pętlą foreach, dzięki czemu można się przekonać, że implementacja interfejsu Iterable zakończyła się sukcesem. Ćwiczenia do samodzielnego wykonania Ćwiczenie 35.1. Zmodyfikuj kod z ćwiczenia 7.1 tak, aby podczas wstawiania dużej liczby elementów przy małym początkowym rozmiarze tablicy nie występowało niekorzystne zjawisko bardzo częstej realokacji danych. Ćwiczenie 35.2. Napisz program wypełniający obiekt klasy TablicaInt (z listingu 7.1) liczbami od 1 do 1000, wykorzystujący do tego celu pętlę for. Początkowym rozmiarem tablicy ma być 1. Pętla ma mieć taką postać, aby nastąpiła co najwyżej jedna realokacja danych. Ćwiczenie 35.3. Zmodyfikuj program z listingu 7.5 w taki sposób, by dane były przechowywane w obiekcie klasy Stack, ale zostały wyświetlone w takiej samej kolejności, w jakiej zostały wprowadzone. Ćwiczenie 35.4. Zmodyfikuj implementację interfejsu Iterable z listingu 7.7 w taki sposób, aby zastosowana rozszerzona pętla for wyświetlała dane w odwrotnej kolejności (tzn. od ostatniego elementu do pierwszego). Ćwiczenie 35.5. Napisz program wczytujący z konsoli wiersze tekstu wprowadzane przez użytkownika. Odczytane dane powinny zostać następnie wyświetlone w następującej kolejności: 1. teksty reprezentujące liczby całkowite, 2. teksty reprezentujące liczby rzeczywiste, 3. pozostałe teksty. Lekcja 36. Typy uogólnione Typy uogólnione pojawiły się w Javie dopiero w wersji 5 (1.5) i była to bardzo duża zmiana w stosunku do poprzednich wersji tego języka. Mówimy o nich także jako o typach ogólnych lub generycznych, co pochodzi od angielskiej nazwy generics. Wszystkie trzy określenia funkcjonują zarówno w literaturze, jak i w mowie potocznej i wszystkie mają swoich zwolenników i przeciwników. W książce będzie stosowany termin typy uogólnione. W skrócie można powiedzieć, że pozwalają one na konstruowanie kodu, który operuje nie na konkretnych typach danych (konkretnych klasach czy interfejsach), ale na typach nieokreślonych, ogólnych. To rozległy temat, którego omówienie Rozdział 7. Kontenery i typy uogólnione 337 wykracza poza ramy tematyczne niniejszej publikacji, ponieważ jednak każdy programista musi znać przynajmniej podstawy tego zagadnienia, lekcja 36. została poświęcona niezbędnym podstawom. Problem kontroli typów Powróćmy do przykładu z klasą Tablica zaprezentowanego w poprzedniej lekcji (listing 7.3). Była to implementacja tablicy dynamicznej, w przypadku której nie trzeba było troszczyć się o rozmiar, gdyż był on zwiększany automatycznie w miarę dodawania nowych danych. Co więcej, było to rozwiązanie uniwersalne, gdyż klasa ta pozwalała na przechowywanie danych dowolnych typów. Można powiedzieć: pełna wygoda. Niestety ta uniwersalność i wygoda niosą też ze sobą pewne zagrożenia. Aby to sprawdzić, dopiszmy do przedstawionego na listingu 7.3 przykładu dwie dodatkowe klasy pakietowe, Triangle i Rectangle, oraz nową metodę main, tak jak jest to widoczne na listingu 7.8. Listing 7.8. public class Tablica { /*tutaj treść klasy Tablica z listingu 7.3*/ public static void main(String args[]){ Tablica rectangles = new Tablica(3); rectangles.set(0, new Rectangle()); rectangles.set(1, new Rectangle()); rectangles.set(2, new Triangle()); for(int i = 0; i < tab.size(); i++){ ((Rectangle) rectangles.get(i)).diagonal(); } } } class Triangle{}; class Rectangle{ public void diagonal(){} } W metodzie main został utworzony obiekt rectangles typu Tablica, którego zadaniem, jak można się domyślić po samej nazwie, jest przechowywanie obiektów klasy Rectangle (prostokąt). Za pomocą metody set zostały do niego dodane trzy elementy. Następnie w pętli for została wywołana metoda diagonal każdego z pobranych obiektów. Ponieważ metoda get zwraca obiekt typu Object, przed wywołaniem metody set niezbędne było dokonanie rzutowania na typ Rectangle. Wszystko działałoby oczywiście prawidłowo, gdyby nie to, że trzecia instrukcja set spowodowała umieszczenie w obiekcie rectangles obiektu typu Triangle, który nie ma metody diagonal (trójkąt, ang. triangle, nie ma przekątnej, ang. diagonal). Kompilator nie ma jednak żadnej możliwości wychwycenia takiego błędu — skoro klasa Tablica może przechowywać dowolne typy obiektowe, to typem drugiego argumentu metody set jest Object. Zawsze więc będzie wykonywane rzutowanie na typ Object (czyli formalnie trzecia instrukcja set jest traktowana jako rectangles.set(2, (Object) new Triangle())). Błąd ujawni się zatem dopiero w trakcie wykonywania programu: przy próbie rzutowania trzeciego elementu pobranego z kontenera rectangles na typ Rectangle zostanie zgłoszony wyjątek ClassCastException, tak jak jest to widoczne na rysunku 7.6. 338 Java. Praktyczny kurs Rysunek 7.6. Podczas wykonywania programu został zgłoszony wyjątek ClassCastException Przedstawiony problem dotyczy nie tylko tej wersji klasy Tablica. Jeśli w podobny sposób wykorzystamy kontener ArrayList, efekt będzie taki sam. Spójrzmy na listing 7.9. W metodzie main została utworzona lista typu ArrayList, do której zostały dodane dwa obiekty, pierwszy klasy Rectangle i drugi klasy Triangle. Następnie w pętli typu foreach została wywołana metoda diagonal, oczywiście po wcześniejszym rzutowaniu każdego pobranego z kontenera obiektu. Po uruchomieniu efekt będzie taki sam jak w poprzednim przykładzie — wystąpi wyjątek ClassCastException. Listing 7.9. import java.util.*; public class Main { public static void main(String args[]){ ArrayList list = new ArrayList(); list.add(new Rectangle()); list.add(new Triangle()); for(Object o: list){ ((Rectangle)o).diagonal(); } } } class Triangle{}; class Rectangle{ public void diagonal(){} } Oczywiście można powiedzieć, że sami jesteśmy sobie winni. Skoro bowiem nieuważnie wrzuciliśmy do kontenera obiekt niewłaściwego typu, to nie możemy mieć pretensji, że aplikacja nie działa. Jak zapobiegać takim problemom? Pomysłów może być kilka, od zupełnie fatalnych, jak np. pisanie osobnych klas kontenerowych dla każdego typu danych, po takie jak sprawdzanie pobieranego typu danych w trakcie wykonania programu, co sprawdzi się w praktyce, choć będzie wymagało zarówno większej pracy programisty, jak i większego narzutu związanego z wykonywaniem dodatkowych instrukcji weryfikujących. Najwygodniejsze byłoby rozwiązanie polegające na możliwości przechowywania każdego typu danych, a przy tym przerzucenie części zadań związanych z kontrolą typów na kompilator. Jest to możliwe dzięki typom uogólnionym. Zwróćmy uwagę, że kompilacja programu z listingu 7.9 powoduje wyświetlenie przez kompilator ostrzeżenia, takiego samego jak to, które pojawiło się w poprzedniej lekcji (rysunek 7.3). Dzieje się tak dlatego, że począwszy od Java 5 (Java 1.5), kontenery korzystają z dobrodziejstw typów uogólnionych. Wyświetlona informacja mówi właśnie o tym, Rozdział 7. Kontenery i typy uogólnione 339 że używamy kontenera w sposób klasyczny, nie wykorzystując możliwości kontroli typów przez kompilator, i wykonujemy przez to potencjalnie niebezpieczną operację, która może wygenerować błąd w programie wynikowym. Po skorzystaniu z opcji Xlint:unchecked, czyli po wykonaniu kompilacji za pomocą polecenia: javac -Xlint:unchecked Main.java kompilator wskaże miejsca w kodzie, które „uzna” za podejrzane. Jest to widoczne na rysunku 7.7. Jak więc korzystać z kontenerów? Odpowiedź na to pytanie znajdzie się w kolejnym podpunkcie tej lekcji. Rysunek 7.7. Kompilator wskazuje podejrzane miejsca w programie Jak korzystać z kontenerów? Kontener może przechowywać obiekty dowolnego typu danych, ale możemy zadeklarować, jaki typ będzie używany w konkretnym przypadku. Jeśli więc w dynamicznej liście typu ArrayList zechcemy przechowywać obiekty typu Rectangle, możemy o tym poinformować kompilator. Nie dopuści on wtedy do jawnego umieszczenia w kontenerze obiektu innego typu (chyba że byłby to obiekt pochodny od Rectangle). W tym celu stosuje się składnię z nawiasami kątowymi, tak jak zostało to zilustrowane na listingu 7.10. Listing 7.10. import java.util.*; public class Main { public static void main(String args[]){ ArrayList<Rectangle> list = new ArrayList<Rectangle>(); list.add(new Rectangle()); //list.add(new Triangle()); for(Rectangle o: list){ o.diagonal(); } list.get(0).diagonal(); } } 340 Java. Praktyczny kurs class Triangle{}; class Rectangle{ public void diagonal(){} } Tym razem przy tworzeniu zmiennej list oraz obiektu klasy ArrayList zostało zaznaczone, że będzie on przechowywał obiekty klasy Rectangle. Typ Rectangle został umieszczony w nawiasie kątowym, zarówno w deklaracji zmiennej list: ArrayList<Rectangle> list jak i instrukcji tworzącej sam obiekt: new ArrayList<Rectangle>() Tym samym instrukcja: list.add(new Rectangle()); może być wykonana bez problemu, ale już ujęta w komentarz: list.add(new Triangle()); spowodowałaby błąd kompilacji widoczny na rysunku 7.8 (w wersjach Javy starszych niż 7 komunikat będzie dużo mniej szczegółowy; w Java 7 i starszych nie będzie również informacji o opcji -Xdiags). Tym razem kompilator jest bowiem w stanie stwierdzić, że nastąpiła próba umieszczenia w kontenerze nieprawidłowego typu danych. Warto też zwrócić uwagę na pętlę for, w której nie ma potrzeby wykonywania rzutowań, ponieważ typ danych jest jasno określony. Tak samo dzieje się w przypadku instrukcji: list.get(0).diagonal(); Rysunek 7.8. Kompilator wykrył próbę użycia niewłaściwego typu danych Rozdział 7. Kontenery i typy uogólnione 341 Można bezpośrednio wywołać metodę diagonal bez potrzeby rzutowania na typ Rectangle, ponieważ po odpowiedniej deklaracji kontenera metoda get będzie zwracała obiekty typu Rectangle. W Java 8 wprowadzono możliwość zastosowania składni uproszczonej związanej z typami uogólnionymi. Można zauważyć, że przy tworzeniu obiektu ArrayList oraz zmiennej referencyjnej trzeba było dwukrotnie użyć zapisu <Rectangle>. Raz przy określaniu typu zmiennej (list) i drugi raz przy użyciu operatora new. To zapis nadmiarowy, łatwo można wywnioskować, że w obu przypadkach chodzi o typ Rectangle. Zatem począwszy od Java 8, poprawny będzie również zapis ArrayList<Rectangle> list = new ArrayList<>(); Znaczenie jest takie samo jak przy zapisie z listingu 7.10. W tej sytuacji mówimy o dookreślaniu typu lub inferowaniu typu (ang. type inference). Stosowanie uogólnień W poprzednim podpunkcie zostało pokazane, w jaki sposób używać klas korzystających z uogólnień, ale trzeba również wiedzieć, jak taką klasę napisać. Zacznijmy jednak od napisania kodu klasy, która mogłaby przechowywać obiekty dowolnego typu. Można ją traktować jako opakowanie, dlatego też nazwiemy ją po prostu Opakowanie. Kod jest widoczny na listingu 7.11. Listing 7.11. public class Opakowanie { private Object data; Opakowanie(Object value) { set(value); } public Object get() { return data; } public void set(Object value) { this.data = value; } public static void main(String args[]){ Opakowanie opk1 = new Opakowanie(new Rectangle()); Opakowanie opk2 = new Opakowanie(new Triangle()); ((Rectangle) opk1.get()).diagonal(); ((Rectangle) opk2.get()).diagonal(); } } class Triangle{}; class Rectangle{ public void diagonal(){} } Dane przechowywane są w prywatnym polu data, którego typem jest Object. Pobieraniem danych zajmuje się metoda get, a ustawianiem — set. Metoda set przyjmuje jeden argument typu Object i przypisuje jego wartość polu data, natomiast metoda 342 Java. Praktyczny kurs get jest bezargumentowa i zwraca wartość typu Object. Oprócz tego do dyspozycji mamy również typowy publiczny konstruktor, a także znane nam już dobrze dwie pakietowe klasy pomocnicze, Triangle i Rectangle. W wersjach Javy przed 5 opisane rozwiązanie było typowym sposobem pozwalającym na przechowywanie danych dowolnych typów. Dobrze znamy już jego wady — zostały zilustrowane w kodzie metody main. Powstały w niej dwa obiekty klasy Opakowanie: opk1 i opk2. Do obiektu opk1 został wstawiony nowy obiekt typu Rectangle, a do opk2 — typu Triangle. Ponieważ konstruktor (podobnie jak metoda set) przyjmuje argument typu Object, w obu przypadkach zostało wykonane niejawne rzutowanie. A zatem instrukcje te zostały potraktowane jako: Opakowanie opk1 = new Opakowanie((Object)new Rectangle()); Opakowanie opk2 = new Opakowanie((Object)new Triangle()); Było to rzutowanie w górę, które może być wykonywane automatycznie (temat ten był omawiany w lekcji 23.). W dalszej części programu z obiektów opk1 i opk2 zostały pobrane przechowywane przez nie dane. Ze względu na to, że metoda get zwraca wartości typu Object, w obu przypadkach niezbędne było wykonanie rzutowania w dół — do typu Rectangle. Tylko dzięki temu można było wywołać metodę diagonal. Oczywiście złożoną instrukcję: ((Rectangle) opk1.get()).diagonal(); można byłoby rozbić na dwie, np.: Rectangle rect1 = (Rectangle) opk1.get(); rect1.diagonal(); wydaje się jednak, że na tym etapie nauki Javy nie ma już takiej potrzeby. Rzecz jasna wiemy już dobrze, że kompilacja takiego kodu uda się bez problemów, natomiast w trakcie wykonania pojawi się wyjątek ClassCastException, jako że w opk2 został zapisany obiekt typu Triangle, który nie może być rzutowany na typ Rectangle, a tym bardziej nie może zostać wykonana metoda diagonal. Skorzystajmy zatem z dobrodziejstw typów uogólnionych. Koncepcja jest następująca: nasza klasa wciąż ma mieć możliwość przechowywania dowolnego typu danych, ale podczas korzystania z niej chcemy mieć możliwość zadecydowania, jaki typ zostanie użyty w konkretnym przypadku. Jak to zrobić? Po pierwsze, trzeba zaznaczyć, że klasa będzie korzystała z typów uogólnionych, po drugie, trzeba zastąpić konkretny typ danych (w tym przypadku typ Object) typem uogólnionym (ogólnym). Tak więc za nazwą klasy w nawiasie kątowym umieszczamy określenie typu uogólnionego, a następnie używamy tego określenia wewnątrz klasy, zastępując nim typ konkretny. Zapewne brzmi to nieco zawile, spójrzmy zatem od razu na listing 7.12. Listing 7.12. public class Opakowanie<T> { private T data; Opakowanie(T value) { set(value); } Rozdział 7. Kontenery i typy uogólnione 343 public T get() { return data; } public void set(T value) { this.data = value; } } Za nazwą klasy w nawiasie kątowym pojawił się symbol T. To informacja, że klasa będzie korzystała z typów uogólnionych oraz że symbolem typu uogólnionego jest T (można ten symbol zmienić na dowolny inny, niekoniecznie jednoliterowy). Ten symbol został umieszczony w każdym miejscu kodu klasy, gdzie wcześniej występował konkretny typ przechowywanych danych (był to typ Object). Tak więc instrukcja private T data; oznacza, że w klasie znajduje się prywatne pole, którego typ zostanie określony dopiero w kodzie aplikacji przy tworzeniu obiektu klasy Opakowanie. Deklaracja: public T get() { oznacza metodę get zwracającą typ danych, który zostanie dokładniej określony przy tworzeniu obiektu klasy Opakowanie itd. Każde wystąpienie T to deklaracja, że właściwy typ danych będzie określony później. Dzięki temu powstała uniwersalna klasa, która może przechowywać dowolne dane, ale która pozwala na kontrolę typów już w momencie kompilacji. Sprawdźmy to, dopisując do kodu metodę main oraz klasy Triangle i Rectangle, tak jak jest to widoczne na listingu 7.13. Listing 7.13. public class Opakowanie<T> { /*tutaj początek kodu klasy Opakowanie z listingu 7.12*/ public static void main(String args[]){ Opakowanie<Rectangle> opk1 = new Opakowanie<Rectangle>( new Rectangle() ); /*Opakowanie<Rectangle> opk2 = new Opakowanie<Rectangle>( new Triangle() );*/ Opakowanie<Triangle> opk3 = new Opakowanie<Triangle>( new Triangle() ); opk1.get().diagonal(); //opk2.get().diagonal(); opk3.get().type(); } } class Triangle{ public void type(){} } class Rectangle{ public void diagonal(){} } 344 Java. Praktyczny kurs Możliwości klasy Opakowanie są testowane w metodzie main. Pierwsza instrukcja to utworzenie zmiennej opk1 oraz przypisanie jej nowego obiektu. Ponieważ w tym obiekcie chcemy przechowywać obiekty klasy Rectangle, zaznaczamy to, umieszczając tę nazwę w nawiasie kątowym za nazwą klasy Opakowanie. W Javie do wersji 8 należy to zrobić zarówno przy deklaracji zmiennej: Opakowanie<Rectangle> opk1 jak i w wywołaniu konstruktora: new Opakowanie<Rectangle>(new Rectangle()); (na listingu to wywołanie zostało rozbite na trzy wiersze). Począwszy od Java 8, wystarczy typ przechowywanego obiektu zaznaczyć tylko przy deklaracji zmiennej. Dzięki temu kompilator „wie”, że obiekt opk1 ma przechowywać obiekty klasy Rectangle (lub klas pochodnych od Rectangle). Bez problemu może więc zostać wykonana instrukcja: opk1.get().diagonal(); jako że metoda get zwraca w tym przypadku dane typu Rectangle, a obiekt takiego typu zawiera metodę diagonal. Instrukcja tworząca obiekt opk2: Opakowanie<Rectangle> opk2 = new Opakowanie<Rectangle>(new Triangle()); została ujęta w komentarz, gdyż jest nieprawidłowa i spowodowałaby błąd kompilacji, który widać na rysunku 7.9. Błąd jest chyba dobrze widoczny — otóż kompilator otrzymał informację, że obiekt opk2 będzie współpracował z danymi typu Rectangle, tymczasem jako argument konstruktora został użyty obiekt klasy Triangle. Ponieważ klasy Rectangle i Triangle nie mają ze sobą nic wspólnego (oprócz tego, że obie dziedziczą po Object), taka instrukcja nie może być wykonana i kompilator protestuje. Z tych samych względów nie może być również wykonana instrukcja: opk2.get().diagonal(); Rysunek 7.9. Próba zastosowania niewłaściwego typu kończy się błędem kompilacji Jeśli więc chcemy, aby obiekt klasy Opakowanie przechowywał obiekty klasy Triangle, zaznaczamy to przy ich tworzeniu. Zostało to pokazane w instrukcji: Opakowanie<Triangle> opk3 = new Opakowanie<Triangle>(new Triangle()); Oznacza ona, że obiekt opk3 będzie współpracował z klasą Triangle. Prawidłowa jest też zatem instrukcja: opk3.get().type(); Rozdział 7. Kontenery i typy uogólnione 345 jako że w tym przypadku metoda get zwróci wartość typu Triangle, a w klasie Triangle istnieje metoda type. Deklaracja klasy jako stosującej uogólnienie nie wyklucza jednak wykorzystywania jej w sposób klasyczny. Taka sytuacja pojawiła się przecież w przykładach z klasami kontenerowymi w lekcji 35. Ta zasada dotyczy także naszych własnych klas, takich jak Opakowanie. Przykładowy obiekt opk4 można więc utworzyć w sposób następujący: Opakowanie opk4 = new Opakowanie( new Triangle() ); Kompilator wygeneruje wtedy takie samo ostrzeżenie jak przedstawione wcześniej na rysunku 7.3, jednak kod uda się bez problemu skompilować. Oczywiście w takim przypadku przy dostępie do obiektów przechowywanych w klasie Opakowanie niezbędne będzie dokonywanie rzutowania, np.: ((Triangle)opk4.get()).type(); i wystąpią wszelkie problemy związane z konwersją typów, opisane na poprzednich stronach. Uogólnianie metod Z uogólnień typów mogą korzystać nie tylko klasy, ale także poszczególne metody w ramach tych klas. Uogólnianie metod jest jednak niezależne od uogólnień klas, więc w klasie uogólnionej mogą się znajdować nieuogólnione metody, a także uogólnione metody mogą się znajdować w zwykłych klasach. Jeśli metoda ma operować na argumencie typu uogólnionego, to specyfikację tego typu należy umieścić przed typem zwracanym przez metodę. Wygląda to tak: modyfikator_dostępu <specyfikacja_typu_ogólnego> typ_zwracany nazwa_metody(argumenty) Prosty przykład znajduje się na listingu 7.14. Listing 7.14. public class Main { public <T> void show (T value){ System.out.println(value.toString()); } public static void main(String args[]){ Main main = new Main(); main.show(1); main.show(0.54); main.show("Przykładowy tekst"); main.show(new Main()); } } Jest to klasa Main zawierająca metodę show. Za modyfikatorem dostępu tej metody (public), a przed typem zwracanym (void) znajduje się deklaracja typu uogólnionego (<T>). Argumentem metody show jest value o typie T. Tak więc w praktyce będzie to 346 Java. Praktyczny kurs mógł być typ dowolny. Pokazuje to metoda main, w której metoda show została wywołana z różnymi argumentami. W pierwszym przypadku jest to argument typu int, w drugim — double, w trzecim — String, a w czwartym — nowy obiekt klasy Main. Ponieważ zadaniem metody show jest konwersja przekazanej jej wartości (przez wywołanie metody toString) na typ String i wyświetlenie jej na ekranie, po uruchomieniu aplikacji zobaczymy widok przedstawiony na rysunku 7.10. Rysunek 7.10. Efekt działania programu z listingu 7.14 Jak zwrócić wiele wartości? Zwykła metoda może zwracać tylko jedną wartość. Jeśli ma być ich więcej, trzeba zastosować dodatkowy obiekt-kontener przechowujący dane. Można użyć którejś z klas kontenerowych, można także napisać własną. Mogłaby ona wyglądać tak jak klasa Opakowanie z przykładu widocznego na listingu 7.15. Listing 7.15. public class Main { public Opakowanie getValues(){ return new Opakowanie(new Triangle(), new Rectangle()); } public static void main(String args[]){ Main main = new Main(); Opakowanie opk = main.getValues(); ((Triangle)opk.pole1).type(); ((Rectangle)opk.pole2).diagonal(); } } class Opakowanie{ Object pole1; Object pole2; Opakowanie(Object pole1, Object pole2){ this.pole1 = pole1; this.pole2 = pole2; } } class Triangle{ public void type(){} } class Rectangle{ public void diagonal(){} } Rozdział 7. Kontenery i typy uogólnione 347 Klasa Opakowanie zawiera dwa pola typu Object, dzięki temu może przechowywać obiekty dowolnych typów. Zawiera także konstruktor pozwalający na inicjalizację obu pól. Z tych właściwości korzysta metoda getValues z klasy Main. Zadaniem tej metody jest zwrócenie dwóch wartości typów Triangle i Rectangle. Odbywa się to przez utworzenie nowego obiektu klasy Opakowanie i zwrócenie go za pomocą instrukcji return: new Opakowanie(new Triangle(), new Rectangle()); Działanie kodu jest testowane w metodzie main, w której najpierw jest tworzony obiekt klasy Main: Main main = new Main(); Następnie jest wywoływana metoda getValues, a wynik jej działania jest przypisywany zmiennej opk: Opakowanie opk = main.getValues(); Ponieważ pole pole1 klasy Opakowanie miało być klasy Triangle, po wykonaniu rzutowania można wywołać metodę type tej klasy: ((Triangle)opk.pole1).type(); i analogicznie, ze względu na to, że pole pole2 miało być klasy Rectangle, po dokonaniu rzutowania na tę klasę można wywołać metodę diagonal: ((Rectangle)opk.pole2).diagonal(); Program da się skompilować i uruchomić, jednak dotyczą go wszystkie wady związane z brakiem kontroli typów, jakie zostały opisane już na początku tego rozdziału. Wystarczy pomyłka choćby przy rzutowaniu, a aplikacja nadal będzie się kompilowała bez problemu, jednak podczas uruchomienia zostaną zgłoszone błędy. Oczywiście można byłoby napisać specyficzną wersję klasy Opakowanie, która przechowywałaby jedynie wartości typów Rectangle i Triangle, jednak straciłoby się wtedy jej uniwersalność, a w założeniu miała ona wciąż przechowywać dowolne dane. Warto zatem wykorzystać rozwiązanie z typami uogólnionymi. Zostało ono zaprezentowane na listingu 7.16. Listing 7.16. public class Main { public Opakowanie<Triangle, Rectangle> getValues(){ return new Opakowanie<Triangle, Rectangle>( new Triangle(), new Rectangle() ); } public static void main(String args[]){ Main main = new Main(); Opakowanie<Triangle, Rectangle> opk = main.getValues(); opk.pole1.type(); opk.pole2.diagonal(); } } 348 Java. Praktyczny kurs class Opakowanie<M, N>{ M pole1; N pole2; Opakowanie(M pole1, N pole2){ this.pole1 = pole1; this.pole2 = pole2; } } class Triangle{ public void type(){} } class Rectangle{ public void diagonal(){} } Przyjrzyjmy się najpierw klasie Opakowanie. Występujący za jej nazwą nawias kątowy mówi wyraźnie, że będzie ona korzystała z typów uogólnionych. Wewnątrz nawiasu występują dwa symbole, M i N, a zatem będą to dwa typy danych. Wewnątrz klasy znajdują się dwa pola, typ pierwszego określa symbol M, a drugiego — N. Podobnie wygląda definicja konstruktora, typem pierwszego argumentu będzie ten wskazany przez M, a drugiego — przez N. Spójrzmy teraz na metodę getValues z klasy Main. Ma ona zwrócić obiekt klasy Opakowanie zawierający dane typów Triangle i Rectangle, stąd deklaracja typu zwracanego jako Opakowanie<Triangle, Rectangle>: public Opakowanie<Triangle, Rectangle> getValues(){ Podobna konstrukcja występuje wewnątrz metody przy tworzeniu nowego obiektu klasy Opakowanie: new Opakowanie<Triangle, Rectangle>(new Triangle(), new Rectangle()); jak również w metodzie main przy deklaracji zmiennej opk, której przypisywany jest wynik działania metody getValues: Opakowanie<Triangle, Rectangle> opk = main.getValues(); Ponieważ tym razem zostało dokładnie określone, jakie typy będą przechowywane przez obiekt klasy Opakowanie, nie ma potrzeby dokonywania rzutowań i można bezpośrednio wywoływać metody type oraz diagonal obiektów klas Triangle i Rectangle: opk.pole1.type(); opk.pole2.diagonal(); Kontrola typów może się teraz odbywać już na etapie kompilacji i nie traci się nic z uniwersalności klasy Opakowanie. Rozdział 7. Kontenery i typy uogólnione 349 Ćwiczenia do samodzielnego wykonania Ćwiczenie 36.1. Popraw kod pętli for z listingu 7.9 tak, aby program uruchamiał się bez błędów. Możesz użyć operatora instanceof badającego, czy obiekt jest danego typu (np. obj1 instaceof String), albo skorzystać z innego sposobu. Ćwiczenie 36.2. Zmodyfikuj kod z listingu 7.10 tak, by powstała klasa Shape bazowa dla Triangle i Rectangle. Zastosuj Shape jako typ uogólniony kontenera ArrayList. Dodaj do kontenera obiekty Triangle i Rectangle oraz zmodyfikuj pętlę for tak, aby program działał poprawnie. Ćwiczenie 36.3. Napisz klasę uogólnioną, która będzie mogła przechowywać wartości trzech różnych typów. Ćwiczenie 36.4. Do klasy Opakowanie z listingu 7.16 dopisz metody pozwalające na niezależną manipulację każdym polem (osobny zapis i odczyt wartości dla każdego pola). Ćwiczenie 36.5. Zmodyfikuj kod klasy Tablica z listingu 7.3 (lekcja 35.) tak, aby korzystała ona ze składni typów uogólnionych. Uwaga: nie możesz utworzyć generycznej tablicy, zapis typu new T[] jest nieprawidłowy. Zastanów się, czy tworzenie tego typu klasy ma sens. Ćwiczenie 36.6. Popraw kod z ćwiczenia 35.5 (z lekcji 35.) tak, by korzystał z typów uogólnionych i żeby w związku z tym kompilator nie generował ostrzeżeń. 350 Java. Praktyczny kurs Rozdział 8. Aplikacje i aplety Programy w Javie mogą być apletami (ang. applet) lub aplikacjami (ang. application). Różnica jest taka, że aplikacja jest programem samodzielnym uruchamianym z poziomu systemu operacyjnego (a dokładniej: maszyny wirtualnej Javy operującej na poziomie systemu operacyjnego) i tym samym ma pełny dostęp do zasobów udostępnianych przez system, natomiast aplet jest uruchamiany pod kontrolą innego programu, najczęściej przeglądarki internetowej, i ma dostęp jedynie do środowiska, które ten program mu udostępni. Aplet to inaczej program zagnieżdżony w innej aplikacji. Zazwyczaj nie ma on dostępu np. do dysku twardego (i innych urządzeń), tak aby ściągnięty nieświadomie z internetu nie mógł zaszkodzić użytkownikowi, np. kasując dane, chyba że zostanie wyposażony w specjalny podpis cyfrowy i użytkownik zgodzi się na udostępnienie mu chronionych zasobów. Osobną kwestią są wykrywane co jakiś czas błędy w mechanizmach bezpieczeństwa przeglądarek i maszyn wirtualnych, które niekiedy powodują, że złośliwie napisane aplety mogą obejść restrykcje i dostać się do chronionych zasobów systemu. Nie są to jednak sytuacje bardzo częste. Obecnie aplety straciły na znaczeniu i nie są już tak często używane, jednak są wciąż powszechne np. na stronach prezentujących notowania giełdowe, oferujących narzędzia do analizy technicznej związanej z grą na giełdach papierów wartościowych, a także w aplikacjach typu czat czy też w niektórych grach działających w przeglądarkach WWW. W rzeczywistości różnice w konstrukcji apletu i aplikacji nie są bardzo duże, jednak rozdział ten dla porządku został podzielony na dwie części. W pierwszej zostaną omówione aplety działające pod kontrolą przeglądarek, a w drugiej — samodzielne aplikacje. Aplety Lekcja 37. Podstawy apletów Wszystkie przykłady programów prezentowane w poprzednich lekcjach pracowały w trybie tekstowym, najwyższy czas zatem przejść w świat aplikacji pracujących w trybie graficznym. W lekcji 37. rozpoczynamy omawianie apletów, czyli niewielkich aplikacji 352 Java. Praktyczny kurs uruchamianych pod kontrolą przeglądarek internetowych. Zostanie w niej przedstawione m.in. to, jak wygląda ogólna konstrukcja apletu, w jaki sposób jest on wywoływany przez przeglądarkę oraz jak przekazać mu parametry, które mogą sterować sposobem jego wykonania. Pierwszy aplet Utworzenie apletu wymaga użycia klasy Applet lub, jeśli miałyby się w nim znaleźć komponenty pakietu (biblioteki) Swing, JApplet. Co prawda w pierwszych przykładach nie będzie wykorzystywany ten pakiet, jednak aplety zostaną wyprowadzone z nowocześniejszej klasy JApplet. Komponenty Swing zostaną natomiast przedstawione w lekcji 42. Przed dokładnym omówieniem budowy tego typu programów warto na rozgrzewkę napisać prosty aplet, który, jakżeby inaczej, będzie wyświetlał na ekranie dowolny napis. Kod takiego apletu jest widoczny na listingu 8.1. Listing 8.1. import javax.swing.JApplet; import java.awt.*; public class PierwszyAplet extends JApplet { public void paint (Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.drawString ("Pierwszy aplet", 100, 50); } } Na początku importujemy klasę JApplet z pakietu javax.swing oraz pakiet java.awt. Pakiet java.awt jest potrzebny, gdyż zawiera definicję klasy Graphics, dzięki której można wykonywać operacje graficzne. Aby utworzyć własny aplet, trzeba wyprowadzić klasę pochodną od JApplet, nasza klasa nazywa się po prostu PierwszyAplet. Co dalej? Otóż aplet będzie wyświetlany w przeglądarce i zajmie pewną część jej okna. By na powierzchni apletu wyświetlić napis, trzeba więc w jakiś sposób uzyskać do niej dostęp. Umożliwia to metoda paint. Jest ona automatycznie wywoływana za każdym razem, kiedy zaistnieje konieczność odrysowania (odświeżenia) powierzchni apletu. Metoda ta otrzymuje w argumencie wskaźnik do specjalnego obiektu udostępniającego metody pozwalające na wykonywanie operacji na powierzchni apletu. Wywołujemy więc metodę clearRect, która wyczyści obszar okna apletu1, a następnie drawString rysującą napis we wskazanych współrzędnych. W powyższym przypadku będzie to napis Pierwszy aplet we współrzędnych x = 100 i y = 50. Metoda clearRect przyjmuje cztery argumenty określające prostokąt, który ma być wypełniony kolorem tła. Dwa pierwsze określają współrzędne lewego górnego rogu, a dwa kolejne — szerokość i wysokość. Szerokość jest uzyskiwana przez wywołanie getSize().width2, a wysokość — getSize().height. 1 W praktyce przed wywołaniem clearRect należałoby użyć metody setColor (patrz lekcja „Kroje pisma (fonty) i kolory”), tak aby na każdej platformie uruchomieniowej uzyskać taki sam kolor tła. W różnych systemach domyślny kolor tła może być bowiem inny. 2 Jest to więc odwołanie do pola width obiektu zwróconego przez wywołanie metody getSize. Jest to obiekt klasy Dimension. Rozdział 8. Aplikacje i aplety 353 Przedstawiony kod należy skompilować w standardowy sposób, aby uzyskać plik PierwszyAplet.class. Do techniki rysowania na powierzchni apletu wrócimy już niebawem, teraz zajmiemy się umieszczeniem go w przeglądarce. W tym celu trzeba napisać fragment kodu HTML i umieścić w nim odpowiedni znacznik. Znacznikiem historycznym służącym do umieszczania apletów był <applet>, w postaci: <applet code = "nazwa_klasy" width = "szerokość apletu" height = "wysokość apletu" > </applet> Wciąż jest on rozpoznawany i używany, choć nie należy do standardu HTML3. Obecnie zamiast niego stosuje się znacznik <object>. Niestety niektóre przeglądarki (w tym dostępny w JDK program appletviewer) z reguły wymagają używania atrybutu code, który w przypadku <object> jest… niestandardowy (prawidłowy jest atrybut data). Aby zatem napisać kod zgodny ze standardami, i tak trzeba użyć rozwiązań niezgodnych formalnie ze standardem HTML. Z reguły stosuje się znacznik <object> w postaci podobnej do <applet>: <object type="application/x-java-applet" code = "nazwa klasy" width = "szerokość apletu" height = "wysokość apletu"> </object> W przypadku naszego pierwszego apletu miałby on postać widoczną na listingu 8.2. Listing 8.2. <object type="application/x-java-applet" code="PierwszyAplet.class" width="320" height="200"> </object> Ten znacznik należy umieścić w kodzie strony HTML4. Taka przykładowa strona5 została przedstawiona na listingu 8.36. 3 W niektórych wersjach JDK zawarte jest narzędzie o nazwie HtmlConverter służące do konwersji kodu HTML, które potrafi automatycznie przetworzyć przestarzały kod ze znacznikami <applet> na kod zgodny ze współczesnymi standardami. 4 Znacznik <object> może również zawierać inne atrybuty niż przedstawione w kodzie, jednak nie są one istotne dla dalszych rozważań. 5 W książce nie ma miejsca na omawianie standardów HTML i sposobów tworzenia stron internetowych. Osoby zainteresowane tym tematem mogą jednak sięgnąć po publikację Tworzenie stron WWW. Praktyczny kurs (http://helion.pl/ksiazki/twspk2.htm). 354 Java. Praktyczny kurs Listing 8.3. <!DOCTYPE html> <html lang="pl"> <head> <meta charset="utf-8"> <title>Moja strona WWW</title> </head> <body> <div> <object type="application/x-java-applet" code="PierwszyAplet.class" width="320" height="200"> </object> </div> </body> </html> Tak skonstruowany plik z kodem HTML można już wczytać do przeglądarki. Pakiet JDK zawiera swoją własną przeglądarkę służącą do testowania apletów — appletviewer. Uruchamia się ją, pisząc w wierszu poleceń: appletviewer nazwa_pliku.html jeśli np. kod HTML został zapisany w pliku index.html: appletviewer index.html Wynik działania naszego apletu w tej przeglądarce jest widoczny na rysunku 8.1. Na rysunku 8.2 zaprezentowany został natomiast ten sam aplet po wczytaniu do Firefoksa (oczywiście należy wczytywać plik index.html, a nie PierwszyAplet.class)7. Należy pamiętać, aby plik ze skompilowanym apletem (PierwszyAplet.class) był umieszczony w tym samym katalogu co plik z kodem HTML (index.html), a dokładniej rzecz ujmując: w lokalizacji wskazanej przez atrybut code znacznika <object>. Rysunek 8.1. Pierwszy aplet w przeglądarce appletviewer 6 Zastosowano kod zgodny ze standardem HTML5, z tym wyjątkiem, że w celu zachowania maksymalnej kompatybilności użyty został parametr code zamiast formalnie poprawnego — data. 7 W celu uruchomienia apletu z pliku lokalnego może być konieczna zmiana systemowych ustawień zabezpieczeń, w tym dopuszczenie w konfiguracji środowiska JRE otwierania apletów dostępnych po protokole FILE. Rozdział 8. Aplikacje i aplety 355 Rysunek 8.2. Pierwszy aplet w przeglądarce Firefox Konstrukcja apletu Kiedy przeglądarka obsługująca Javę odnajdzie w kodzie HTML osadzony aplet, wykonuje określoną sekwencję czynności. Zaczyna oczywiście od sprawdzenia, czy w podanej lokalizacji znajduje się plik zawierający kod danej klasy. Jeśli tak, kod ten jest ładowany do pamięci i tworzona jest instancja (czyli obiekt) znalezionej klasy, a tym samym wywołany zostaje konstruktor. Następnie jest wykonywana metoda init utworzonego obiektu informująca go o tym, że został załadowany do systemu. W metodzie init można zatem umieścić, o ile zachodzi taka potrzeba, procedury inicjacyjne. Kiedy aplet jest gotowy do uruchomienia, przeglądarka wywołuje jego metodę start, informując, że powinien rozpocząć swoje działanie. Przy pierwszym ładowaniu apletu metoda start jest zawsze wykonywana po metodzie init. W momencie gdy aplet powinien zakończyć swoje działanie, jest z kolei wywoływana jego metoda stop. O tym, że rzeczywiście występuje taka sekwencja instrukcji, można się przekonać, uruchamiając (czy też wczytując do przeglądarki) kod z listingu 8.4. Wynik działania dla appletviewera uruchamianego z konsoli systemowej jest widoczny na rysunku 8.3. Na rysunku 8.4 został z kolei przedstawiony obraz konsoli Javy w przeglądarce korzystającej z wtyczki JRE 1.8. Listing 8.4. import javax.swing.JApplet; import java.awt.*; public class Aplet extends JApplet { public Aplet() { System.out.println("Konstruktor..."); } public void init() { System.out.println("Inicjalizacja apletu..."); } public void start() { System.out.println("Uruchomienie apletu..."); } public void stop() { System.out.println("Zatrzymanie apletu..."); } public void paint (Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.drawString ("Pierwszy aplet", 100, 50); } } 356 Java. Praktyczny kurs Rysunek 8.3. Kolejność wykonywania metod w aplecie na konsoli systemowej Rysunek 8.4. Kolejność wykonywania metod w aplecie na konsoli Java Plugin Parametry apletu W przypadku niektórych apletów pojawia się potrzeba przekazania im parametrów z kodu HTML — na przykład wartości sterujących ich pracą. Taka możliwość oczywiście istnieje, znacznik <object> (a także <applet>) pozwala na zastosowanie znaczników wewnętrznych <param>. Ogólnie konstrukcja taka będzie miała postać: <object <!--atrybuty znacznika object--> > <param name = "nazwa1" value = "wartość1"> <!--tutaj dalsze znaczniki param--> <param name = "nazwaN" value = "wartośćN"> </object> Znaczniki <param> umożliwiają właśnie przekazywanie różnych parametrów. Załóżmy przykładowo, że apletowi ma być przekazany tekst, który będzie następnie wyświetlany na jego powierzchni. Należy zatem w kodzie HTML zastosować następującą konstrukcję: <object <!--atrybuty znacznika object--> > <param name="tekst" value="Testowanie parametrów apletu"> </object> Oznacza ona, że aplet będzie miał dostęp do parametru o nazwie tekst i wartości Testowanie parametrów apletu. Napiszmy więc teraz kod odpowiedniej klasy, która będzie potrafiła ten parametr odczytać. Jest on widoczny na listingu 8.5. Listing 8.5. import javax.swing.JApplet; import java.awt.*; Rozdział 8. Aplikacje i aplety 357 public class Aplet extends JApplet { private String tekst; public void init() { if((tekst = getParameter("tekst")) == null) tekst = "Nie został podany parametr: tekst"; } public void paint (Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.drawString (tekst, 60, 50); } } Do odczytu wartości parametru została użyta metoda getParameter, której jako argument należy przekazać nazwę parametru (w tym przypadku tekst). Metoda ta zwraca wartość wskazanego argumentu lub wartość null, jeżeli parametr nie został uwzględniony w kodzie HTML. W klasie Aplet deklarujemy zatem pole typu String o nazwie tekst. W metodzie init, która zostanie wykonana po załadowaniu apletu przez przeglądarkę, odczytujemy wartość parametru tekst i przypisujemy ją polu tekst. Sprawdzamy jednocześnie, czy pole to otrzymało wartość null (co by oznaczało, że parametr nie został umieszczony w kodzie HTML i nie można było go pobrać) — jeśli tak, przypisujemy mu własny tekst. Działanie metody paint jest takie samo jak we wcześniejszych przykładach, wyświetla ona po prostu zawartość pola tekst na ekranie, w miejscu o wskazanych współrzędnych (x = 60, y = 50). Pozostaje więc napisanie kodu HTML zawierającego taki aplet, co zostało przedstawione na listingu 8.6. Po uruchomieniu kodu (nie można zapomnieć o wcześniejszej kompilacji klasy Aplet) zobaczymy widok zaprezentowany na rysunku 8.5. Listing 8.6. <object type="application/x-java-applet" code="Aplet.class" width="300" height="100"> <param name="tekst" value="Testowanie parametrów apletu"> </object> Rysunek 8.5. Napis przekazany apletowi w postaci parametru 358 Java. Praktyczny kurs Lekcja 38. Kroje pisma (fonty) i kolory Wiadomo już, w jaki sposób wyświetlać w apletach napisy, ten temat był poruszany w pierwszej lekcji niniejszego rozdziału. W lekcji 38. zostaną omówione sposoby umożliwiające wykorzystanie dostępnych w systemie fontów (potocznie określanych też jako czcionki). Zostanie przedstawiona bliżej klasa Font. W drugiej części lekcji będzie natomiast mowa o kolorach, czyli klasie Color i takim jej wykorzystaniu, które umożliwi pokolorowanie napisów wyświetlanych w apletach. Kroje pisma Przykłady z poprzedniej lekcji pokazywały, w jaki sposób na powierzchni apletu wyświetlić napis. Przydałaby się w takim razie możliwość zmiany wielkości i, ewentualnie, kroju pisma. Jest to jak najbardziej możliwe. W pakiecie java.awt znajduje się klasa Font, która pozwala na wykonywanie tego typu manipulacji. Trzeba jednak wiedzieć, że czcionki w Javie dzielą się na dwa rodzaje: czcionki logiczne oraz czcionki fizyczne. W uproszczeniu można powiedzieć, że czcionki fizyczne są zależne od systemu, na którym działa maszyna wirtualna, natomiast czcionki logiczne to rodziny czcionek, które muszą być obsługiwane przez każde środowisko uruchomieniowe Javy (JRE). W środowisku Java 2 i kolejnych (a więc w wersjach od 1.2 wzwyż) jest dostępnych jedynie pięć czcionek logicznych: Serif, SansSerif, Monospaced, Dialog, DialogInput. Należy jednak pamiętać, że nie są to czcionki wbudowane, które będą tak samo wyglądały w każdym systemie. Zamiast tego jest wykorzystywana technika mapowania, tzn. dobierana jest czcionka systemowa (z systemu, na którym działa środowisko uruchomieniowe Javy) najlepiej pasująca do jednej z wymienionych rodzin. Aby w praktyce zobaczyć, jakie są różnice pomiędzy wymienionymi krojami, napiszmy teraz aplet, który wyświetli je na ekranie. Jest on widoczny na listingu 8.7. Listing 8.7. import javax.swing.JApplet; import java.awt.*; public class Aplet extends JApplet { Font serif, sansSerif, monospaced, dialog, dialogInput; public void init() { serif = new Font("Serif", Font.BOLD, 24); sansSerif = new Font("SansSerif", Font.BOLD, 24); monospaced = new Font("Monospaced", Font.BOLD, 24); dialog = new Font("Dialog", Font.BOLD, 24); dialogInput = new Font("DialogInput", Font.BOLD, 24); } public void paint (Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.setFont(serif); gDC.drawString ("Czcionka Serif", 60, 40); gDC.setFont(sansSerif); gDC.drawString ("Czcionka SansSerif", 60, 80); Rozdział 8. Aplikacje i aplety 359 gDC.setFont(monospaced); gDC.drawString ("Czcionka Monospaced", 60, 120); gDC.setFont(dialog); gDC.drawString ("Czcionka Dialog", 60, 160); gDC.setFont(dialogInput); gDC.drawString ("Czcionka DialogInput", 60, 200); } } W klasie Aplet przygotowujemy pięć pól klasy Font odpowiadających poszczególnym krojom pisma: serif, sansSerif, monospaced, dialog i dialogInput. W metodzie init tworzymy pięć obiektów klasy Font i przypisujemy je przygotowanym polom. Konstruktor klasy Font przyjmuje trzy parametry. Pierwszy z nich określa nazwę rodziny czcionek, drugi — styl czcionki, natomiast trzeci — jej wielkość. Styl czcionki ustalamy, posługując się stałymi (zmienne finalne) z klasy Font. Do dyspozycji są trzy różne wartości: Font.BOLD — czcionka pogrubiona, Font.ITALIC — czcionka pochylona, Font.PLAIN — czcionka zwyczajna. Z instrukcji zawartych w metodzie init wynika zatem, że każdy krój będzie pogrubiony, o wielkości liter równej 24 punktom. Wyświetlanie napisów odbywa się oczywiście w metodzie paint. Wykorzystujemy w tym celu znaną nam już z poprzednich przykładów metodę drawString, która wyświetla tekst w miejscu ekranu o współrzędnych wskazywanych przez drugi (współrzędna x) i trzeci (współrzędna y) argument. Do zmiany kroju pisma stosujemy metodę o nazwie setFont. Przyjmuje ona jako argument obiekt klasy Font zawierający opis danego fontu. Ostatecznie po skompilowaniu kodu i uruchomieniu apletu na ekranie pojawi się widok zaprezentowany na rysunku 8.6. Rysunek 8.6. Lista czcionek dostępnych standardowo 360 Java. Praktyczny kurs Nic nie stoi również na przeszkodzie, aby w konstruktorze klasy Font podać nazwę innej rodziny czcionek. Co się jednak stanie, jeśli danej czcionki nie ma w systemie? W takiej sytuacji zostanie wykorzystana czcionka domyślna, czyli kod będzie działał tak, jakby w konstruktorze został przekazany parametr default. Czcionką domyślną jest Dialog. Aby uniknąć takich niespodzianek, najlepiej po prostu pobrać listę wszystkich dostępnych fontów i tylko te wykorzystywać. Umożliwia to klasa GraphicsEnvironment. Zawiera ona dwie interesujące nas metody: getAllFonts oraz getAvailableFontFamily 8 Names . Pierwsza z nich zwraca tablicę obiektów typu Font zawierającą wszystkie dostępne czcionki, natomiast druga — tablicę obiektów typu String z nazwami dostępnych rodzin czcionek. Na listingu 8.8 jest widoczny kod apletu, który wykorzystuje wymienione metody do wyświetlania na ekranie czcionek dostępnych w systemie. Listing 8.8. import javax.swing.JApplet; import java.awt.*; public class Aplet extends JApplet { Font[] fonts; String[] fontNames; public void init() { GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); fonts = ge.getAllFonts(); fontNames = ge.getAvailableFontFamilyNames(); } public void paint(Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); int y = 20; for(int i = 0; i < fonts.length; i++){ gDC.drawString(fonts[i].getFontName(), 20, y); y += 15; } y = 20; for(int i = 0; i < fontNames.length; i++){ gDC.drawString(fontNames[i], 240, y); y += 15; } } } W aplecie zostały zadeklarowane dwa pola typu tablicowego: fonts oraz fontNames. Pierwsze z nich będzie przechowywać tablicę obiektów typu Font, drugie — typu String. W metodzie init trzeba utworzyć obiekt typu GraphicsEnvironment, który udostępnia potrzebne funkcje getAllFonts oraz getAvailableFontFamilyNames. Obiekt ten tworzymy, wywołując statyczną metodę getLocalGraphicsEnvironment z klasy GraphicsEnvironment, i przypisujemy zmiennej ge. Następnie pobieramy listę fontów (i przypisujemy ją polu fonts) oraz listę nazw rodzin czcionek (i przypisujemy ją tablicy fontNames). 8 Metody te są dostępne w JDK w wersji 1.2 i wyższych. Rozdział 8. Aplikacje i aplety 361 W metodzie paint pozostaje odczytać i wyświetlić na ekranie zawartość tablic. Służą do tego dwie pętle typu for. Do wyświetlania wykorzystuje się tradycyjnie metodę drawString. Ponieważ tablica fonts zawiera obiekt typu Font, aby uzyskać nazwy czcionek, wywołujemy metodę getFontName. Tablica fontNames zawiera obiekty typu String (ciągi znaków), można więc bezpośrednio użyć ich jako argumentu metody drawString. Do określenia współrzędnej y każdego napisu wykorzystuje się zmienną y, której wartość jest po każdym wyświetleniu zwiększana o 15. Przykładowy wynik działania programu jest widoczny na rysunku 8.7. W pierwszej kolumnie są wyświetlane nazwy konkretnych fontów, a w drugiej nazwy rodzin czcionek dostępnych w systemie, na którym aplet został uruchomiony. Obie listy z pewnością będą na tyle duże, że nie zmieszczą się w standardowym oknie apletu. W ramach ćwiczenia do samodzielnego wykonania można więc napisać program, który te dane wyświetli w oknie konsoli (podpunkt „Ćwiczenia do samodzielnego wykonania”). Rysunek 8.7. Lista czcionek i rodzin czcionek dostępnych w systemie Powróćmy teraz do przykładu, który wyświetlał na ekranie prosty napis. Zauważmy, że występuje w nim problem pozycjonowania tekstu na ekranie. Centrowanie musi się odbywać metodą prób i błędów, co z pewnością nie jest wygodne. Automatyczne wykonanie tego zadania wymaga jednak znajomości długości oraz wysokości napisu. Wiadomo też, że dla każdego fontu wartości te będą różne. Trzeba w związku z tym posłużyć się metodami udostępnianymi przez klasę FontMetrics: stringWidth i getHeight. Pierwsza z nich podaje długość napisu w pikselach, natomiast druga — jego przeciętną wysokość przy zastosowaniu danego fontu. Można to wykorzystać do ustawienia tekstu na środku powierzchni apletu. Kod realizujący to zadanie jest widoczny na listingu 8.9. Listing 8.9. import javax.swing.JApplet; import java.awt.*; public class Aplet extends JApplet { String tekst = "Przykładowy tekst"; Font font = null; public void init() { font = new Font("Arial", Font.BOLD, 20); } public void paint (Graphics gDC) { gDC.setFont(font); 362 Java. Praktyczny kurs FontMetrics fm = gDC.getFontMetrics(); int strWidth = fm.stringWidth(tekst); int strHeight = fm.getHeight(); int x = (getWidth() - strWidth) / 2; int y = (getHeight() + strHeight) / 2; gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.drawString(tekst, x, y); } } W metodzie init tworzymy nowy obiekt klasy Font reprezentujący pogrubioną czcionkę Arial o wielkości 20 punktów i przypisujemy go polu font klasy Aplet. W metodzie paint wywołujemy metodę setFont obiektu gDC, przekazując jej jako argument obiekt font. Tym samym ustalamy krój pisma, z którego chcemy korzystać przy wyświetlaniu napisów. Następnie pobieramy obiekt klasy FontMetrics, wywołując metodę get FontMetrics, i przypisujemy go zmiennej fm. Ten obiekt udostępnia metody niezbędne do określania wysokości i szerokości napisu. Szerokość uzyskuje się przez wywołanie metody stringWidth, a wysokość — przez wywołanie metody getHeight. Kiedy pobierzemy te wartości, pozostaje już tylko proste wyliczenie współrzędnych, od których powinno rozpocząć się wyświetlanie napisu. Wystarczy użyć wzorów: x = (szerokość apletu – szerokość napisu) / 2 y = (wysokość apletu + szerokość napisu) / 2 Po wykonaniu obliczeń można już wywołać metodę drawString, która spowoduje, że napis zapisany w polu tekst pojawi się na ekranie. Kolory Aby zmienić kolor, którym obiekty są domyślnie rysowane na powierzchni apletu (jak można było przekonać się we wcześniejszych przykładach, jest to kolor czarny), trzeba skorzystać z metody setColor zdefiniowanej z klasie Graphics. Jako argument przyjmuje ona obiekt klasy Color określający kolor, który będzie używany w dalszych operacjach. W klasie tej zostały zdefiniowane dwadzieścia cztery pola statyczne i finalne określające łącznie dwanaście różnych kolorów. Pola te zostały zebrane w tabeli 8.1. Aby zatem zmienić kolor wyświetlanego tekstu w aplecie z listingu 8.9 na czerwony, należałoby przed wywołaniem drawString dodać do metody paint wywołanie metody setColor(Color.red) lub setColor(Color.RED). Metoda paint miałaby wtedy następującą postać: public void paint (Graphics gDC) { //tutaj początkowe instrukcje metody gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.setColor(Color.RED); gDC.drawString(tekst, x, y); } Rozdział 8. Aplikacje i aplety 363 Tabela 8.1. Stałe określające kolory w klasie Color Nazwa stałej Nazwa stałej Reprezentowany kolor Color.black Color.BLACK Czarny Color.blue Color.BLUE Niebieski Color.darkGray Color.DARK_GRAY Ciemnoszary Color.gray Color.GRAY Szary Color.green Color.GREEN Zielony Color.lightGray Color.LIGHT_GRAY Jasnoszary Color.magenta Color.MAGENTA Magenta (fuksja)9 Color.orange Color.ORANGE Pomarańczowy Color.pink Color.PINK Różowy Color.red Color.RED Czerwony Color.white Color.WHITE Biały Color.yellow Color.YELLOW Żółty Szybko przekonamy się, że dwanaście kolorów to jednak trochę za mało — niestety nie ma więcej stałych w klasie Color. Jeśli więc w tabeli 8.1 nie odnajdziemy odpowiadającego nam koloru, musimy postąpić inaczej. Otóż należy w takim przypadku utworzyć nowy obiekt klasy Color. Do dyspozycji jest siedem konstruktorów, są one dokładniej opisane w dokumentacji JDK. Najwygodniej i najprościej będzie zastosować jeden z dwóch: Color(int rgb) Color(int r, int g, int b) Pierwszy z nich przyjmuje jeden argument, którym jest wartość typu int. Musi być ona skonstruowana w taki sposób, aby bity 16 – 23 określały wartość składowej R (w modelu RGB), bity 8 – 15 wartość składowej G, a 0 – 7 składowej B. Drugi z wymienionych konstruktorów przyjmuje natomiast wartość składowych w postaci trzech argumentów typu int. Warto może w tym miejscu przypomnieć, że w modelu RGB kolor jest określany za pomocą trzech składowych: czerwonej (R, ang. red), zielonej (G, ang. green) i niebieskiej (B, ang. blue). Każdy z parametrów przekazanych drugiemu konstruktorowi może przyjmować wartości z zakresu 0 – 255 (jest to więc 8-bitowy model RGB), łącznie można zatem przedstawić 16 777 216 różnych kolorów. W tabeli 8.2 zostały przedstawione składowe RGB dla kilkunastu przykładowych kolorów. Napiszmy zatem aplet wyświetlający w centrum powierzchni swojego okna napis, którego treść oraz kolor będą zdefiniowane poprzez zewnętrzne parametry określone w kodzie HTML. Kolor będzie określany za pomocą trzech parametrów odzwierciedlających poszczególne składowe RGB. Kod klasy realizującej takie zadanie został przedstawiony na listingu 8.10. 9 Rodzaj purpury, w modelu RGB z reguły określany przez składowe R = 255, G = 0, B = 255. 364 Java. Praktyczny kurs Tabela 8.2. Składowe RGB dla wybranych kolorów Kolor Składowa R Składowa G Składowa B Beżowy 245 245 220 Biały 255 255 255 Błękitny 0 191 255 Brązowy 165 42 42 Czarny 0 0 0 Czerwony 255 0 0 Ciemnoczerwony 139 0 00 Ciemnoniebieski 0 0 139 Ciemnoszary 169 169 169 Ciemnozielony 0 100 0 Fioletowy 238 130 238 Koralowy 255 127 80 Niebieski 0 0 255 Oliwkowy 107 142 35 Purpurowy 128 0 128 Srebrny 192 192 192 Stalowoniebieski 70 130 180 Szary 128 128 128 Zielony 0 255 0 Żółtozielony 195 205 50 Żółty 255 255 0 Listing 8.10. import javax.swing.JApplet; import java.awt.*; public class Aplet extends JApplet { String tekst; Font font = null; Color color = null; public void init() { int paramR = 0, paramG = 0, paramB = 0; try{ paramR = Integer.parseInt(getParameter("paramR")); paramG = Integer.parseInt(getParameter("paramG")); paramB = Integer.parseInt(getParameter("paramB")); } catch(NumberFormatException e){ paramR = 0; paramG = 0; paramB = 0; } color = new Color(paramR, paramG, paramB); Rozdział 8. Aplikacje i aplety 365 font = new Font("Arial", Font.BOLD, 20); tekst = getParameter("tekst"); if(tekst == null) tekst = ""; } public void paint (Graphics gDC) { gDC.setFont(font); FontMetrics fm = gDC.getFontMetrics(); int strWidth = fm.stringWidth(tekst); int strHeight = fm.getHeight(); int x = (getWidth() - strWidth) / 2; int y = (getHeight() + strHeight) / 2; gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.setColor(color); gDC.drawString(tekst, x, y); } } W klasie Aplet deklarujemy trzy pola: tekst — będzie zawierało wyświetlany napis, font — zawierające obiekt klasy Font opisujący krój pisma, oraz color — opisujące kolor napisu. W metodzie init odczytujemy wartości przekazanych parametrów o nazwach paramR, paramG i paramB. Ponieważ metoda getParameter zwraca wartość typu String, próbujemy dokonać konwersji na typ int za pomocą metody parseInt z klasy Integer. Jeśli któraś z konwersji się nie powiedzie, zostanie wygenerowany wyjątek NumberFormatException, który przechwytujemy w bloku try. Jeśli taki wyjątek wystąpi, przywracamy początkowe wartości zmiennym paramR, paramG i paramB. Tworzymy następnie nowy obiekt klasy Color i nowy obiekt klasy Font. Odczytujemy także wartość parametru tekst i przypisujemy go polu o takiej samej nazwie. Jeśli okaże się, że zostanie zwrócona wartość pusta null, czyli parametr o nazwie tekst nie został przekazany, pole tekst otrzyma pusty ciąg znaków. Wyświetlaniem napisu w centrum okna zajmuje się metoda paint, której zasada działania jest identyczna z tą przedstawioną na początku tego podrozdziału. Na listingu 8.11 został zaprezentowany przykładowy kod znacznika <object> zawierający wywołanie apletu. Listing 8.11. <object type="application/x-java-applet" code="Aplet.class" width="400" height="210" > <param name="paramr" value="168"> <param name="paramg" value="18"> <param name="paramb" value="240"> <param name="tekst" value="Przykładowy tekst"> </object> 366 Java. Praktyczny kurs Ćwiczenia do samodzielnego wykonania Ćwiczenie 38.1. Napisz program, który w trybie tekstowym wyświetli na konsoli systemowej listę czcionek dostępnych w systemie. Ćwiczenie 38.2. Napisz aplet wyświetlający na środku powierzchni okna tekst, którego treść oraz wielkość i styl czcionki będą przekazywane w postaci parametrów zawartych w kodzie HTML. Ćwiczenie 38.3. Napisz aplet, który będzie wyświetlał tekst o kolorze określonym przez parametr przekazywany z kodu HTML. Parametrem tym powinna być jedna liczba typu int. Ćwiczenie 38.4. Wyświetl na ekranie apletu krótki tekst, w którym każda litera będzie miała inny kolor (kolory mogą być losowe)10. Zrób tak, aby odległość pomiędzy literami była stała (możesz użyć czcionki o stałej szerokości znaku). Tekst powinien być wyśrodkowany w pionie i poziomie. Ćwiczenie 38.5. Zmodyfikuj kod ćwiczenia 38.4 w taki sposób, aby przy każdym odświeżeniu okna apletu litery zmieniały kolory. Lekcja 39. Grafika Aplety pracują w środowisku graficznym, czas więc poznać podstawowe operacje graficzne, jakie można wykonywać. Służą do tego metody klasy Graphics, niektóre z nich, jak drawString czy setColor, były już wykorzystywane we wcześniej prezentowanych przykładach. W tej lekcji okaże się, jak rysować proste figury geometryczne, takie jak linie, wielokąty czy okręgi, oraz jak wyświetlać obrazy z plików graficznych. Przedstawiony zostanie też interfejs ImageObserver, który pozwala na kontrolowanie postępów w ładowaniu plików graficznych do apletu. Rysowanie figur geometrycznych Rysowanie figur geometrycznych umożliwiają wybrane metody klasy Graphics. Rysować można zarówno same kontury, jak i pełne figury wypełnione zadanym kolorem. 10 Jeśli konieczne będzie uzyskanie dostępu do poszczególnych liter tekstu, można użyć metody charAt z klasy String. Np. w wyniku wywołania "test".charAt(1) zostanie uzyskana litera e (drugi znak, czyli ten o indeksie 1; indeksowanie zaczyna się od 0). Rozdział 8. Aplikacje i aplety 367 Do dyspozycji jest zestaw metod pozwalających na tworzenie m.in. linii, kół, elips i okręgów oraz wielokątów. Najpopularniejsze metody zostały zebrane w tabeli 8.3. Tabela 8.3. Wybrane metody klasy Graphics Deklaracja metody Opis void drawLine(int x1, int y1, int x2, int y2) Rysuje linię rozpoczynającą się w punkcie o współrzędnych x1, y1 i kończącą się w punkcie x2, y2. void drawOval(int x, int y, int width, int height) Rysuje owal wpisany w prostokąt opisany parametrami x, y oraz width i height. void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) Rysuje wielokąt o punktach wskazywanych przez tablice xPoints i yPoints. Liczbę segmentów linii, z których składa się figura, wskazuje parametr nPoints. void drawPolygon(Polygon p) Rysuje wielokąt opisany przez argument p. void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) Rysuje sekwencję połączonych ze sobą odcinków (linii) o współrzędnych zapisanych w tablicach xPoints i yPoints. Liczba segmentów jest określona przez argument nPoints. void drawRect(int x, int y, int width, int height) Rysuje prostokąt zaczynający się w punkcie o współrzędnych x, y oraz szerokości i wysokości określonej przez argumenty width i height. void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) Rysuje prostokąt o zaokrąglonych rogach zaczynający się w punkcie o współrzędnych x, y oraz szerokości i wysokości określonej przez argumenty width i height. Stopień zaokrąglenia rogów jest określany przez argumenty arcWidth i arcHeight. void fillOval(int x, int y, int width, int height) Rysuje koło lub elipsę wpisaną w prostokąt opisany parametrami x, y oraz width i height. void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) Rysuje wypełniony bieżącym kolorem wielokąt o punktach wskazywanych przez tablice xPoints i yPoints. Liczbę segmentów, z których składa się figura, wskazuje argument nPoints. void fillPolygon(Polygon p) Rysuje wypełniony bieżącym kolorem wielokąt opisany przez argument p. void fillRect(int x, int y, int width, int height) Rysuje wypełniony bieżącym kolorem prostokąt zaczynający się w punkcie o współrzędnych x, y oraz szerokości i wysokości określonej przez argumenty width i height. void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) Rysuje wypełniony bieżącym kolorem prostokąt o zaokrąglonych rogach, zaczynający się w punkcie o współrzędnych x, y oraz szerokości i wysokości określonej przez argumenty width i height. Stopień zaokrąglenia rogów jest definiowany przez argumenty arcWidth i arcHeight. 368 Java. Praktyczny kurs Punkty i linie Klasa Graphics nie oferuje oddzielnej metody, która umożliwiałaby rysowanie pojedynczych punktów, zamiast tego można jednak skorzystać z metody rysującej linie. Wystarczy, jeśli punkt początkowy i docelowy będą miały te same współrzędne. Tak więc aby narysować punkt o współrzędnych x = 100 i y = 200, można zastosować następującą instrukcję: drawLine(100, 200, 100, 200); Prosty aplet rysujący prostokąt (składający się z czterech oddzielnych linii) z pojedynczym punktem w środku jest widoczny na listingu 8.12, a efekt jego działania na rysunku 8.8. Listing 8.12. import javax.swing.JApplet; import java.awt.*; public class Aplet extends JApplet { public void paint (Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.drawLine(100, 40, 200, 40); gDC.drawLine(100, 120, 200, 120); gDC.drawLine(100, 40, 100, 120); gDC.drawLine(200, 40, 200, 120); gDC.drawLine(150, 80, 150, 80); } } Rysunek 8.8. Efekt działania apletu z listingu 8.12 Aby narysować punkty lub linie w różnych kolorach, trzeba skorzystać z metody set Color, podobnie jak w przykładach z lekcji 38. Należy zatem utworzyć nowy obiekt klasy Color opisujący dany kolor i użyć go jako argumentu tej metody. Po jej wykonaniu wszystkie obiekty będą rysowane w wybranym kolorze. Rozdział 8. Aplikacje i aplety 369 Koła i elipsy Do tworzenia kół i elips służą dwie metody klasy Graphics: drawOval oraz fillOval. Pierwsza rysuje sam kontur figury, druga figurę wypełnioną aktualnym kolorem. Kolor oczywiście można zmieniać przy użyciu metody setColor. Czemu do tworzenia koła i elipsy została zdefiniowana tylko jedna metoda? Ponieważ koło jest po prostu szczególnym przypadkiem elipsy, w której oba promienie są sobie równe. Nie czas, by zagłębiać się w niuanse matematyki, trzeba tylko wiedzieć, w jaki sposób określać rozmiary figur w wymienionych metodach. Przyjmują one bowiem cztery parametry opisujące prostokąt, w który można wpisać koło lub elipsę. Ponieważ w prostokąt o określonych rozmiarach można wpisać tylko jeden kształt owalny, określenie prostokąta jednoznacznie wyznacza koło lub elipsę. Obie metody przyjmują cztery argumenty. Pierwsze dwa określają współrzędne x i y lewego górnego rogu prostokąta, natomiast dwa kolejne — jego szerokość oraz wysokość. Przykład wykorzystania metod drawOval i fillOval do narysowania na powierzchni apletu kół i elips o różnych kolorach został zaprezentowany na listingu 8.13, a efekt widać na rysunku 8.9. Listing 8.13. import javax.swing.JApplet; import java.awt.*; public class Aplet extends JApplet { public void paint (Graphics gDC){ gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.setColor(Color.BLUE); gDC.drawOval (20, 20, 100, 100); gDC.drawOval (140, 120, 100, 40); gDC.setColor(Color.RED); gDC.fillOval (20, 120, 100, 40); gDC.fillOval (140, 20, 100, 100); } } Rysunek 8.9. Efekt działania metod fillOval i drawOval z klasy Graphics 370 Java. Praktyczny kurs Wielokąty Do rysowania wielokątów wykorzystuje się kilka różnych metod — można rysować zarówno same kontury (metody zaczynające się słowem draw), jak i figury wypełnione kolorem (metody zaczynające się słowem fill). Do rysowania prostokątów służą metody drawRectangle oraz fillRectangle. Przyjmują one cztery argumenty — dwa pierwsze określają współrzędne lewego górnego rogu, natomiast dwa kolejne — szerokość oraz wysokość figury. Istnieje również możliwość narysowania prostokąta o zaokrąglonych rogach11. Służą do tego metody drawRoundRect oraz fillRoundRect. W takim przypadku do wymienionych przed chwilą argumentów dochodzą dwa dodatkowe określające średnicę łuku zaokrąglenia w poziomie oraz w pionie (deklaracje wszystkich wymienionych metod znajdują się w tabeli 8.3). Przykładowy aplet rysujący opisane figury jest przedstawiony na listingu 8.14, a efekt jego działania — na rysunku 8.10. Listing 8.14. import javax.swing.JApplet; import java.awt.*; public class Aplet extends JApplet { public void paint (Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.setColor(Color.BLUE); gDC.drawRect(20, 20, 80, 80); gDC.drawRoundRect(120, 20, 80, 80, 20, 20); gDC.setColor(Color.GREEN); gDC.fillRoundRect(20, 120, 80, 80, 40, 20); gDC.fillRect(120, 120, 80, 80); } } Rysunek 8.10. Efekt działania metod rysujących różnego rodzaju prostokąty 11 Oczywiście formalnie taka figura nie jest już prostokątem. Rozdział 8. Aplikacje i aplety 371 Oprócz prostokątów można rysować praktycznie dowolne wielokąty określone zbiorem punktów wskazujących kolejne wierzchołki. Podobnie jak w przypadku wcześniej omawianych kształtów, mogą to być zarówno jedynie kontury, jak i figury wypełnione kolorem. Punkty określające wierzchołki przekazuje się w postaci dwóch tablic, pierwsza zawiera współrzędne x, druga współrzędne y. Jeśli więc chcemy narysować sam kontur, wykorzystamy metodę: drawPolygon(int[] x, int[] y, int ile) jeśli natomiast ma to być pełna figura — metodę: fillPolygon(int[] x, int[] y, int ile) Parametr ile określa liczbę wierzchołków. Jeżeli ostatni punkt nie będzie się pokrywał z pierwszym, figura zostanie automatycznie domknięta. Na listingu 8.15 jest widoczny aplet rysujący dwa sześciokąty, wykorzystujący wymienione metody. Efekt jego działania został zaprezentowany na rysunku 8.11. Listing 8.15. import javax.swing.JApplet; import java.awt.*; public class Aplet extends JApplet { int tabX1[] = {20, 60, 140, 180, 140, 60}; int tabY1[] = {100, 20, 20, 100, 180, 180}; int tabX2[] = {200, 240, 320, 360, 320, 240}; int tabY2[] = {100, 20, 20, 100, 180, 180}; public void paint (Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.setColor(Color.RED); gDC.drawPolygon(tabX1, tabY1, 6); gDC.setColor(Color.GREEN); gDC.fillPolygon(tabX2, tabY2, 6); } } Rysunek 8.11. Wykorzystanie metod drawPolygon i fillPolygon do wyświetlenia sześciokątów 372 Java. Praktyczny kurs Istnieją również przeciążone wersje metod drawPolygon i fillPolygon, które przyjmują jako argument obiekt klasy Polygon. Ich deklaracje mają postać: drawPolygon(Polygon p) fillPolygon(Polygon p) Aby z nich skorzystać, należy oczywiście najpierw utworzyć obiekt klasy Polygon. Ma ona dwa konstruktory, jeden bezargumentowy i drugi w postaci: Polygon(int[] x, int[] y, int ile) Pierwsze dwa argumenty to tablice współrzędnych kolejnych punktów, natomiast argument trzeci to liczba punktów. Przykładowy sześciokąt można zatem utworzyć w następujący sposób: int tabX[] = {20, 60, 140, 180, 140, 60}; int tabY[] = {100, 20, 20, 100, 180, 180}; Polygon polygon = new Polygon(tabX, tabY, 6); Wczytywanie grafiki Wczytywanie obrazów graficznych umożliwia metoda o nazwie getImage z klasy JApplet (Applet). Wczytuje ona wskazany w argumencie plik graficzny w formacie GIF, JPG lub PNG i zwraca obiekt klasy Image, który może zostać wyświetlony na ekranie. Metoda getImage występuje w dwóch następujących wersjach: getImage(URL url) getImage(URL url, String name) Pierwsza z nich przyjmuje jako argument obiekt klasy URL bezpośrednio wskazujący na plik z obrazem, druga wymaga podania argumentu klasy URL wskazującego na umiejscowienie pliku (np. http://host.domena/java/obrazy/) i drugiego, określającego nazwę pliku. Jeśli obraz znajduje się w strukturze katalogów serwera, na którym jest umieszczony dokument HTML i (lub) kod apletu, wygodne będzie użycie drugiej postaci konstruktora. Nie trzeba wtedy tworzyć nowego obiektu klasy URL bezpośrednio — wystarczy wywołać jedną z dwóch metod: getDocumentBase() lub: getCodeBase() Metoda getDocumentBase zwraca obiekt URL wskazujący lokalizację dokumentu HTML, w którym znajduje się aplet, natomiast getCodeBase — lokalizację, w której umieszczony jest kod apletu. Po otrzymaniu zwróconego przez getImage obiektu klasy Image będzie można go użyć jako argumentu metody drawImage rysującej obraz na powierzchni apletu. Metoda drawImage występuje w kilku przeciążonych wersjach (ich opis można znaleźć w dokumentacji JDK), w najprostszej postaci należy zastosować wywołanie: gDC.drawImage (img, wspX, wspY, this); Rozdział 8. Aplikacje i aplety 373 Argument img to referencja do obiektu klasy Image, wspX to współrzędna x, a wspY — współrzędna y, this to referencja do obiektu implementującego interfejs ImageObserver (w naszym przypadku będzie to obiekt apletu, czyli obiekt klasy dziedziczącej po JApplet (Applet) — nieco dalej zostanie to wyjaśnione dokładniej). Przykładowy aplet wyświetlający opisanym sposobem plik graficzny jest widoczny na listingu 8.16, natomiast efekt działania tego apletu został zaprezentowany na rysunku 8.12. Listing 8.16. import javax.swing.JApplet; import java.awt.*; public class Aplet extends JApplet { Image img; public void init() { img = getImage(getDocumentBase(), "obraz.jpg"); } public void paint(Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.drawImage (img, 20, 20, this); } } Rysunek 8.12. Obraz wczytany za pomocą metody getImage Czas wyjaśnić, do czego służy czwarty parametr metody drawImage, którym w przypadku apletu z listingu 8.16 było wskazanie na obiekt tego apletu (wskazanie this). Przede wszystkim trzeba wiedzieć, co się dzieje w kolejnych fazach pracy takiego apletu. W metodzie init wywołujemy metodę getImage, przekazując jej w argumencie lokalizację pliku. Ta metoda zwraca obiekt klasy Image niezależnie od tego, czy wskazany plik graficzny istnieje, czy nie. Ładowanie danych rozpocznie się dopiero w momencie pierwszego wywołania metody drawImage. 374 Java. Praktyczny kurs Sama metoda drawImage działa natomiast w taki sposób, że po jej wywołaniu jest wyświetlana dostępna część obrazu (czyli albo nic, albo część obrazu, albo cały obraz) i metoda kończy działanie. Jeśli cały obraz był dostępny, jest zwracana wartość true, jeśli nie — wartość false. Jeśli obraz nie był w pełni dostępny i zwrócona została wartość false, jest on ładowany w tle. W trakcie tego ładowania, czyli napływania kolejnych danych z sieci, jest wywoływana metoda imageUpdate obiektu implementującego interfejs ImageObserver, który został przekazany jako czwarty argument metody drawImage. Ponieważ klasa JApplet implementuje ten interfejs, można jej obiekt wykorzystać jako czwarty argument metody. Powstanie wtedy sytuacja, w której obiekt apletu jest informowany o postępach ładowania obrazu. Aby mieć możliwość kontrolowania procesu wczytywania i wyświetlania obrazu, należy przeciążyć metodę imageUpdate z klasy JApplet. Przykładowo: jeśli w trakcie ładowania obrazu na pasku stanu miałaby być wyświetlana informacja o tym procesie, należałoby zastosować kod widoczny na listingu 8.17. Listing 8.17. import javax.swing.JApplet; import java.awt.*; import java.awt.image.*; public class Aplet extends JApplet { Image img; public void init() { img = getImage(getDocumentBase(), "obraz.jpg"); } public void paint (Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.drawImage (img, 20, 20, this); } public boolean imageUpdate(Image img, int flags, int x, int y, int width, int height) { if ((flags & ImageObserver.ALLBITS) == 0){ showStatus ("Ładowanie obrazu..."); return true; } else{ showStatus ("Obraz załadowany"); repaint(); return false; } } } Metoda imageUpdate przy każdym wywołaniu otrzymuje cały zestaw argumentów, czyli: img — referencję do rysowanego obrazu (jest to ważne, gdyż jednocześnie może być przecież ładowanych kilka obrazów), x i y — współrzędne powierzchni apletu, w których rozpoczyna się obraz, width i height — wysokość i szerokość obrazu, oraz najważniejszy dla nas w tej chwili — flags. Argument flags to wartość typu int, w której poszczególne bity informują o stanie ładowanego obrazu. Informacje o ich dokładnym znaczeniu można znaleźć w dokumentacji JDK w opisie interfejsu ImageObserver. Rozdział 8. Aplikacje i aplety 375 Najważniejszy dla nas jest bit piąty, którego ustawienie na 1 oznacza, że obraz został całkowicie załadowany i może zostać wyświetlony w ostatecznej formie. Do zbadania stanu tego bitu wykorzystuje się zdefiniowaną w klasie ImageObserver stałą (pole statyczne i finalne typu int) ImageObserver.ALLBITS. Jeśli wynikiem operacji bitowej AND między argumentem flags oraz stałą ALLBITS jest 0, oznacza to, że bit ten jest wyłączony, a zatem obraz nie jest załadowany, jeśli natomiast wynik tej operacji jest różny od 0, to bit jest włączony i dysponujemy pełnym obrazem. Te właściwości są zatem wykorzystywane w metodzie imageUpdate. Badany jest wynik iloczynu logicznego flags & ImageObserver.ALLBITS. Kiedy jest on równy 0, wykonana zostaje metoda showStatus ustawiająca na pasku stanu przeglądarki tekst (rysunek 8.13) informujący, że obraz jest w trakcie ładowania. Kiedy jest różny od 0, zostaje wyświetlony napis, że obraz został załadowany. W tym drugim przypadku należy dodatkowo odświeżyć ekran apletu, za co odpowiada metoda repaint. Rysunek 8.13. Wyświetlanie informacji o stanie załadowania obrazu Ćwiczenia do samodzielnego wykonania Ćwiczenie 39.1. Napisz aplet rysujący owal, którego rozmiar i położenie będą przekazywane w postaci argumentów z kodu HTML. Ćwiczenie 39.2. Zmodyfikuj kod z listingu 8.15 tak, aby wykorzystywał metody rysujące wielokąty, które przyjmują jako argumenty obiekty klasy Polygon. Ćwiczenie 39.3. Zmień kod apletu z listingu 8.17 tak, aby informacja o stanie ładowania obrazu była wyświetlana nie w wierszu statusu przeglądarki, ale na środku powierzchni apletu. Ćwiczenie 39.4. Napisz aplet rysujący wzór podobny do pokazanego na rysunku 8.14. Do rysowania figur spróbuj użyć wyłącznie metody drawRect. Jeśli to możliwe, zrób tak, aby w całym kodzie było tylko jedno wywołanie tej metody. Ćwiczenie 39.5. Napisz aplet rysujący wielobarwne koło. Postaraj się uzyskać płynną zmianę kolorów. 376 Java. Praktyczny kurs Rysunek 8.14. Wzór graficzny do ćwiczenia 39.4 Lekcja 40. Dźwięki i obsługa myszy Niektóre aplety wymagają reakcji na działania użytkownika, zwykle chodzi o zdarzenia związane z obsługą myszy. Jeśli aplet ma reagować na zmiany położenia kursora czy na kliknięcia, może bezpośrednio implementować odpowiedni interfejs bądź też korzystać z dodatkowego obiektu. Jak taka obsługa wygląda w praktyce, okaże się właśnie w lekcji 40. W drugiej części tej lekcji będzie mowa o odtwarzaniu dźwięków przez aplety i interfejsie AudioClip. Interfejs MouseListener Interfejs MouseListener jest zdefiniowany w pakiecie java.awt.event, a zatem klasa, która będzie go implementowała, musi zawierać odpowiednią dyrektywę import. Jest on dostępny we wszystkich JDK, począwszy od wersji 1.1. Znajdują się w nim deklaracje pięciu metod zebranych w tabeli 8.4. Tabela 8.4. Metody interfejsu MouseListener Deklaracja metody Opis Od JDK void mouseClicked(MouseEvent e) Metoda wywoływana po kliknięciu przyciskiem myszy. 1.1 void mouseEntered(MouseEvent e) Metoda wywoływana, kiedy kursor myszy wejdzie w obszar komponentu. 1.1 void mouseExited(MouseEvent e) Metoda wywoływana, kiedy kursor myszy opuści obszar komponentu. 1.1 void mousePressed(MouseEvent e) Metoda wywoływana po naciśnięciu przycisku myszy. 1.1 void mouseReleased(MouseEvent e) Metoda wywoływana po puszczeniu przycisku myszy. 1.1 Każda klasa implementująca ten interfejs musi zatem zawierać definicję wszystkich wymienionych metod, nawet jeśli nie będzie ich wykorzystywała. Szkielet apletu będzie miał postać widoczną na listingu 8.18. Jak widać, możliwe jest reagowanie na pięć różnych zdarzeń opisanych w tabeli 8.4 i w komentarzach w zaprezentowanym kodzie. Rozdział 8. Aplikacje i aplety 377 Listing 8.18. import javax.swing.JApplet; import java.awt.event.*; public class Aplet extends JApplet implements MouseListener { //pola i metody apletu public void mouseClicked(MouseEvent e) { //kod wykonywany po kliknięciu myszą } public void mouseEntered(MouseEvent e) { //kod wykonywany, kiedy kursor wejdzie w obszar komponentu } public void mouseExited(MouseEvent e) { //kod wykonywany, kiedy kursor opuści obszar komponentu } public void mousePressed(MouseEvent e) { //kod wykonywany, kiedy zostanie wciśnięty przycisk myszy } public void mouseReleased(MouseEvent e) { //kod wykonywany, kiedy przycisk myszy zostanie zwolniony } } Każda metoda otrzymuje jako argument obiekt klasy MouseEvent pozwalający określić rodzaj zdarzenia oraz współrzędne kursora. Aby dowiedzieć się, który przycisk został wciśnięty, korzysta się z metody getButton12, współrzędne natomiast można otrzymać, wywołując metody getX i getY. Metoda getButton zwraca wartość typu int, którą należy porównywać ze stałymi zdefiniowanymi w klasie MouseEvent: MouseEvent.BUTTON1, MouseEvent.BUTTON2, MouseEvent.BUTTON3, MouseEvent.NOBUTTON. Pierwsze trzy określają numer przycisku, natomiast ostatnia informuje, że żaden przycisk podczas danego zdarzenia nie był wciśnięty. Na listingu 8.19 jest widoczny przykładowy aplet, który wyświetla współrzędne ostatniego kliknięcia myszą oraz informację o tym, który przycisk został użyty. Listing 8.19. import javax.swing.JApplet; import java.awt.*; import java.awt.event.*; public class Aplet extends JApplet implements MouseListener { String tekst = ""; public void init() { addMouseListener(this); 12 Metoda ta jest dostępna, począwszy od JDK 1.4, wcześniejsze wersje JDK jej nie zawierają. 378 Java. Praktyczny kurs } public void paint (Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.drawString(tekst, 20, 20); } public void mouseClicked(MouseEvent evt) { int button = evt.getButton(); switch(button){ case MouseEvent.BUTTON1 : tekst = "Przycisk 1, ";break; case MouseEvent.BUTTON2 : tekst = "Przycisk 2, ";break; case MouseEvent.BUTTON3 : tekst = "Przycisk 3, ";break; default : tekst = ""; } tekst += "współrzędne: x = " + evt.getX() + ", "; tekst += "y = " + evt.getY(); repaint(); } public void mouseEntered(MouseEvent evt){} public void mouseExited(MouseEvent evt){} public void mousePressed(MouseEvent evt){} public void mouseReleased(MouseEvent evt){} } Ponieważ interesuje nas jedynie zdarzenie polegające na kliknięciu myszą, treść metod niezwiązanych z nim, czyli mouseEntered, mouseExited, mousePressed, mouseReleased, pozostaje pusta. Niemniej ich definicje muszą znajdować się w klasie Aplet, gdyż wymaga tego interfejs MouseListener (por. lekcje 26. i 27.). W metodzie mouseClicked, wywołując metodę getButton, odczytujemy kod naciśniętego przycisku i przypisujemy go zmiennej pomocniczej button (nie jest niezbędna i została użyta wyłącznie ze względu na zwiększenie czytelności kodu; w praktyce wywołanie evt.getButton() mogłoby być umieszczone bezpośrednio w instrukcji switch). Następnie sprawdzamy wartość tej zmiennej za pomocą instrukcji wyboru switch i w zależności od tego, czy jest to wartość BUTTON1, BUTTON2, czy BUTTON3, przypisujemy odpowiedni ciąg znaków zmiennej tekst, która odpowiada za napis, jaki będzie wyświetlany na ekranie. W dalszej części kodu dodajemy do zmiennej tekst określenie współrzędnej x i współrzędnej y, w której nastąpiło kliknięcie. Wartości wymienionych współrzędnych odczytujemy dzięki metodom getX i getY. Na końcu metody mouseClicked wywołujemy metodę repaint, która spowoduje odświeżenie ekranu. Odświeżenie ekranu wiąże się oczywiście z wywołaniem metody paint, w której znana nam dobrze metoda drawString jest używana do wyświetlenia tekstu zawartego w polu tekst. Bardzo ważny jest również fragment kodu wykonywany w metodzie init. Otóż wywołujemy tam metodę addMouseListener, której w argumencie przekazujemy wskazanie do apletu. Takie wywołanie oznacza, że wszystkie zdarzenia związane z obsługą myszy, określone przez interfejs MouseListener, będą obsługiwane przez obiekt wskazany w argumencie metody. Ponieważ argumentem jest referencja do obiektu apletu (choć może być to obiekt dowolnej klasy implementującej interfejs MouseListener), w tym przypadku informujemy po prostu maszynę wirtualną, że nasz aplet samodzielnie będzie obsługiwał zdarzenia związane z myszą. Rozdział 8. Aplikacje i aplety 379 O wywołaniu metody addMouseListener trzeba koniecznie pamiętać, gdyż jeśli jej zabraknie, kompilator nie zgłosi żadnego błędu, a program po prostu nie będzie działał. Jest to błąd stosunkowo często popełniany przez początkujących programistów. Jeśli jednak będziemy o tej instrukcji pamiętać, to po skompilowaniu i uruchomieniu apletu oraz kliknięciu pierwszego przycisku na ekranie pojawi się widok podobny do zaprezentowanego na rysunku 8.15. Rysunek 8.15. Wynik działania apletu wyświetlającego informacje o kliknięciach myszą Skoro metoda addActionListener może przekazać obsługę zdarzeń dowolnemu obiektowi implementującemu interfejs MouseListener, warto sprawdzić, jak by to wyglądało w praktyce. Napiszmy dodatkową klasę pakietową współpracującą z klasą Aplet i odpowiedzialną za obsługę myszy. Aby nie komplikować kodu, jej zadaniem będzie jedynie wyświetlenie na konsoli współrzędnych ostatniego kliknięcia. Kod obu klas został zaprezentowany na listingu 8.20. Listing 8.20. import javax.swing.JApplet; import java.awt.event.*; public class Aplet extends JApplet { public void init() { addMouseListener(new MyMouseListener()); } } class MyMouseListener implements MouseListener { public void mouseClicked(MouseEvent evt) { String tekst = ""; int button = evt.getButton(); switch(button){ case MouseEvent.BUTTON1 : tekst = "Przycisk 1, ";break; case MouseEvent.BUTTON2 : tekst = "Przycisk 2, ";break; case MouseEvent.BUTTON3 : tekst = "Przycisk 3, ";break; default : tekst = ""; } tekst += "współrzędne: x = " + evt.getX() + ", "; tekst += "y = " + evt.getY(); System.out.println(tekst); } public void mouseEntered(MouseEvent evt){} public void mouseExited(MouseEvent evt){} public void mousePressed(MouseEvent evt){} public void mouseReleased(MouseEvent evt){} } 380 Java. Praktyczny kurs Klasa Aplet nie implementuje w tej chwili interfejsu MouseListener, gdyż obsługa myszy jest przekazywana innej klasie. Pozostała w niej jedynie metoda init, w której wywołuje się metodę addActionListener. Argumentem przekazanym metodzie addActionListener jest nowy obiekt klasy MyMouseListener. Oznacza to, że aplet ma reagować na zdarzenia myszy, ale ich obsługa została przekazana obiektowi klasy MyMouseListener. Klasa MyMouseListener jest klasą pakietową, może być zatem zdefiniowana w tym samym pliku co klasa Aplet (lekcja 17.). Implementuje ona oczywiście interfejs MouseListener, inaczej obiekt tej klasy nie mógłby być argumentem metody addActionListener. Wewnątrz klasy MyMouseListener zostały zdefiniowane metody z interfejsu, jednak kod wykonywalny zawiera jedynie metoda mouseClicked. Jej treść jest bardzo podobna do kodu metody mouseClicked z poprzedniego przykładu. Jedyną różnicą jest to, że zmienna tekst jest zdefiniowana wewnątrz tej metody i zamiast metody repaint jest wykonywana instrukcja System.out.println wyświetlająca na konsoli współrzędne kliknięcia. Uruchomienie takiego apletu spowoduje, że współrzędne kliknięcia będą się pojawiały na konsoli, tak jak jest to widoczne na rysunku 8.16. Rysunek 8.16. Współrzędne kliknięcia pojawiają się na konsoli Tym razem nie da się wyświetlić tekstu bezpośrednio w obszarze apletu, gdyż klasa MyMouseListener nie ma do niego dostępu. Jak go uzyskać? Można by na przykład przekazać referencję do obiektu apletu w konstruktorze klasy MyMouseListener (ćwiczenie 40.4 z podpunktu „Ćwiczenia do samodzielnego wykonania”). O wiele lepszym rozwiązaniem byłoby jednak zastosowanie klasy wewnętrznej, a jeszcze lepiej anonimowej klasy wewnętrznej (lekcje 28. – 30.). Takie rozwiązanie zostanie pokazane już w kolejnej lekcji. Interfejs MouseMotionListener Interfejs MouseMotionListener pozwala na obsługiwanie zdarzeń związanych z ruchem myszy. Zawiera definicję dwóch metod — zostały przedstawione w tabeli 8.5. Pierwsza jest wywoływana, jeśli przycisk myszy został wciśnięty i mysz się porusza, natomiast druga przy każdym ruchu myszy bez wciśniętego przycisku. Tabela 8.5. Metody interfejsu MouseMotionListener Deklaracja metody Opis Od JDK void mouseDragged(MouseEvent e) Metoda wywoływana podczas ruchu myszy, kiedy wciśnięty jest jeden z przycisków. 1.1 void mouseMoved(MouseEvent e) Metoda wywoływana przy każdym ruchu myszy, o ile nie jest wciśnięty żaden przycisk. 1.1 Rozdział 8. Aplikacje i aplety 381 Opierając się na przykładzie z listingu 8.19, bez problemu można napisać aplet, który będzie wyświetlał aktualne współrzędne położenia kursora myszy. Kod realizujący to zadanie został przedstawiony na listingu 8.21. Listing 8.21. import javax.swing.JApplet; import java.awt.*; import java.awt.event.*; public class Aplet extends JApplet implements MouseMotionListener { String tekst = ""; public void init() { addMouseMotionListener(this); } public void paint (Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.drawString(tekst, 20, 20); } public void mouseMoved(MouseEvent evt) { tekst = "zdarzenie mouseMoved, "; tekst += "współrzędne: x = " + evt.getX() + ", "; tekst += "y = " + evt.getY(); repaint(); } public void mouseDragged(MouseEvent evt) { tekst = "zdarzenie mouseDragged, "; tekst += "współrzędne: x = " + evt.getX() + ", "; tekst += "y = " + evt.getY(); repaint(); } } Sposób postępowania jest tu identyczny z prezentowanym w przypadku interfejsu MouseListener. Klasa Aplet implementuje interfejs MouseMotionListener, zawiera zatem metody mouseMoved i mouseDragged. W metodzie init jest wywoływana metoda addMouse MotionListener (podobnie jak we wcześniejszych przykładach addMouseListener), dzięki czemu wszystkie zdarzenia związane z poruszaniem myszy będą obsługiwane przez klasę Aplet. W metodach mouseMoved oraz mouseDragged odczytujemy współrzędne kursora myszy dzięki funkcjom getX i getY, przygotowujemy treść pola tekst oraz wywołujemy metodę repaint wymuszającą odświeżenie ekranu. W metodzie paint wyświetlamy natomiast zawartość pola tekst na ekranie. Dodatkowe parametry zdarzenia Niekiedy przy przetwarzaniu zdarzeń związanych z obsługą myszy zachodzi potrzeba sprawdzenia stanu klawiszy specjalnych Alt, Shift lub Ctrl. Jest tak np. wtedy, kiedy program ma inaczej reagować, gdy kliknięcie nastąpiło z równoczesnym wciśnięciem jednego z wymienionych klawiszy. Musi zatem istnieć sposób pozwalający na sprawdzenie, czy zaszła taka specjalna sytuacja. Tym sposobem jest dokładniejsze zbadanie obiektu klasy MouseEvent, który jest przekazywany funkcji obsługującej każde zdarzenie związane z obsługą myszy. 382 Java. Praktyczny kurs Klasa MouseEvent (a dokładniej klasa InputEvent, po której MouseEvent dziedziczy) udostępnia metodę o nazwie getModifiers, zwracającą wartość typu int, której poszczególne bity określają dodatkowe parametry zdarzenia. Stan tych bitów bada się poprzez porównanie z jedną ze stałych13 zdefiniowanych w klasie MouseEvent. W sumie jest dostępnych kilkadziesiąt stałych, w większości odziedziczonych po klasach bazowych, ich opis można znaleźć w dokumentacji JDK. Dla nas interesujące są trzy wartości: MouseEvent.SHIFT_DOWN_MASK, MouseEvent.ALT_DOWN_MASK, MouseEvent.CTRL_DOWN_MASK. Pierwsza z nich oznacza, że został wciśnięty klawisz Shift, druga — że został wciśnięty klawisz Alt, a trzecia — że został wciśnięty klawisz Ctrl14. Porównania z wartością zwróconą przez getModifiers dokonuje się przez wykonanie operacji bitowej AND, czyli iloczynu bitowego. Jeśli więc wynikiem operacji: getModifiers() & MouseEvent.SHIFT_DOWN jest wartość 0, oznacza to, że klawisz Shift nie był wciśnięty, a jeśli wartość tej operacji jest różna od 0, Shift był wciśnięty. Przykładowy aplet wykorzystujący opisaną technikę do stwierdzenia, które z klawiszy funkcyjnych były wciśnięte podczas przesuwania kursora myszy, jest widoczny na listingu 8.22, a efekt działania tego apletu przedstawiono na rysunku 8.17. Listing 8.22. import javax.swing.JApplet; import java.awt.*; import java.awt.event.*; public class Aplet extends JApplet implements MouseMotionListener { String tekst = ""; public void init() { addMouseMotionListener(this); } public void paint (Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.drawString(tekst, 20, 20); } public void mouseMoved(MouseEvent evt) { tekst = "Wciśnięte klawisze ["; int modifiers = evt.getModifiersEx(); if((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0){ tekst += " SHIFT "; } if((modifiers & MouseEvent.ALT_DOWN_MASK) != 0){ tekst += " ALT "; } if((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0){ 13 Przez stałą rozumiemy pole statyczne i finalne klasy. 14 W wersjach JDK poniżej 1.4 były wykorzystywane stałe SHIFT_DOWN, ALT_DOWN i CTRL_DOWN. Rozdział 8. Aplikacje i aplety 383 tekst += " CTRL "; } tekst += "], "; tekst += "współrzędne: x = " + evt.getX() + ", "; tekst += "y = " + evt.getY(); repaint(); } public void mouseDragged(MouseEvent evt){} } Rysunek 8.17. Przykładowy efekt działania apletu z listingu 8.22 Dźwięki Pisane przez nas aplety można wyposażyć w możliwość odtwarzania dźwięków. Java obsługuje standardowo kilka formatów plików dźwiękowych, są to AU, AIFF, WAVE oraz MIDI. W klasie JApplet została zdefiniowana metoda play, która pobiera i odtwarza klip dźwiękowy. Występuje ona w dwóch przeciążonych wersjach: play(URL url) play(URL url, String name) Pierwsza z nich przyjmuje jako argument obiekt klasy URL bezpośrednio wskazujący na plik dźwiękowy, druga wymaga podania argumentu klasy URL wskazującego na umiejscowienie pliku (np. http://host.domena/java/sound/) i drugiego określającego nazwę pliku. Jeśli plik znajduje się w strukturze katalogów serwera, na którym umieszczony jest dokument HTML i (lub) kod apletu, wygodniejsze może być użycie drugiej postaci konstruktora, podobnie jak w przypadku metody getImage omawianej w lekcji 39. Przykład apletu, który podczas uruchamiania odtwarza plik dźwiękowy, jest widoczny na listingu 8.23. Listing 8.23. import javax.swing.JApplet; public class Aplet extends JApplet { public void start() { play (getDocumentBase(), "ding.au"); } } Drugim sposobem na odtwarzanie dźwięku jest wykorzystanie interfejsu AudioClip. Interfejs ten zawiera definicje trzech metod: start, stop i loop. Metoda start rozpoczyna odtwarzanie dźwięku, stop kończy odtwarzanie, natomiast loop rozpoczyna odtwarzanie dźwięku w pętli. Ponieważ AudioClip został zdefiniowany w pakiecie java.applet, tym razem jako klasa apletu zostanie wykorzystana Applet (zamiast 384 Java. Praktyczny kurs JApplet). Obiekt implementujący interfejs AudioClip otrzymamy, wywołując metodę getAudioClip z klasy Applet. Metoda ta występuje w dwóch przeciążonych wersjach: getAudioClip(URL url) getAudioClip(URL url, String name) Znaczenie argumentów jest takie samo jak w przypadku opisanej powyżej metody play. Proste wykorzystanie interfejsu AudioClip zostało zilustrowane w przykładzie z listingu 8.24. Po uruchomieniu apletu rozpoczyna się odtwarzanie dźwięku, a gdy aplet kończy pracę, odtwarzanie jest zatrzymywane. Listing 8.24. import java.applet.Applet; import java.applet.AudioClip; public class Aplet extends Applet { AudioClip audioClip; public void init() { audioClip = getAudioClip(getDocumentBase(), "ding.au"); } public void start() { audioClip.loop(); } public void stop() { audioClip.stop(); } } Znacznie ciekawsze byłoby jednak połączenie możliwości odtwarzania dźwięków oraz reakcji na zdarzenia związane z obsługą myszy. Możliwości, jakie dają interfejsy MouseListener oraz AudioClip, pozwalają na napisanie apletu, który będzie np. odtwarzał plik dźwiękowy, kiedy użytkownik kliknie myszą. Aplet realizujący takie zadanie jest przedstawiony na listingu 8.25. Listing 8.25. import java.applet.Applet; import java.applet.AudioClip; import java.awt.event.*; public class Aplet extends Applet implements MouseListener { AudioClip audioClip; public void init() { addMouseListener(this); audioClip = getAudioClip(getDocumentBase(), "ding.wav"); } public void mouseClicked(MouseEvent evt) { audioClip.play(); } public void mouseEntered(MouseEvent evt){} public void mouseExited(MouseEvent evt){} public void mousePressed(MouseEvent evt){} public void mouseReleased(MouseEvent evt){} } Rozdział 8. Aplikacje i aplety 385 Klasa Aplet implementuje interfejs MouseListener, a zatem zawiera definicje wszystkich jego metod. W tym przypadku używana jest jednak tylko metoda mouseClicked, która będzie wywoływana po każdym kliknięciu przyciskiem myszy. Rozpoczynamy w niej odtwarzanie dźwięku poprzez wywołanie metody play obiektu audioClip. Obiekt wskazywany przez pole audioClip uzyskujemy w metodzie init przez wywołanie metody getAudioClip z klasy Applet, dokładnie tak samo jak w poprzednim przykładzie. Oczywiście w kodzie znajduje się również wywołanie metody addMouseListener, bez której aplet nie będzie reagował na kliknięcia. Ćwiczenia do samodzielnego wykonania Ćwiczenie 40.1. Zmień kod apletu z listingu 8.19 w taki sposób, aby reagował nie na kliknięcia, ale na samo naciśnięcie przycisku myszy. Ćwiczenie 40.2. Napisz aplet, w którym obsługa ruchów myszy będzie realizowana przez oddzielną klasę MyMouseMotionListener. Przy każdym ruchu myszy wyświetl współrzędne kursora myszy na konsoli. Ćwiczenie 40.3. Napisz aplet, który będzie odtwarzał plik dźwiękowy, kiedy użytkownik zbliży kursor myszy na mniej niż 10 pikseli od brzegów powierzchni apletu. Wysokość oraz szerokość obszaru apletu można uzyskać dzięki wywołaniu metody getWidth oraz getHeight klasy JApplet (Applet). Ćwiczenie 40.4. Napisz aplet, który przy kliknięciu będzie odtwarzał dźwięki. Odtwarzanie powinno być realizowane przez pakietową klasę MyAudioClip implementującą interfejs AudioClip. Ćwiczenie 40.5. Napisz aplet, który będzie pozwalał na rysowanie na jego powierzchni kolorowych odcinków. Kolor każdego odcinka powinien być losowy. 386 Java. Praktyczny kurs Aplikacje Lekcja 41. Tworzenie aplikacji Na początku rozdziału 8. była mowa o różnicach między aplikacją i apletem, wiadomo zatem, że aplikacja potrzebuje do uruchomienia jedynie maszyny wirtualnej, a aplet jest programem wbudowanym, zagnieżdżonym w innym programie, najczęściej w przeglądarce internetowej. Wszystkie programy, które powstawały w rozdziałach 1. – 7., były właśnie aplikacjami, pracującymi jednak w trybie tekstowym. W tej lekcji będzie pokazane, w jaki sposób tworzy się aplikacje pracujące w trybie graficznym, czyli popularne aplikacje okienkowe. Pierwsze okno W lekcji 37. powstał nasz pierwszy aplet, przedstawiony na listingu 8.1. Zobaczmy teraz, jak napisać aplikację, która będzie wykonywała to samo zadanie, czyli wyświetli napis na ekranie. Będzie to wymagało napisania klasy np. o nazwie PierwszaAplikacja, która będzie dziedziczyć po klasie JFrame. Jest to klasa zawarta w pakiecie javax.swing. Alternatywnie można użyć również klasy Frame z pakietu java.awt, jednak jest ona uznawana za przestarzałą. Kod pierwszej aplikacji został przedstawiony na listingu 8.26. Listing 8.26. import javax.swing.*; import java.awt.*; public class PierwszaAplikacja extends JFrame { public PierwszaAplikacja() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(320, 200); setVisible(true); } public void paint(Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.drawString ("Pierwsza aplikacja", 100, 100); } public static void main(String args[]) { new PierwszaAplikacja(); } } Za utworzenie okna odpowiada klasa PierwszaAplikacja, która dziedziczy po klasie JFrame. W konstruktorze za pomocą metody setSize ustalamy rozmiary okna, natomiast za pomocą metody setVisible powodujemy, że zostanie ono wyświetlone na ekranie (został przekazany argument true). Za wyświetlenie na ekranie napisu odpowiada metoda drawString z klasy Graphics, odbywa się to dokładnie tak samo jak w przypadku apletów omawianych w poprzednich lekcjach. Należy również zwrócić uwagę na instrukcję: setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Rozdział 8. Aplikacje i aplety 387 która powoduje, że domyślnym działaniem wykonywanym podczas zamykania okna (np. gdy użytkownik kliknie przycisk zamykający lub wybierze taką akcję z menu systemowego) będzie zakończenie działania całego programu (wskazuje na to stała EXIT_ON_CLOSE). Jeśli ta instrukcja zostanie pominięta, nie będzie można w standardowy sposób zakończyć działania aplikacji (okno zostanie zamknięte, ale aplikacja wciąż będzie działać). Obiekt klasy PierwszaAplikacja jest tworzony w metodzie main, od której rozpoczyna się wykonywanie kodu. Po uruchomieniu zobaczymy widok przedstawiony na rysunku 8.18. Rysunek 8.18. Wygląd prostej aplikacji Wraz z platformą Java 2 SE5 pojawił się nowy model obsługi zdarzeń dla biblioteki Swing, w którym operacje związane z komponentami (a okno aplikacji jest komponentem) nie powinny być obsługiwane bezpośrednio, ale mają trafiać do kolejki zdarzeń. Dotyczy to również samego uruchamiania aplikacji. Należy użyć metody invokeLater klasy SwingUtilities, która umieści wywołanie w kolejce zdarzeń. Argumentem tej metody musi być obiekt implementujący interfejs Runnable, a operacja, którą chce się wykonać, powinna się znaleźć w metodzie run tego interfejsu. Zgodnie z tym standardem metoda main powinna mieć postać: public static void main(String args[]) { SwingUtilities.invokeLater(new Runnable() { public void run() { new PierwszaAplikacja(); } }); } Ten sposób będzie wykorzystywany na listingach w dalszej części rozdziału. Zdarzenia związane z oknem Z każdym oknem związany jest zestaw zdarzeń reprezentowanych przez interfejs WindowListener. Interfejs ten definiuje metody, które zostały zebrane w tabeli 8.6. Oczywiście sama implementacja interfejsu to nie wszystko, należy dodatkowo poinformować system, że to właśnie dane okno ma odbierać wysyłane komunikaty, do czego doprowadzi wywołanie metody addWindowListener (por. metody addMouseListener i addMouseMotionListener z lekcji 40.). Zatem zamiast stosować metodę setDefault CloseOperation, można również w nieco inny sposób obsłużyć zdarzenie polegające na zamknięciu okna. Przykładowy kod klasy Aplikacja tworzącej okno reagujące na próby zamknięcia przy użyciu interfejsu WindowListener jest widoczny na listingu 8.27. 388 Java. Praktyczny kurs Tabela 8.6. Metody interfejsu WindowListener Deklaracja metody Opis Od JDK void windowActivated(WindowEvent e) Metoda wywoływana po aktywacji okna. 1.1 void windowClosed(WindowEvent e) Metoda wykonywana, kiedy okno zostanie zamknięte poprzez wywołanie metody dispose. 1.1 void windowClosing(WindowEvent e) Metoda wywoływana, kiedy następuje próba zamknięcia okna z menu systemowego. 1.1 void windowDeactivated(WindowEvent e) Metoda wywoływana po dezaktywacji okna. 1.1 void windowDeiconified(WindowEvent e) Metoda wywoływana, kiedy okno zmieni stan ze zminimalizowanego na normalny. 1.1 void windowIconified(WindowEvent e) Metoda wywoływana po minimalizacji okna. 1.1 void windowOpened(WindowEvent e) Metoda wywoływana po otwarciu okna. 1.1 Listing 8.27. import javax.swing.*; import java.awt.event.*; public class Aplikacja extends JFrame implements WindowListener { public Aplikacja() { addWindowListener(this); setSize(320, 200); setVisible(true); } public static void main(String args[]) { SwingUtilities.invokeLater(new Runnable() { public void run() { new Aplikacja(); } }); } public void windowClosing(WindowEvent e){ dispose(); } public void windowClosed(WindowEvent e){} public void windowOpened(WindowEvent e){} public void windowIconified(WindowEvent e){} public void windowDeiconified(WindowEvent e){} public void windowActivated(WindowEvent e){} public void windowDeactivated(WindowEvent e){} } Klasa Aplikacja dziedziczy po klasie JFrame i implementuje interfejs WindowListener. Wywołanie metody addWindowListener (z parametrem this wskazującym na obiekt aplikacji) w konstruktorze powoduje, że informacje o zdarzeniach będą przekazywane właśnie obiektowi aplikacji, czyli że będą wywoływane metody windowClosing, windowClosed, win dowOpened, windowIconified, windowDeiconified, windowActivated, windowDeactivated z klasy Aplikacja. Ponieważ interesuje nas jedynie obsługa zdarzenia polegającego na zamknięciu okna, wystarczy oprogramować metodę windowClosing. Jest ona wywoływana, kiedy użytkownik próbuje zamknąć okno poprzez wybranie odpowiedniej pozycji z menu systemowego bądź też poprzez kliknięcie odpowiedniej ikony paska tytułowego Rozdział 8. Aplikacje i aplety 389 okna. W takiej sytuacji wywoływana jest metoda dispose, która powoduje zwolnienie zasobów związanych z oknem, zamknięcie okna i jeżeli jest to ostatnie okno aplikacji, zakończenie pracy aplikacji. Obsługa zdarzeń przez klasy anonimowe W przykładzie z listingu 8.27 została przedstawiona aplikacja reagująca na zdarzenia związane z jej oknem. Konkretnie, była to aplikacja, która kończyła swoje działanie po wybraniu przez użytkownika odpowiedniej pozycji z menu systemowego lub też kliknięciu właściwej ikony z paska tytułowego. Było to możliwe dzięki implementacji interfejsu WindowListener bezpośrednio przez klasę okna. W takim wypadku konieczna była jednak deklaracja wszystkich metod klasy WindowListener, nawet tych, które nie były wykorzystywane. Zamiast tego wygodniej jest skorzystać z klasy adaptera, czyli specjalnej klasy zawierającej puste implementacje metod danego interfejsu. W przypadku interfejsu WindowListener jest to klasa WindowAdapter. Jeśli więc z WindowAdapter wyprowadzi się własną klasę i przesłoni w niej wybraną metodę, nie będzie konieczności definiowania pozostałych. Taka sytuacja została zobrazowana na listingu 8.28. Listing 8.28. import javax.swing.*; import java.awt.event.*; public class Aplikacja extends JFrame { class MyWindowAdapter extends WindowAdapter { public void windowClosing(WindowEvent e){ dispose(); } } public Aplikacja() { addWindowListener(new MyWindowAdapter()); setSize(320, 200); setVisible(true); } public static void main(String args[]) { SwingUtilities.invokeLater(new Runnable() { public void run() { new Aplikacja(); } }); } } Tym razem w klasie Aplikacja została zdefiniowana klasa wewnętrzna MyWindowAdapter, pochodna od WindowAdapter, a w niej metoda windowClosing. W tej metodzie wywoływana jest natomiast metoda dispose klasy Aplikacja. Jest to możliwe, jako że klasa wewnętrzna ma dostęp do metod klasy zewnętrznej (por. lekcja 28.). W konstruktorze klasy Aplikacja została wywołana metoda addWindowListener i został jej przekazany w postaci argumentu obiekt klasy MyWindowAdapter (addWindowListener(new MyWindow Adapter());). To nic innego jak informacja, że zdarzeniami związanymi z oknem będzie się zajmował właśnie ten obiekt. Tak więc za całą obsługę zdarzenia będzie odpowiadać klasa MyWindowAdapter. 390 Java. Praktyczny kurs Warto przy tym zauważyć, że ta klasa mogłaby być z powodzeniem klasą anonimową (por. lekcja 30.), jej nazwa w przedstawionej sytuacji tak naprawdę do niczego nie jest potrzebna. Program mógłby więc przyjąć postać widoczną na listingu 8.29. Listing 8.29. import javax.swing.*; import java.awt.event.*; public class Aplikacja extends JFrame { public Aplikacja() { addWindowListener( new WindowAdapter(){ public void windowClosing(WindowEvent e){ dispose(); } } ); setSize(320, 200); setVisible(true); } public static void main(String args[]) { //tutaj kod metody main } } Spójrzmy: w konstruktorze jest wywoływana metoda addWindowListener oznajmiająca, że zdarzenia związane z obsługą okna będą przekazywane obiektowi będącemu argumentem tej metody. Tym argumentem jest z kolei obiekt klasy anonimowej pochodnej od WindowAdapter. Ponieważ klasa anonimowa jest z natury rzeczy klasą wewnętrzną, ma ona dostęp do składowych klasy zewnętrznej — Aplikacja — i może wywołać metodę dispose zwalniającą zasoby i zamykającą okno aplikacji. Podobnie można postąpić przy obsłudze zdarzeń związanych z myszą. Jeśli potrzebna jest implementacja interfejsu MouseListener, należy skorzystać z klasy MouseAdapter, jeśli natomiast niezbędny jest interfejs MouseMotionListener, trzeba skorzystać z klasy MouseMotionAdapter. Oba te adaptery są zdefiniowane w pakiecie java.awt. Pakiet javax.swing.event udostępnia natomiast dodatkowy adapter zbiorczy implementujący wszystkie interfejsy związane z myszą. Jest to MouseInputAdapter. Wróćmy do listingu 8.21 z lekcji 40., do kodu apletu, który pokazywał aktualne współrzędne kursora myszy, i przeróbmy go w taki sposób, aby do obsługi zdarzeń był wykorzystywany obiekt anonimowej klasy dziedziczącej po MouseInputAdapter. Kod takiego apletu jest widoczny na listingu 8.30 (usunięta została jedynie istniejąca na listingu 8.21 obsługa metody mouseDragged). Listing 8.30. import import import import javax.swing.JApplet; javax.swing.event.MouseInputAdapter; java.awt.Graphics; java.awt.event.*; Rozdział 8. Aplikacje i aplety 391 public class Aplet extends JApplet { String tekst = ""; public void init() { addMouseMotionListener( new MouseInputAdapter(){ public void mouseMoved(MouseEvent evt) { tekst = "zdarzenie mouseMoved, "; tekst += "współrzędne: x = " + evt.getX() + ", "; tekst += "y = " + evt.getY(); repaint(); } } ); } public void paint (Graphics gDC) { gDC.clearRect(0, 0, getSize().width, getSize().height); gDC.drawString(tekst, 20, 20); } } Menu Rzadko która aplikacja okienkowa może obyć się bez menu. W Javie w celu dodania menu trzeba skorzystać z kilku klas: JMenuBar, JMenu i JMenuItem. Pierwsza z nich opisuje pasek menu, druga — menu znajdujące się na tym pasku, a trzecia — poszczególne elementy menu. Pasek menu dodaje się do okna aplikacji za pomocą metody setJMenuBar, natomiast menu do paska — przy użyciu metody add. Jak to wygląda w praktyce, zobrazowano na listingu 8.31. Listing 8.31. import javax.swing.*; public class Aplikacja extends JFrame { public Aplikacja() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JMenuBar mb = new JMenuBar(); JMenu menu1 = new JMenu("Menu 1"); JMenu menu2 = new JMenu("Menu 2"); JMenu menu3 = new JMenu("Menu 3"); mb.add(menu1); mb.add(menu2); mb.add(menu3); setJMenuBar(mb); setSize(320, 200); setVisible(true); } public static void main(String args[]) { SwingUtilities.invokeLater(new Runnable() { public void run() { new Aplikacja(); 392 Java. Praktyczny kurs } }); } } W konstruktorze tworzymy nowy obiekt klasy JMenuBar i przypisujemy go zmiennej mb. Następnie tworzymy trzy obiekty klasy JMenu i dodajemy je do paska menu (czyli obiektu mb) za pomocą metody add. W konstruktorze klasy JMenu przekazujemy nazwy menu, czyli tekst, który będzie przez nie wyświetlany. Pasek menu dodajemy do okna przez wywołanie metody setJMenuBar. Ostatecznie po skompilowaniu i uruchomieniu aplikacji na ekranie zobaczymy widok zaprezentowany na rysunku 8.19. Rysunek 8.19. Aplikacja z trzema pozycjami menu Do tak stworzonego menu należy dodać poszczególne pozycje. Służy do tego klasa JMenuItem. Obiekt tej klasy dodaje się do konkretnego menu za pomocą metody add. Jeśli zatem każde menu utworzone w aplikacji z listingu 8.31 ma mieć po dwie pozycje, konstruktor należy zmodyfikować w sposób przedstawiony na listingu 8.32. Listing 8.32. public Aplikacja() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JMenuBar mb = new JMenuBar(); JMenu menu1 = new JMenu("Menu 1"); JMenu menu2 = new JMenu("Menu 2"); JMenu menu3 = new JMenu("Menu 3"); JMenuItem menuItem11 = new JMenuItem("Pozycja 1"); JMenuItem menuItem12 = new JMenuItem("Pozycja 2"); JMenuItem menuItem21 = new JMenuItem("Pozycja 1"); JMenuItem menuItem22 = new JMenuItem("Pozycja 2"); JMenuItem menuItem31 = new JMenuItem("Pozycja 1"); JMenuItem menuItem32 = new JMenuItem("Pozycja 2"); menu1.add(menuItem11); menu1.add(menuItem12); menu2.add(menuItem21); menu2.add(menuItem22); menu3.add(menuItem31); menu3.add(menuItem32); Rozdział 8. Aplikacje i aplety 393 mb.add(menu1); mb.add(menu2); mb.add(menu3); setJMenuBar(mb); setSize(320, 200); setVisible(true); } Zostało utworzonych sześć różnych obiektów klasy JMenuItem odpowiadających poszczególnym pozycjom menu. Obiekty te zostają dołączone do kolejnych menu poprzez wywołanie metod add klasy JMenu. Przykładowo instrukcja menu1.add(menuItem11); powoduje dodanie pierwszej pozycji do pierwszego menu. Po wykonaniu wszystkich instrukcji powyższego kodu każde menu będzie miało po dwie pozycje o nazwach Pozycja 1 i Pozycja 2. Wygląd rozwiniętego menu został przedstawiony na rysunku 8.20. Rysunek 8.20. Menu z rozwiniętymi pozycjami Wiadomo już, jak tworzyć menu, warto więc teraz dowiedzieć się, w jaki sposób spowodować, aby program reagował na wybranie jednej z pozycji. Łatwo się domyślić, że trzeba będzie skorzystać z jakiegoś interfejsu typu MenuListener i przekazać zdarzenia do pewnego obiektu, być może obiektu aplikacji, być może obiektu dodatkowej klasy. Rzeczywiście tak należy postąpić. Nie ma jednak interfejsu o nazwie MenuListener, trzeba zatem skorzystać z interfejsu ActionListener. Jest w nim zdefiniowana tylko jedna metoda o nazwie actionPerformed. Do reagowania na wybór danej pozycji menu można więc zastosować technikę przedstawioną na listingu 8.33. Listing 8.33. import javax.swing.*; import java.awt.event.*; public class Aplikacja extends JFrame { private JMenuItem miZamknij; private ActionListener al = new ActionListener(){ public void actionPerformed(ActionEvent e) { if(e.getSource() == miZamknij) dispose(); } }; public Aplikacja() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 394 Java. Praktyczny kurs JMenuBar mb = new JMenuBar(); JMenu menu = new JMenu("Plik"); miZamknij = new JMenuItem("Zamknij"); menu.add(miZamknij); mb.add(menu); setJMenuBar(mb); miZamknij.addActionListener(al); setSize(320, 200); setVisible(true); } public static void main(String args[]) { //kod metody main } } Aplikacja ma w tej chwili jedynie menu Plik z jedną pozycją o nazwie Zamknij (widać to na rysunku 8.21). Sposób utworzenia menu jest analogiczny do wykorzystanego w poprzednich przykładach, z tą różnicą, że zmienna określająca pozycję menu Zamknij jest prywatnym polem klasy Aplikacja. Chodzi o to, aby mieć do tej pozycji dostęp nie tylko w obrębie konstruktora, ale w całej klasie. Po utworzeniu paska menu (obiekt mb), samego menu (obiekt menu) oraz pozycji (obiekt miZamknij) do tej pozycji dodaje się obsługę zdarzeń, wywołując instrukcję: miZamknij.addActionListener(al); Rysunek 8.21. Menu aplikacji z listingu 8.33 Tym samym zdarzenia związane z obsługą tego menu będą przekazywane do metody actionPerformed obiektu al. Obiekt ten jest obiektem klasy anonimowej implementującej interfejs ActionListener i został zdefiniowany na początku klasy Aplikacja. W metodzie actionPerformed sprawdzamy, czy obiektem, który wywołał zdarzenie, jest miZamknij, czyli pozycja menu o nazwie Zamknij. Jeśli tak, zamykamy okno, a tym samym całą aplikację (wywołanie metody dispose). Aby uzyskać referencję do obiektu, który wywołał zdarzenie, wywołujemy metodę getSource obiektu klasy ActionEvent otrzymanego jako argument metody actionPerformed. Pozycji menu (a dokładniej: każdemu komponentowi pochodnemu od java.awt.Button lub javax.swing.AbstractButton) można także przypisać komendę, dzięki której będzie można łatwo zidentyfikować źródło zdarzenia. Robi się to za pomocą metody setAction Rozdział 8. Aplikacje i aplety 395 Command. W argumencie należy przekazać wymyśloną nazwę komendy. Z kolei w klasie ActionEvent znajduje się metoda getActionCommand pozwalająca na ustalenie, jaka komenda jest powiązana z danym zdarzeniem. Aplikacja z listingu 8.33 mogłaby zatem być napisana również w sposób przedstawiony na listingu 8.34. Listing 8.34. import javax.swing.*; import java.awt.event.*; public class Aplikacja extends JFrame { private ActionListener al = new ActionListener(){ public void actionPerformed(ActionEvent e) { if("Zamknij".equals(e.getActionCommand())) dispose(); } }; public Aplikacja() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JMenuBar mb = new JMenuBar(); JMenu menu = new JMenu("Plik"); JMenuItem miZamknij = new JMenuItem("Zamknij"); miZamknij.setActionCommand("Zamknij"); menu.add(miZamknij); mb.add(menu); //dalsze instrukcje konstruktora } public static void main(String args[]) { //treść metody main } } Obiekt pozycji menu Zamknij jest tworzony na takiej samej zasadzie jak w poprzednim przykładzie, z tą różnicą, że nie ma teraz pola miZamknij w klasie Aplikacja. Zamiast niego używana jest jedynie tymczasowa zmienna referencyjna miZamknij (zostanie utracona po zakończeniu pracy konstruktora). Za pomocą wywołania setActionCommand obiektowi menu jest też przypisywana komenda Zamknij15, którą będzie można odczytać z obiektu obsługującego zdarzenie przekazywanego metodzie actionPreformed. Konstrukcja tej metody również przypomina tę z listingu 8.33, jednak inaczej wygląda instrukcja warunkowa. Tym razem sprawdzane jest, czy komenda pobrana za pomocą metody getActionCommand z klasy ActionEvent odpowiada ciągowi znaków Zamknij (czyli czy została wybrana pozycja menu z przypisaną komendą Zamknij). Jeśli tak, zostaje wywołana metoda dispose zamykająca okno i kończąca pracę aplikacji. 15 Tak naprawdę w tym konkretnym przypadku jest to czynność nadmiarowa, gdyż jeśli nie zostanie wywołana metoda setActionCommand, za komendę przypisaną zdarzeniu uznaje się tekst danej pozycji menu. 396 Java. Praktyczny kurs Menu kaskadowe Pozycje menu można łączyć kaskadowo i uzyskiwać w ten sposób rozbudowane struktury podmenu. Utworzenie tego typu konstrukcji jest możliwe poprzez dodanie do menu innego menu, czyli przekazanie metodzie add z klasy JMenu (w postaci argumentu) innego obiektu tej klasy. Przykład tego typu konstrukcji jest widoczny na listingu 8.35. Listing 8.35. import javax.swing.*; import java.awt.event.*; public class Aplikacja extends JFrame { private ActionListener al = new ActionListener(){ public void actionPerformed(ActionEvent e) { String text = ((JMenuItem)e.getSource()).getText(); System.out.println(text); } }; public Aplikacja() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JMenuBar mb = new JMenuBar(); JMenu menu = new JMenu("Menu 1"); JMenu submenu1 = new JMenu("Pozycja 4"); JMenu submenu2 = new JMenu("Pozycja 8"); menu.add(new JMenuItem("Pozycja 1")); menu.getItem(0).addActionListener(al); menu.add(new JMenuItem("Pozycja 2")); menu.getItem(1).addActionListener(al); menu.add(new JMenuItem("Pozycja 3")); menu.getItem(2).addActionListener(al); submenu1.add(new JMenuItem("Pozycja 5")); submenu1.getItem(0).addActionListener(al); submenu1.add(new JMenuItem("Pozycja 6")); submenu1.getItem(1).addActionListener(al); submenu1.add(new JMenuItem("Pozycja 7")); submenu1.getItem(2).addActionListener(al); submenu2.add(new JMenuItem("Pozycja 9")); submenu2.getItem(0).addActionListener(al); submenu2.add(new JMenuItem("Pozycja 10")); submenu2.getItem(1).addActionListener(al); submenu2.add(new JMenuItem("Pozycja 11")); submenu2.getItem(2).addActionListener(al); menu.add(submenu1); submenu1.add(submenu2); mb.add(menu); setJMenuBar(mb); setSize(320, 200); setVisible(true); } Rozdział 8. Aplikacje i aplety 397 public static void main(String args[]) { //kod metody main } } Zostały utworzone trzy różne menu (obiekty klasy JMenu) o nazwach menu, submenu1 i submenu2. Pierwsze menu otrzymuje etykietę Menu 1, drugie — Pozycja 4, a trzecie — Pozycja 8. Dlaczego akurat takie nazwy etykiet? Otóż każde z menu otrzymuje po trzy pozycje, a następnie do pierwszego dodaje się submenu1, a do niego — submenu2. Etykieta menu dodawanego staje się ostatnią pozycją menu, do którego zostało ono dodane. Jeśli więc etykietą submenu1 jest Pozycja 4, to po dodaniu submenu1 do menu etykieta Pozycja 4 staje się ostatnią pozycją w menu. Jeśli opis nadal nie jest do końca jasny, rysunek 8.22 powinien rozwiać wszelkie wątpliwości. Jest to ilustracja struktury menu z listingu 8.34. Rysunek 8.22. Menu kaskadowe z listingu 8.34 Obsługą zdarzeń zajmuje się, podobnie jak we wcześniej prezentowanych przykładach, obiekt klasy anonimowej implementującej interfejs ActionListener reprezentowany przez prywatne pole al z klasy Aplikacja. W związku z tym dla każdego obiektu reprezentującego daną pozycję menu jest wywoływana metoda addActionListener (addActionListener(al)). Wykorzystywany jest tu jednak inny sposób dostępu do tych obiektów. Ponieważ są one tworzone bezpośrednio w metodzie add klasy JMenu, np.: menu.add(new JMenuItem("Pozycja 1")); to aby otrzymać odpowiednią referencję, trzeba wywołać metodę getItem, podając jako argument indeks wybranego menu. Indeks pierwszego menu ma wartość 0, drugiego — 1 itd. Nowe instrukcje pojawiły się również w metodzie actionPerformed. Jej zadaniem jest wyświetlenie na konsoli nazwy menu, które zostało wybrane przez użytkownika. W celu pobrania tej nazwy jest wykonywana złożona instrukcja: String text = ((JMenuItem)e.getSource()).getText(); 398 Java. Praktyczny kurs Obiekt e to obiekt klasy ActionEvent zawierający wszystkie informacje o zdarzeniu. Metoda getSource pozwala na pobranie obiektu, który zapoczątkował dane zdarzenie, a więc obiektu pozycji menu wybranej przez użytkownika. Ponieważ typem wartości zwracanej przez getSource jest Object, dokonywane jest rzutowanie na typ JMenuItem, a potem następuje wywołanie metody getText, która zwraca nazwę wybranego menu (tekst danej pozycji, np. Pozycja 1, Pozycja 5). Uzyskany tekst jest wyświetlany na konsoli za pomocą instrukcji: System.out.println(text); W tym miejscu warto zwrócić uwagę na to, że taki sposób obsługi był możliwy, ponieważ jedynymi źródłami zdarzeń były obiekty związane z pozycjami menu, czyli klasy JMenuItem. W ramach ćwiczeń do samodzielnego przemyślenia można wykonać takie samo zadanie w sytuacji, kiedy źródłami zdarzeń są również inne komponenty niż JMenuItem. CheckBoxMenu Oprócz zwykłych menu zaprezentowanych na poprzednich stronach pakiet javax.swing oferuje też menu, które umożliwiają zaznaczanie poszczególnych pozycji (ang. checkbox menu). Za ich reprezentację odpowiada klasa o nazwie JCheckBoxMenuItem. Tworzenie tego typu menu odbywa się na takiej samej zasadzie jak zwykłych, z tą różnicą, że zamiast klasy JMenuItem używa się JCheckBoxMenuItem. Zmienić niestety trzeba również sposób obsługi zdarzeń. Należy wykorzystać dodatkowy interfejs o nazwie ItemListener. Definiuje on tylko jedną metodę o nazwie itemStateChanged, która jest wywoływana za każdym razem, kiedy zmieni się stan komponentu (w naszym przypadku stan pozycji menu). Kod aplikacji zawierającej przykładowe menu z możliwością zaznaczania poszczególnych pozycji został przedstawiony na listingu 8.36. Listing 8.36. import javax.swing.*; import java.awt.event.*; public class Aplikacja extends JFrame { private JCheckBoxMenuItem menuItem1, menuItem2; private ItemListener il = new ItemListener(){ public void itemStateChanged(ItemEvent e) { if(e.getSource() == menuItem1){ if(menuItem1.getState()){ System.out.println("Pozycja 1 jest zaznaczona."); } else{ System.out.println("Pozycja 1 nie jest zaznaczona."); } } else if(e.getSource() == menuItem2){ if(menuItem2.getState()){ System.out.println("Pozycja 2 jest zaznaczona."); } else{ Rozdział 8. Aplikacje i aplety 399 System.out.println("Pozycja 2 nie jest zaznaczona."); } } } }; public Aplikacja() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JMenuBar mb = new JMenuBar(); JMenu menu = new JMenu("Menu 1"); menuItem1 = new JCheckBoxMenuItem("Pozycja 1", true); menuItem1.addItemListener(il); menu.add(menuItem1); menuItem2 = new JCheckBoxMenuItem("Pozycja 2", false); menuItem2.addItemListener(il); menu.add(menuItem2); mb.add(menu); setJMenuBar(mb); setSize(320, 200); setVisible(true); } public static void main(String args[]) { //treść metody main } } Struktura menu jest tworzona w taki sam sposób, jak w przypadku elementów typu JMenuItem. Wykorzystuje się jedynie dodatkowy argument konstruktora klasy CheckBoxMenuItem, który określa, czy dana pozycja ma być zaznaczona (true), czy nie (false). Wygląd menu jest widoczny na rysunku 8.23. Rysunek 8.23. Menu umożliwiające zaznaczanie poszczególnych pozycji Obsługa zdarzeń przychodzących z obiektów klasy JCheckBoxMenuItem wymaga implementacji interfejsu ItemListener, a tym samym metody itemStateChanged. Metoda ta będzie wywoływana za każdym razem, kiedy nastąpi zmiana stanu danej pozycji menu, czyli kiedy zostanie ona zaznaczona lub jej zaznaczenie zostanie usunięte. Wtedy należy sprawdzić, która pozycja spowodowała wygenerowanie zdarzenia: menuItem1 czy menuItem2. Referencję uzyskuje się przez wywołanie metody getSource. Aby sprawdzić, 400 Java. Praktyczny kurs czy dana pozycja jest zaznaczona, czy nie, należy skorzystać z kolei z metody get State. Jeżeli zwróci ona wartość true, pozycja jest zaznaczona, jeśli false — nie jest. Menu kontekstowe Istnieje także możliwość wyposażenia aplikacji w menu kontekstowe. Należy wtedy skorzystać z klasy JPopupMenu. Konstrukcja taka jest bardzo podobna do konstrukcji zwyczajnego menu, z tą różnicą, że menu kontekstowego nie dodaje się do paska menu. Przykładowa aplikacja zawierająca menu kontekstowe została przedstawiona na listingu 8.37. Listing 8.37. import javax.swing.*; import java.awt.event.*; public class Aplikacja extends JFrame { private JPopupMenu popupMenu; private JMenuItem miPozycja1, miPozycja2, miZamknij; private ActionListener al = new ActionListener(){ public void actionPerformed(ActionEvent e) { if(e.getSource() == miZamknij) dispose(); } }; private MouseAdapter ma = new MouseAdapter(){ public void mousePressed(MouseEvent e) { if(e.isPopupTrigger()) popupMenu.show(e.getComponent(), e.getX(), e.getY()); } public void mouseReleased(MouseEvent e) { if(e.isPopupTrigger()) popupMenu.show(e.getComponent(), e.getX(), e.getY()); } }; public Aplikacja() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); popupMenu = new JPopupMenu(); miPozycja1 = new JMenuItem("Pozycja 1"); miPozycja2 = new JMenuItem("Pozycja 2"); miZamknij = new JMenuItem("Zamknij"); miPozycja1.addActionListener(al); miPozycja2.addActionListener(al); miZamknij.addActionListener(al); popupMenu.add(miPozycja1); popupMenu.add(miPozycja2); popupMenu.addSeparator(); popupMenu.add(miZamknij); addMouseListener(ma); setSize(320, 200); setVisible(true); Rozdział 8. Aplikacje i aplety 401 } public static void main(String args[]) { //treść metody main } } Obiekt menu kontekstowego tworzy się, wywołując konstruktor klasy JPopupMenu. Konstruktor ten może być bezargumentowy, tak jak na powyższym listingu, lub też może przyjmować jeden argument typu String. W tym drugim przypadku menu otrzyma identyfikującą je nazwę. Struktura menu jest tworzona dokładnie tak samo jak w przypadku wcześniej omawianych zwyczajnych menu — dodaje się po prostu kolejne obiekty klasy JMenuItem, które staną się pozycjami menu. Dodatkowo za pomocą wywołania metody addSeparator dodany został również separator pozycji menu. Otrzymaną strukturę przedstawiono na rysunku 8.24. Obsługa zdarzeń jest również taka sama jak w poprzednich przykładach. Obiektem obsługującym zdarzenia powiązanym z każdym z obiektów JMenuItem jest obiekt klasy anonimowej pochodnej od ActionListener. W metodzie actionPerformed sprawdzane jest, czy wybrana pozycja menu to Zamknij (czyli czy obiektem źródłowym zdarzenia jest miZamknij). Jeśli tak, wywołana zostaje metoda dispose obiektu aplikacji, która zwalnia zasoby związane z oknem i zamyka okno. Ponieważ jest to jedyne okno aplikacji, czynność ta jest równoznaczna z zakończeniem pracy całego programu. Rysunek 8.24. Aplikacja wyposażona w menu kontekstowe Aby jednak menu kontekstowe pojawiło się na ekranie, użytkownik aplikacji musi nacisnąć prawy klawisz myszy. To oznacza, że aplikacja musi reagować na zdarzenia związane z obsługą myszy, a jak wiadomo z lekcji 38., wymaga to implementacji interfejsu MouseListener. W tym celu został użyty obiekt klasy anonimowej pochodnej od MouseAdapter, dzięki czemu obsługiwane są metody mousePressed oraz mouseReleased. Jest to niezbędne, gdyż menu kontekstowe jest wywoływane w różny sposób w różnych systemach. W obu przypadkach metoda isPopupTrigger pozwala sprawdzić, czy zdarzenie jest wynikiem wywołania przez użytkownika menu kontekstowego. Jeśli tak (wywołanie isPopupTrigger zwróciło wartość true), menu jest wyświetlane w miejscu wskazywanym przez kursor myszy. Wyświetlenie menu odbywa się za pomocą metody show obiektu wskazywanego przez popupMenu. Pierwszym parametrem jest obiekt, na którego powierzchni ma się pojawić menu i względem którego będą obliczane współrzędne wyświetlania, przekazywane jako drugi i trzeci argument. W naszym przypadku pierwszym argumentem jest po pro- 402 Java. Praktyczny kurs stu obiekt aplikacji uzyskiwany przez wywołanie e.getComponent(). Metoda getCompo nent klasy MouseEvent zwraca bowiem obiekt, który zapoczątkował zdarzenie. Ćwiczenia do samodzielnego wykonania Ćwiczenie 41.1. Popraw kod z listingu 8.26 w taki sposób, aby wyświetlany tekst znajdował się w centrum okna aplikacji. Możesz skorzystać z rozwiązania podanego w lekcji poświęconej apletom. Ćwiczenie 41.2. Zmień kod z listingu 8.27 tak, aby obsługa zdarzeń odbywała się poprzez obiekt klasy wewnętrznej implementującej interfejs WindowListener. Ćwiczenie 41.3. Zmodyfikuj kod z listingu 8.27 tak, by obsługa zdarzeń odbywała się za pomocą obiektu niezależnej klasy pakietowej implementującej interfejs WindowListener. Ćwiczenie 41.4. Napisz aplikację zawierającą wielopoziomowe menu kontekstowe. Ćwiczenie 41.5. Napisz aplikację, która będzie wyświetlała w swoim oknie aktualne współrzędne kursora myszy. Ćwiczenie 41.6. Napisz aplikację zawierającą menu. Po wybraniu z niego dowolnej pozycji powinno pojawić się nowe okno (w tym celu utwórz nowy obiekt klasy JFrame). Okno to musi dać się zamknąć przez kliknięcie odpowiedniej ikony z paska tytułu. Lekcja 42. Komponenty Każda aplikacja okienkowa, oprócz w menu, których różne rodzaje pojawiły się w lekcji 41., jest wyposażona także w wiele innych elementów graficznych, takich jak przyciski, etykiety, pola tekstowe czy listy rozwijane16. Pakiet javax.swing zawiera odpowiedni zestaw klas, które pozwalają na zastosowanie tego rodzaju komponentów. Tych klas jest bardzo wiele. Część z nich zostanie przedstawiona w ostatniej, 42. lekcji. 16 Często spotyka się też termin „lista rozwijalna”. Rozdział 8. Aplikacje i aplety 403 Klasa Component Praktycznie wszystkie graficzne komponenty, które pozwalają na zastosowanie typowych elementów środowiska okienkowego, dziedziczą, pośrednio lub bezpośrednio, po klasie JComponent, a zatem są wyposażone w jej metody17. Metod tych jest dosyć dużo, ich pełną listę można znaleźć w dokumentacji JDK. W tabeli 8.7 zostały zebrane niektóre z nich, przydatne przy wykonywaniu typowych operacji. Większość została odziedziczona po klasie Component biblioteki AWT. W kolumnie Od JDK została zawarta informacja na temat tego, kiedy po raz pierwszy dana metoda pojawiła się w JDK. Wartości w nawiasach oznaczają natomiast, kiedy dana metoda została redefiniowana w klasie JComponent. Tabela 8.7. Wybrane metody klasy Component Deklaracja metody Opis Od JDK void addKeyListener (KeyListener l) Ustala obiekt, który będzie obsługiwał zdarzenia związane z klawiaturą. 1.1 void addMouseListener (MouseListener l) Ustala obiekt, który będzie obsługiwał zdarzenia związane z klikaniem przyciskami myszy. 1.1 void addMouseMotionListener (MouseMotionListener l) Ustala obiekt, który będzie obsługiwał zdarzenia związane z ruchami myszy. 1.1 boolean contains(int x, int y) Sprawdza, czy komponent zawiera punkt o współrzędnych x, y. 1.1 boolean contains(Point p) Sprawdza, czy komponent zawiera punkt wskazywany przez argument p. 1.1 Color getBackground() Pobiera kolor tła komponentu. 1.0 Rectangle getBounds() Zwraca rozmiar komponentu w postaci obiektu klasy Rectangle. 1.0 Cursor getCursor() Zwraca kursor związany z komponentem. 1.1 Font getFont() Zwraca font związany z komponentem. 1.0 FontMetrics getFontMetrics (Font font) Zwraca obiekt opisujący właściwości fontu określonego przez argument font. 1.0 (1.5) Color getForeground() Zwraca pierwszoplanowy kolor komponentu. 1.0 Graphics getGraphics() Zwraca tzw. graficzny kontekst urządzenia, obiekt pozwalający na wykonywanie operacji graficznych na komponencie. 1.0 (1.2) int getHeight() Zwraca wysokość komponentu. 1.2 String getName() Zwraca nazwę komponentu. 1.1 Container getParent() Zwraca obiekt nadrzędny komponentu. 1.0 Dimension getSize() Zwraca rozmiary komponentu w postaci obiektu klasy Dimension. 1.1 int getWidth() Zwraca szerokość komponentu. 1.2 int getX() Zwraca współrzędną x położenia komponentu. 1.2 17 A także klas, po których dziedziczy JComponent. 404 Java. Praktyczny kurs Tabela 8.7. Wybrane metody klasy Component (ciąg dalszy) Deklaracja metody Opis Od JDK int getY() Zwraca współrzędną y położenia komponentu. 1.2 void repaint() Odrysowuje cały obszar komponentu. 1.0 void repaint(int x, int y, int width, int height) Odrysowuje wskazany obszar komponentu. 1.0 void setBackground(Color c) Ustala kolor tła komponentu. 1.0 (1.2) void setBounds(int x, int y, int width, int height) Ustala rozmiar i położenie komponentu. 1.1 void setBounds(Rectangle r) Ustala rozmiar i położenie komponentu. 1.1 void setCursor(Cursor cursor) Ustawia rodzaj kursora przypisany komponentowi. 1.1 void setFont(Font f) Ustawia font przypisany komponentowi. 1.0 (1.2) void setLocation(int x, int y) Zmienia położenie komponentu. 1.1 void setLocation(Point p) Zmienia położenie komponentu. 1.1 void setName(String name) Ustala nazwę komponentu. 1.1 void setSize(Dimension d) Ustala rozmiary komponentu. 1.1 void setSize(int width, int height) Ustala rozmiary komponentu. 1.1 void setVisible(boolean b) Wyświetla lub ukrywa komponent. 1.0 (1.2) Etykiety Etykiety tekstowe to jedne z najprostszych komponentów graficznych. Umożliwiają wyświetlanie tekstu. Aby utworzyć etykietę, należy skorzystać z klasy JLabel. Konstruktor klasy JLabel może być bezargumentowy, tworzy wtedy pustą etykietę, może też przyjmować jako argument tekst, który ma być na niej wyświetlany. Po utworzeniu etykiety znajdujący się na niej tekst można pobrać za pomocą metody getText, natomiast aby go zmienić, trzeba skorzystać z metody setText. Etykietę umieszcza się w oknie lub na innym komponencie, wywołując metodę add. Prosty przykład obrazujący wykorzystanie klasy JLabel jest widoczny na listingu 8.38, natomiast efekt jego działania na rysunku 8.25. Listing 8.38. import javax.swing.*; public class Aplikacja extends JFrame { public Aplikacja() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(null); JLabel label1 = new JLabel("Pierwsza etykieta"); label1.setBounds(100, 40, 120, 20); JLabel label2 = new JLabel(); label2.setText("Druga etykieta"); label2.setBounds(100, 60, 120, 20); Rozdział 8. Aplikacje i aplety 405 add(label1); add(label2); setSize(320, 200); setVisible(true); } public static void main(String args[]) { SwingUtilities.invokeLater(new Runnable() { public void run() { new Aplikacja(); } }); } } Rysunek 8.25. Wygląd przykładowych etykiet tekstowych Aplikacja wykorzystuje oba wymienione wyżej sposoby tworzenia etykiet. W pierwszym przypadku (obiekt label1) już w konstruktorze jest przekazywany tekst, jaki ma być wyświetlany. W drugim przypadku (obiekt label2) tworzona etykieta jest pusta, a tekst jest jej przypisywany za pomocą metody setText. Rozmiary oraz położenie etykiet są ustalane dzięki metodzie setBounds. Pierwsze dwa argumenty tej metody określają współrzędne x i y, natomiast dwa kolejne — szerokość oraz wysokość. Etykiety są dodawane do okna aplikacji za pomocą instrukcji: add(label1); add(label2); Metoda setLayout ustala rozkład elementów w oknie. Przekazany jej argument null oznacza, że rezygnujemy z automatycznego rozmieszczania elementów i będziemy je pozycjonować ręcznie18. W tym przypadku jest to jednoznaczne z tym, że argumenty metody setBounds będą traktowane jako bezwzględne współrzędne okna aplikacji. Przyciski Za obsługę i wyświetlanie przycisków odpowiada klasa JButton. Podobnie jak w przypadku klasy JLabel, konstruktor może być bezargumentowy, powstaje wtedy przycisk bez napisu na jego powierzchni, może również przyjmować argument klasy String. W tym drugim przypadku przekazany napis pojawi się na przycisku. Jeśli zostanie zastosowany konstruktor bezargumentowy, późniejsze przypisanie tekstu przyciskowi będzie 18 Czytelnicy zainteresowani rozkładami automatycznymi powinni zapoznać się z opisem klasy LayoutManager oraz klas pochodnych, a także z opisem metody setLayout klasy Container. 406 Java. Praktyczny kurs możliwe za pomocą metody setText. W odróżnieniu od etykiet przyciski powinny jednak reagować na kliknięcia myszą, przy ich stosowaniu niezbędne będzie zatem zaimplementowanie interfejsu ActionListener. Przykładowa aplikacja zawierająca dwa przyciski, taka, że po kliknięciu drugiego z nich nastąpi jej zamknięcie, jest widoczna na listingu 8.39, natomiast wygląd okna został zobrazowany na rysunku 8.26. Listing 8.39. import javax.swing.*; import java.awt.event.*; public class Aplikacja extends JFrame { private JButton button1, button2; public Aplikacja() { ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e) { if(e.getSource() == button2) dispose(); } }; setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(null); button1 = new JButton("Pierwszy przycisk"); button1.setBounds(80, 40, 160, 20); button1.addActionListener(al); button2 = new JButton(); button2.setText("Drugi przycisk"); button2.setBounds(80, 80, 160, 20); button2.addActionListener(al); add(button1); add(button2); setSize(320, 200); setVisible(true); } public static void main(String args[]) { //kod metody main } } Rysunek 8.26. Aplikacja zawierająca dwa przyciski Rozdział 8. Aplikacje i aplety 407 Pierwszy przycisk jest tworzony za pomocą konstruktora przyjmującego w argumencie obiekt klasy String, czyli po prostu ciąg znaków. W drugim przypadku do ustawiania napisu wyświetlanego na przycisku wykorzystuje się metodę setText. Do ustalenia położenia i rozmiarów przycisków używa się natomiast metody setBounds, której działanie jest identyczne z omawianym w przypadku przedstawionych wcześniej etykiet. Ponieważ przyciski muszą reagować na kliknięcia, tworzony jest nowy obiekt (al) anonimowej klasy implementującej interfejs ActionListener. W metodzie actionPerformed sprawdzamy, czy źródłem zdarzenia był drugi przycisk (if(e.getSource() == button2)). Jeśli tak, zamykamy okno i kończymy działanie aplikacji (wywołanie metody dispose). Pola tekstowe Istnieje kilka rodzajów pól tekstowych: JTextField, JPasswordField, JFormattedText Field, JTextArea, JEditorPane i JTextPane. Wszystkie te klasy dziedziczą bezpośrednio bądź pośrednio po klasie JTextComponent, która z kolei dziedziczy po JComponent. Nie ma niestety miejsca na dokładne omówienie wszystkich tych komponentów, przedstawione zostaną więc jedynie przykłady użycia dwóch podstawowych typów: JTextField oraz JTextArea. Pierwszy z nich pozwala na wprowadzenie tekstu w jednej linii, drugi — tekstu wielowierszowego. Ich obsługa, jak okaże się za chwilę, jest podobna. Klasa JTextField Klasa JTextField tworzy jednowierszowe pole tekstowe, takie jak zaprezentowane na rysunku 8.27. Oferuje ona pięć konstruktorów przedstawionych w tabeli 8.8. Przykład wykorzystujący pole tekstowe jest widoczny na listingu 8.40. Tabela 8.8. Konstruktory klasy JTextField Konstruktor Opis JTextField() Tworzy nowe, puste pole tekstowe. JTextField(Document doc, String text, int columns) Tworzy nowe pole tekstowe o zadanej liczbie kolumn i zawartości, zgodne z modelem dokumentu wskazanym przez argument doc. JTextField(int columns) Tworzy nowe pole tekstowe o zadanej liczbie kolumn. JTextField(String text) Tworzy nowe pole tekstowe zawierające wskazany tekst. JTextField(String text, int columns) Tworzy nowe pole tekstowe o zadanej liczbie kolumn zawierające wskazany tekst. Listing 8.40. import javax.swing.*; import java.awt.event.*; public class Aplikacja extends JFrame { private JTextField textField; private JButton button1; public Aplikacja() { ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e) { if(e.getSource() == button1) 408 Java. Praktyczny kurs setTitle(textField.getText()); } }; setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(null); textField = new JTextField(); textField.setBounds(100, 50, 100, 20); button1 = new JButton("Kliknij!"); button1.setBounds(100, 80, 100, 20); button1.addActionListener(al); add(button1); add(textField); setSize(320, 200); setVisible(true); } public static void main(String args[]) { //kod metody main } } Rysunek 8.27. Tekst z pola tekstowego stanie się tytułem okna aplikacji Pole tekstowe tworzymy za pomocą instrukcji, których znaczenie nie powinno budzić żadnych wątpliwości. Jego szerokość ustawiamy na 100 pikseli, a wysokość na 20. Do dyspozycji mamy również przycisk, którego kliknięcie będzie powodowało, że tekst znajdujący się w polu stanie się tytułem okna aplikacji. Do obsługi zdarzeń wykorzystujemy interfejs ActionListener i metodę actionPerformed, podobnie jak miało to miejsce w poprzednich przykładach. Tekst zapisany w polu tekstowym odczytujemy za pomocą metody getText, natomiast do zmiany tytułu okna aplikacji wykorzystujemy metodę setTitle. Sytuacja nieco się komplikuje, jeśli chcemy mieć możliwość reagowania na każdą zmianę tekstu, jaka zachodzi w polu tekstowym JTextField. Otóż należy monitorować zmiany dokumentu (obiektu klasy Document) powiązanego z komponentem JText Field. Trzeba zatem skorzystać z interfejsu DocumentListener. Najpierw tworzy się nowy obiekt implementujący ten interfejs, a następnie przekazuje się go jako argument metody setDocumentListener wywołanej na rzecz obiektu zwróconego przez wywołanie Rozdział 8. Aplikacje i aplety 409 getDocument klasy JTextField. Przy założeniu, że obiektem implementującym interfejs DocumentListener jest dl, a pole tekstowe reprezentuje obiekt textField, wywołanie po- winno mieć postać: textField.getDocument().addDocumentListener(dl) W interfejsie DocumentListener zdefiniowane zostały trzy metody: changedUpdate — wywoływana po zmianie atrybutu lub atrybutów; insertUpdate — wywoływana przy wstawianiu treści do dokumentu; removeUpdate — wywoływana przy usuwaniu treści z dokumentu. Aby zatem napisać aplikację zawierającą pole tekstowe, taką, że każda zmiana w tym polu powodowałaby zmianę napisu znajdującego się na pasku tytułowym okna, można wykorzystać kod przedstawiony na listingu 8.41. Listing 8.41. import javax.swing.*; import javax.swing.event.*; public class Aplikacja extends JFrame { private JTextField textField; public Aplikacja() { DocumentListener dl = new DocumentListener() { public void changedUpdate(DocumentEvent e) { setTitle(textField.getText()); } public void insertUpdate(DocumentEvent e) { setTitle(textField.getText()); } public void removeUpdate(DocumentEvent e) { setTitle(textField.getText()); } }; setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(null); textField = new JTextField(); textField.setBounds(100, 50, 100, 20); textField.getDocument().addDocumentListener(dl); add(textField); setSize(320, 200); setVisible(true); } public static void main(String args[]) { //kod metody main } } 410 Java. Praktyczny kurs Klasa JTextArea Klasa JTextArea pozwala na tworzenie komponentów umożliwiających wprowadzenie większej ilości tekstu. Oferuje ona pięć konstruktorów przedstawionych w tabeli 8.9. Przykład wykorzystania obiektu tej klasy jest widoczny na listingu 8.42. Tabela 8.9. Konstruktory klasy JTextArea Konstruktor Opis JTextArea() Tworzy pusty komponent JTextArea. JTextArea(Document doc) Tworzy nowe pole tekstowe zgodne z modelem dokumentu wskazanym przez argument doc. JTextArea(Document doc, String text, int rows, int columns) Tworzy nowe pole tekstowe zgodne z modelem dokumentu wskazanym przez argument doc, o zadanej zawartości oraz określonej liczbie wierszy i kolumn. JTextArea(int rows, int columns) Tworzy pusty komponent JTextArea o określonej liczbie wierszy i kolumn. JTextArea(String text) Tworzy komponent JTextArea zawierający tekst określony przez argument text. JTextArea(String text, int rows, int columns) Tworzy komponent JTextArea zawierający tekst określony przez argument text, o liczbie wierszy i kolumn określonej argumentami rows i columns. Listing 8.42. import javax.swing.*; import java.awt.event.*; import java.io.*; public class Aplikacja extends JFrame { private JTextArea textArea; private JButton button1; public Aplikacja() { ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e) { if(e.getSource() == button1) save(); } }; setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(null); textArea = new JTextArea(); textArea.setBounds(30, 30, 260, 200); this.add(textArea); button1 = new JButton("Zapisz"); button1.setBounds(100, 240, 100, 20); button1.addActionListener(al); add(textArea); add(button1); Rozdział 8. Aplikacje i aplety 411 setSize(320, 300); setVisible(true); } public void save() { FileWriter fileWriter = null; String text = textArea.getText(); try{ fileWriter = new FileWriter("test.txt", true); fileWriter.write(text, 0, text.length()); fileWriter.close(); } catch(IOException e){ //System.out.println("Błąd podczas zapisu pliku."); } } public static void main(String args[]) { //treść metody main } } Tworzony jest jeden komponent klasy JTextArea (reprezentowany przez pole textArea) oraz jeden przycisk, tak jak widać na rysunku 8.28. Aplikacja działa w taki sposób, że po kliknięciu przycisku cały tekst z pola tekstowego zostaje zapisany w pliku o nazwie test.txt (jeżeli pliku nie będzie na dysku, zostanie utworzony, a jeśli będzie, zawartość pola zostanie dopisana na jego końcu). Obsługę zdarzeń związanych z klikaniem przycisku zapewnia obiekt klasy anonimowej implementującej interfejs ActionListener, dokładnie tak samo jak w przypadku wcześniej prezentowanych przykładów. Po kliknięciu przycisku jest zatem wywoływana metoda actionPerformed, która sprawdza, czy sprawcą zdarzenia jest rzeczywiście przycisk button1. Jeśli tak, wywoływana jest metoda save z klasy Aplikacja. Metoda save korzysta z obiektu klasy FileWriter do zapisania zawartości komponentu textArea w pliku (por. lekcja 34.). Rysunek 8.28. Aplikacja wykorzystująca pole tekstowe klasy TextArea Pola JCheckBox Klasa JCheckBox tworzy komponenty będące polami wyboru, które umożliwiają zaznaczanie opcji. Konstruktory klasy zostały przedstawione w tabeli 8.10, a program wyświetlający sześć pól typu JCheckBox pokazano na listingu 8.43. Wygląd okna takiej aplikacji jest widoczny na rysunku 8.29. 412 Java. Praktyczny kurs Tabela 8.10. Konstruktory klasy JCheckBox Konstruktor Opis JCheckBox() Tworzy niezaznaczone pole wyboru, bez ikony i przypisanego tekstu. JCheckBox(Action a) Tworzy nowe pole wyboru o właściwościach wskazanych przez argument a. JCheckBox(Icon icon) Tworzy niezaznaczone pole wyboru z przypisaną ikoną. JCheckBox(Icon icon, boolean selected) Tworzy pole wyboru z ikoną, jego stan jest określony przez argument selected. JCheckBox(String text) Tworzy niezaznaczone pole wyboru z przypisanym tekstem. JCheckBox(String text, boolean selected) Tworzy pole wyboru z przypisanym tekstem, którego stan jest określony przez argument selected. JCheckBox(String text, Icon icon) Tworzy niezaznaczone pole wyboru z przypisaną ikoną oraz tekstem. JCheckBox(String text, Icon icon, boolean selected) Tworzy pole wyboru, z przypisaną ikoną i tekstem, którego stan jest określony przez argument selected. Listing 8.43. import javax.swing.*; import java.awt.*; public class Aplikacja extends JFrame { public Aplikacja() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new GridLayout(3, 2)); JCheckBox JCheckBox JCheckBox JCheckBox JCheckBox JCheckBox cb1 cb2 cb3 cb4 cb5 cb6 = = = = = = new new new new new new JCheckBox("Opcja JCheckBox("Opcja JCheckBox("Opcja JCheckBox("Opcja JCheckBox("Opcja JCheckBox("Opcja 1"); 2"); 3"); 4"); 5"); 6"); add(cb1); add(cb2); add(cb3); add(cb4); add(cb5); add(cb6); setSize(320, 200); setVisible(true); } public static void main(String args[]) { //kod metody main } } Rozdział 8. Aplikacje i aplety 413 Rysunek 8.29. Pola JCheckBox umieszczone w oknie aplikacji korzystającej z rozkładu siatkowego Od razu widać pewną różnicę w stosunku do poprzednich przykładów: tym razem nie są podawane żadne współrzędne miejsc, w których miałyby się znaleźć poszczególne elementy okna. Zamiast tego zastosowano jeden z rozkładów automatycznych, tzw. rozkład tabelaryczny lub siatkowy (ang. grid layout). Za jego ustawienie odpowiada linia: setLayout(new GridLayout(3, 2)); Parametrem metody setLayout jest nowy obiekt klasy GridLayout. Dzięki temu powierzchnia okna aplikacji zostanie podzielona na sześć części, tabelę o trzech wierszach i dwóch kolumnach. Komponenty — dodawane do okna aplikacji — będą od tej chwili automatycznie umieszczane w kolejnych komórkach takiej tabeli, dzięki czemu uzyskamy ich równomierny rozkład. Listy rozwijane Tworzenie list rozwijalnych umożliwia klasa o nazwie JComboBox. Jej konstruktory zostały przedstawione w tabeli 8.11, a wybrane metody w tabeli 8.12. Oczywiście w rzeczywistości metod jest o wiele więcej, jednak te zaprezentowane poniżej umożliwiają wykonywanie podstawowych operacji. Przykład użycia listy został zobrazowany w kodzie widocznym na listingu 8.44. Tabela 8.11. Konstruktory klasy JComboBox Konstruktor Opis JComboBox() Tworzy pustą listę. JComboBox(ComboBoxModel aModel) Tworzy nową listę z modelem danych wskazanym przez argument aModel. JComboBox(Object[] items) Tworzy listę zawierającą dane znajdujące się w tablicy items. JComboBox(Vector<?> items) Tworzy listę zawierającą dane znajdujące się w kolekcji items. Listing 8.44. import javax.swing.*; import java.awt.event.*; public class Aplikacja extends JFrame { JComboBox<String> cmb; JLabel label; 414 Java. Praktyczny kurs Tabela 8.12. Wybrane metody klasy JComboBox Deklaracja metody Opis void addItem(Object anObject) Dodaje nową pozycję do listy. Object getItemAt(int index) Pobiera element listy znajdujący się pod wskazanym indeksem. int getItemCount() Pobiera liczbę elementów listy. int getSelectedIndex() Pobiera indeks zaznaczonego elementu. Object getSelectedItem() Pobiera zaznaczony element. void insertItemAt(Object anObject, int index) Wstawia nowy element we wskazanej pozycji listy. boolean isEditable() Zwraca true, jeśli istnieje możliwość edycji listy. void removeAllItems() Usuwa wszystkie elementy listy. void removeItem (Object anObject) Usuwa wskazany element listy. void removeItemAt(int anIndex) Usuwa element listy znajdujący się pod wskazanym indeksem. void setEditable (boolean aFlag) Ustala, czy lista ma mieć możliwość edycji. void setSelectedIndex (int anIndex) Zaznacza element listy znajdujący się pod wskazanym indeksem. void setSelectedItem (Object anObject) Zaznacza wskazany element listy. public Aplikacja() { ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e) { int itemIndex = cmb.getSelectedIndex(); if(itemIndex < 1){ return; } String itemText = cmb.getSelectedItem().toString(); label.setText(itemText); } }; setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(null); label = new JLabel("Wybierz pozycję z listy"); label.setBounds(70, 10, 190, 20); cmb = new JComboBox<String>(); cmb.setBounds (70, 40, 190, 20); cmb.addItem("Wybierz książkę..."); cmb.addItem("Java. Ćwiczenia praktyczne"); cmb.addItem("Java. Ćwiczenia zaawansowane"); cmb.addItem("Java. Tablice informatyczne"); cmb.addItem("Java. Leksykon kieszonkowy"); cmb.addActionListener(al); Rozdział 8. Aplikacje i aplety 415 add(cmb); add(label); setSize(340, 200); setVisible(true); } public static void main(String args[]) { //kod metody main } } Została tu utworzona aplikacja zawierająca jedną listę rozwijaną oraz jedną etykietę, tak jak jest to widoczne na rysunku 8.30. Działanie programu jest następujące: po wybraniu z listy nowej pozycji przypisany jej tekst pojawi się na etykiecie. Rysunek 8.30. Wygląd aplikacji z listingu 8.44 Sposób utworzenia listy oraz etykiety nie powinien budzić żadnych wątpliwości, odbywa się to podobnie jak we wcześniejszych przykładach z bieżącej lekcji. W przypadku listy została użyta składnia typów uogólnionych, dzięki czemu wiadomo, że na liście będą umieszczane elementy typu String (ciągi znaków). Elementy listy są dodawane za pomocą metody addItem z klasy JComboBox. Zdarzenia nadchodzące z listy obsługuje obiekt klasy anonimowej implementującej interfejs ActionListener (obiekt reprezentowany przez zmienną al). Jest w nim oczywiście zdefiniowana tylko jedna metoda — actionPerformed. Za pomocą wywołania cmb.getSelectedIndex() pobiera ona najpierw indeks zaznaczonego elementu listy. Jeśli okaże się, że indeks ten jest mniejszy od 1 (co by oznaczało, że albo nie został zaznaczony żaden z elementów, albo też zaznaczony jest element o indeksie 0), metoda kończy działanie, wywołując instrukcję return. Jeśli indeks jest większy od 0, pobierany jest tekst przypisany zaznaczonemu elementowi listy: String itemText = cmb.getSelectedItem().toString(); który następnie jest używany jako argument metody setText obiektu etykiety: label.setText(itemText); 416 Java. Praktyczny kurs Ćwiczenia do samodzielnego wykonania Ćwiczenie 42.1. Napisz aplikację zawierającą pole tekstowe, etykietę i przycisk. Po kliknięciu przycisku zawartość pola tekstowego powinna się znaleźć na etykiecie. Ćwiczenie 42.2. Zmodyfikuj kod z listingu 8.39 tak, aby zamknięcie aplikacji następowało jedynie po kliknięciu przycisków w kolejności: Pierwszy przycisk, Drugi przycisk. Ćwiczenie 42.3. Do aplikacji z listingu 8.44 dodaj przycisk. Po jego kliknięciu powinny zostać usunięte wszystkie elementy listy. Ćwiczenie 42.4. Napisz aplikację pozwalającą użytkownikowi na dodawanie i usuwanie elementów listy rozwijanej. Ćwiczenie 42.5. Napisz aplikację umożliwiającą prostą edycję plików tekstowych (wczytywanie, zapisywanie, modyfikację treści). Skorowidz A adapter MouseInputAdapter, 390 aplety, 351, 354 konstrukcja, 355 parametry, 356 tworzenie, 352 aplikacja, 351, 386 Eclipse, 6 jEdit, 6 NetBeans, 6 Notepad++, 6 argumenty finalne, 162 konstruktorów, 111 metod, 100 ASCII, 270 B blok default, 56 else, 48 finally, 192 switch, 55 try…catch, 165, 169–171, 180 błąd arytmetyczny, 179 implementacji interfejsu, 234 kompilacji, 31, 39, 127, 143, 197, 256 w pętli while, 274 w programie, 275 bufor, 315 C ciąg formatujący, 289 znaków quit, 330 czcionki, 359 D deklaracja, 24 metody, 120 tablicy, 77, 80, 85 typu uogólnionego, 345 zmiennej typu klasowego, 93 deklaracje proste, 24 wielu zmiennych, 25 dekrementacja, 35 domyślna strona kodowa, 293 domyślne wartości typów danych, 99 dookreślanie typu, 341 dostęp chroniony, protected, 133 do katalogu, 298 do klasy wewnętrznej, 253 do klasy zewnętrznej, 245 do obiektu, 248 do składowych, 261 prywatny, private, 131 publiczny, public, 130 dynamiczna zmiana pojemności, 327 dynamiczny wektor elementów, 328 działania arytmetyczne, 34 działanie etykietowanej instrukcji break, 72 etykietowanej instrukcji continue, 70, 72 konstruktorów odziedziczonych, 126 operatora dekrementacji, 37 operatora inkrementacji, 36 pętli for, 77 przeciążonej metody, 203 dziedziczenie, 121, 122 interfejsów, 235, 238 po klasie zewnętrznej, 252 dzielenie przez zero, 173 dźwięki, 383 418 Java. Praktyczny kurs E edytor tekstowy, 6 elementy tablic, 76 elipsa, 369 enkapsulacja, 134 etykiety, 71 puste, 405 tekstowe, 404, 405 F figury geometryczne, 366 finalne argumenty metod, 162 klasy, 156 metody, 161 pola, 157, 159 formatowanie, 290 G grafika, 366 H hermetyzacja, 134 hierarchia klas, 224 wyjątków, 176 if…else if, 51 Math.sqrt(), 50 return, 97 switch, 53–57 System.out.print, 33, 87 System.out.println, 28 throw, 183, 190 instrukcje sterujące, 47 interfejs, 222, 224 ActionListener, 393, 397 AudioClip, 383 DocumentListener, 408 Drawable, 224, 228, 229 Iterable, 333–335 Iterator, 335 jako typ zwracany, 254 MouseListener, 376–380, 385, 390, 401 MouseMotionListener, 380 WindowListener, 387 interfejsy funkcjonalne, 261, 264 J jawna konwersja typu, 197 JDK, Java Development Kit, 6 JRE, Java Runtime Environment, 6, 10 K I iloczyn logiczny, 43 implementacja interfejsu, 223 interfejsu MouseListener, 401 wielu interfejsów, 230 importowanie pakietu, 271 inferowanie typu, 341 informacje o kliknięciach, 379 o stanie załadowania obrazu, 375 inicjalizacja obiektu, 110 pól finalnych, 160 tablicy, 80, 85 inkrementacja, 35 instalacja JDK, 9 w systemie Linux, 11 w systemie Windows, 10 instrukcja, 23 break, 55, 66–68 continue, 68–71 if…else, 47 katalogi, 298, 304 pobranie zawartości, 298 tworzenie, 302 usuwanie, 304 klasa, 17, 91 ActionEvent, 394, 398 Aplet, 365 Applet, 352, 383 ArrayList, 328 BufferedInputStream, 321 BufferedOutputStream, 321 BufferedReader, 272, 320 CheckBoxMenu, 398 Color, 363 Component, 403 Console, 288, 289 Dane, 118 DivideByZeroException, 190 Double, 278 Exception, 183 File, 295, 302 FileInputStream, 316 FileOutputStream, 318 FileReader, 316, 318 FileWriter, 318, 411 Font, 359 FontMetrics, 361 Skorowidz Frame, 386 GeneralException, 189 Graphics, 362, 366 GraphicsEnvironment, 360 ImageObserver, 375 InputStream, 270, 271, 272 Integer, 277 JApplet, 352, 374 JButton, 405 JCheckBox, 411 JCheckBoxMenuItem, 398, 399 JComboBox, 413, 415 JFrame, 386 JLabel, 404 JMenu, 391, 396 JMenuBar, 391 JMenuItem, 391, 392 JPopupMenu, 400 JTextArea, 410 JTextComponent, 407 JTextField, 407, 409 Main, 94 Matcher, 301 MouseEvent, 377, 382 MouseInputAdapter, 390 MouseMotionAdapter, 390 MyFilenameFilter, 300, 301 MyMouseListener, 380 MyWindowAdapter, 389 Object, 201 OutputStreamWriter, 293 Pattern, 301 Polygon, 372 PrintStream, 291 PrintWriter, 293, 294 Punkt, 93, 103 Punkt3D, 124 RandomAccessFile, 307–312 Reader, 272 RuntimeException, 176, 177 Scanner, 286–288 Shape, 208, 213 Stack, 331 StreamTokenizer, 279–286 SwingUtilities, 387 Tablica, 167 uruchomieniowa, 108 WindowAdapter, 389 klasy abstrakcyjne, 212, 223 anonimowe, 257, 264 bazowe, 122 chronione, 251 finalne, 156 kontenerowe, 328 pakietowe, 141, 249, 250 419 pochodne, 225 potomne, 122 prywatne, 251 publiczne, 141, 250 statyczne, 260 statyczne zagnieżdżone, 239, 260 wewnętrzne, 141, 238, 240, 246, 249 wewnętrzne lokalne, 244 zagnieżdżone, 254 zewnętrzne, 241, 245 klasyfikacja klas, 250 kod ASCII, 270 pośredni, 15 Unicode, 293 kodowanie, 292 kolory, 362 koło, 369 komenda java, 13 javac, 13 komentarz, 19 blokowy, 20 liniowy, 21 kompilacja, 14, 15 kompilacja wyrażenia regularnego, 301 kompilator, 6, 15 kompilator javac, 7, 14 komponenty, 402 komunikat o błędzie, 175, 237 konflikt nazw, 232, 237, 238 koniec pliku, 314 konsola, 288 konsola systemowa, 12 konstrukcja apletu, 355 konstruktor, 110, 212 argumenty, 111 bezargumentowy, 111 domyślny, 93, 218 klasy bazowej, 125 klasy potomnej, 125 przeciążanie, 112 wywoływanie metod, 115, 219 konstruktory klasy JCheckBox, 412 ComboBox, 413 JTextArea, 410 JTextField, 407 OutputStreamWriter, 293 kontenery, 323, 339 kontrola typów, 330, 337, 348 konwersja typów prostych, 195, 196, 199 kopiowanie plików, 312, 314, 320 kroje pisma, 358, 359 420 Java. Praktyczny kurs L liczba, 277 linie, 368 lista czcionek, 359, 361 listy rozwijane, 413 M maszyna wirtualna, 6 menu, 391 kaskadowe, 396, 397 kontekstowe, 400, 401 umożliwiające zaznaczanie, 399 metoda actionPerformed, 393, 408 actionPreformed, 395 add, 330, 392 addActionListener, 379 addMouseListener, 379, 385 addWindowListener, 387–390 clearRect, 352 createNewFile, 302 delete, 304 dispose, 390, 395 draw, 213 drawImage, 373 drawOval, 369 drawPolygon, 371 drawRectangle, 370 drawShape, 215 drawString, 386 exists, 303, 310 fillOval, 369 fillPolygon, 371 fillRectangle, 370 finalize, 118 get, 326, 343 getActionCommand, 395 getAllFonts, 360 getAudioClip, 384 getAvailableFontFamilyNames, 360 getButton, 377 getCodeBase, 372 getDocumentBase, 372 getFontMetrics, 362 getHeight, 362 getImage, 372, 373 getItem, 397 getLocalGraphicsEnvironment, 360 getMessage(), 174 getParameter, 357 getSource, 394, 399 getValues, 347 getX, 377 getY, 377 hasNext, 335 hasNextInt, 287 imageUpdate, 374 invokeLater, 387 list, 298 main, 106 mkdir, 303 mkdirs, 303 mouseClicked, 378, 380, 385 mouseDragged, 380 next, 335 nextLine, 287 nextToken, 280–282 paint, 362 parseDouble, 278 parseInt, 277 play, 385 printf, 289 read, 270, 271, 313 readLine, 272, 275 remove, 335 resize, 325, 327 save, 411 set, 325, 326 setActionCommand, 395 setBounds, 405 setColor, 362 setDocumentListener, 408 setFont, 362 setJMenuBar, 392 setLayout, 405 setSize, 386 setText, 405 show, 345, 401 size, 325 start, 286 stringWidth, 362 super, 127 System.gc, 118 toString, 202 windowClosing, 389 write, 313 metody, 95 argumenty, 100 domyślne, 227, 229 finalne, 161 interfejsu DocumentListener, 409 interfejsu Iterable, 334 interfejsu MouseListener, 376 interfejsu MouseMotionListener, 380 interfejsu WindowListener, 388 klasy anonimowej, 259 klasy ArrayList, 329 klasy Component, 403, 404 klasy Console, 289 klasy File, 296, 297, 298 klasy FileInputStream, 316 klasy FileOutputStream, 318 Skorowidz 421 klasy FileReader, 317 klasy FileWriter, 318 klasy Graphics, 366 klasy InputStream, 270 klasy JComboBox, 414 klasy PrintStream, 291, 292 klasy RandomAccessFile, 307, 308 klasy Scanner, 286 klasy Stack, 331 prywatne, 210 przeciążanie, 108, 144 przesłanianie, 146 statyczne, 152, 277 wywołanie polimorficzne, 208, 220, 259 wywoływane w konstruktorach, 115, 219 zwracanie wyniku, 97 mnożenie liczb, 284 model RGB, 363 mysza, 376 N narzędzia, 6 narzędzia JDK, 11 nawias kątowy, 343 klamrowy, 47, 95, 170 kwadratowy, 75, 80, 82 okrągły, 170, 196 nazwa klasy, 92 pakietu, 138 pliku, 92 zmiennych, 26 negacja logiczna, 43 nieprawidłowa implementacja interfejsów, 234 niewłaściwy typ danych, 340, 344 niszczenie obiektu, 121 O obiekt, 17, 94 file, 298 InputStreamReader, 272 jako argument, 102 klasy wewnętrznej, 246, 249 klasy zewnętrznej, 249 outp, 294 strTok, 284 System.in, 269, 272, 287 obsługa dźwięków, 383 konsoli, 288 menu, 391, 394 myszy, 376 okna, 390 przycisków, 405 zdarzeń, 389, 394, 397 odczyt danych z pliku, 309 odniesienie, 27 odśmiecacz, 120 odtwarzanie dźwięków, 383 odwołanie do nieistniejącego elementu, 166 do prywatnego pola, 246 do pustej zmiennej obiektowej, 179 odzyskiwanie pamięci, 118 okno wyboru składników JDK, 10 opcja encoding, 31, 293 operacje na plikach, 306 na tablicach, 74 na zmiennych, 33 operator +, 28 dekrementacji, 35 inkrementacji, 35 kropki, 94 new, 81, 85, 93 warunkowy, 57 operatory arytmetyczne, 34 bitowe, 41, 42 logiczne, 42, 43 porównywania, 44, 45 przypisania, 44 P pakiet, 137, 250 java.awt, 352, 358 java.io, 271 java.util, 328 javax.swing, 352, 402 JDK, 6 parametr mode, 308 this, 388 parametry apletu, 356 zdarzenia, 381 pasek menu, 391 pętla, 59 do…while, 63, 64 for, 59–61 foreach, 65, 333 while, 62, 63, 273 pętle zagnieżdżone, 70, 82 pierwiastki równania kwadratowego, 284 plik, 304 index.html, 354 Main.class, 15 Main.java, 14, 95 422 pliki dźwiękowe, 383 kopiowanie, 312, 314, 320 odczyt danych, 309, 316 o dostępie swobodnym, 307 operacje strumieniowe, 316 tworzenie, 302 usuwanie, 304 zapis danych, 310, 318 pobranie tokena, 281 zawartości katalogu, 298 pola, 91, 149 finalne, 157 finalne typów prostych, 158 finalne typów referencyjnych, 159 interfejsów, 226 JCheckBox, 411 niezainicjowane, 98 statyczne, 153 tekstowe, 407, 408 pole nval, 281 sval, 281 ttype, 280 polecenie chcp, 30 cmd, 12 import, 138 javac, 14 polimorficzne wywoływanie metod, 208, 220, 259 polimorfizm, 195, 206 polskie znaki, 292 ponowne zgłaszanie wyjątków, 187 poprawność danych, 165 późne wiązanie, 204, 206 praklasa, 202 priorytety operatorów, 45 problem kontroli typów, 337 programowanie obiektowe, 91, 195 przechowywanie wielu danych, 323 przechwytywanie wielu wyjątków, 177 przeciążanie konstruktorów, 112 metod, 108 przeglądanie kontenerów, 333 przekroczenie zakresu wartości, 41 przesłanianie metod, 144–149 pól, 146, 149, 151 przyciski, 405 przypisanie, 24 punkty, 368 Java. Praktyczny kurs R referencja, reference, 27 referencje do obiektu, 102, 114, 207 reprezentacja liczb, 42 RGB, 363 rodzaje klas wewnętrznych, 246, 249 pól tekstowych, 407 wyjątków, 173 rodziny czcionek, 361 rozmiar tablicy, 78, 326 równanie kwadratowe, 50, 284, 286 rysowanie elips, 369 figur, 366 kół, 369 linii, 368 prostokątów, 368, 370 punktów, 368 wielokątów, 370, 371 rzeczywisty typ obiektu, 204 rzutowanie na interfejs, 256 na typ Object, 201 na typy interfejsowe, 254 na typy klasowe, 257 obiektów, 195 typów obiektowych, 128, 197, 201, 341 w górę, 206 S sekcja finally, 190 sekwencja ucieczki, 32 sekwencje znaków specjalnych, 32 składnia ExtendedOutside.Inside, 253 Outside.Inside, 253 składowe klas wewnętrznych, 242 klasy, 92 RGB, 364 statyczne, 152 skrypt startowy powłoki, 13 słowo kluczowe else, 47 extends, 235 final, 156 implements, 226 package, 137 super, 211 this, 114, 116 void, 96, 100 Skorowidz 423 specyfikator dostępu, 129 private, 131 protected, 133 public, 130 sprawdzanie poprawności danych, 165 stała EXIT_ON_CLOSE, 387 napisowa, 276 stałe klasy Color, 363 MouseEvent, 382 stan zmiennej iteracyjnej, 60 standard CP1250, 292 CP852, 293 Unicode, 30, 293 standardowe wejście, 269 wyjście, 279 statyczność klasy wewnętrznej, 260 sterta, heap, 94 stos, stack, 94 stosowanie uogólnień, 341 strona kodowa, 293 strona kodowa 1250, 30 struktura programu, 13 tablicy, 74 tablicy dwuwymiarowej, 80 strumieniowe operacje na plikach, 316 strumień wejściowy, 269 suma logiczna, 43 symbol T, 343 system plików, 295 system wejścia-wyjścia, 269 Ś ścieżka dostępu do katalogu, 298 średnik, 258 środowisko programistyczne, 6 uruchomieniowe, 7, 10 T tablice, 74 dwuwymiarowe, 80 dynamiczne, 326, 328 nieregularne, 84, 89 tekst, 271 terminal, 12 testowanie apletów, 354 tokeny, 279, 280 tworzenie apletu, 352 aplikacji, 386 etykiet, 405 interfejsów, 222 katalogów, 302 klas wewnętrznych, 239 klasy anonimowej, 257–259 list rozwijalnych, 413 menu, 392 nieregularnej tablicy dwuwymiarowej, 86 obiektów, 105 obiektów klas wewnętrznych, 248 obiektu, 96, 98 obiektu strTok, 281 obiektu wyjątku, 183 pakietów, 137 paska menu, 394 plików, 302 tablicy, 80, 82 wielu klas wewnętrznych, 240 własnych wyjątków, 188 tymczasowa zmienna referencyjna, 395 typ boolean, 19 byte, 18 char, 19 double, 19 float, 19 int, 18 long, 18 Object, 201 short, 18 typy arytmetyczne całkowitoliczbowe, 18 arytmetyczne zmiennopozycyjne, 19 danych, 16 generyczne, 336 obiektowe, 26, 197 proste, primitive types, 18 uogólnione, 336 U Unicode, 293 uogólnianie metod, 345 uogólnienia, 341 uruchamianie apletu, 354 usuwanie katalogów, 304 komentarzy, 256 plików, 304 W wartości domyślne pól, 98 parametru mode, 308 pola ttype, 280 424 wartość aktualnego tokena, 280 null, 27, 120, 275 warunek zakończenia pętli, 274 wbudowane typy danych, 18 wczesne wiązanie, 208 wczytywanie danych, 277, 283, 286, 288 grafiki, 372 tekstu, 271, 273, 277 wersje Javy, 7 wiązanie czasu wykonania, 206 dynamiczne, 206 wielokąty, 370 wiersz poleceń, 12 tekstu, 273 wirtualna maszyna Javy, 15 właściwość length, 78, 83 wprowadzanie, Patrz wczytywanie współrzędne biegunowe, 134 kliknięcia, 380 wybór składników JDK, 10 wyjątek, exception, 165, 169 ArithmeticException, 175, 179, 181 ArrayIndexOutOfBoundsException, 76, 165, 169, 324 ClassCastException, 205, 206, 337 DivideByZeroException, 189 FileNotFoundException, 308, 310 IOException, 271, 303 NullPointerException, 179, 181, 276 NumberFormatException, 277, 278, 365 PatternSyntaxException, 300 UnsupportedEncodingException, 293, 294 UnsupportedOperationException, 335 wyjątki własne, 182, 188 wykonanie programu, 15 wykorzystanie kontenerów, 339 obiektów, 140 pakietów, 139 wyrażenia lambda, 266 regularne, 299–302 wyświetlanie elementów tablicy nieregularnej, 87 menu, 401 napisu, 386 polskich znaków, 294 przycisków, 405 systemowego komunikatu, 175 wartości zmiennych, 27 zawartości katalogu, 299 zawartości tablicy, 81 Java. Praktyczny kurs wywołania konstruktorów, 216 metod, 115, 219 metod klas pochodnych, 204 metod klasy wewnętrznej, 247 metod nieistniejących, 225 metod przesłoniętych, 147 złożone konstruktorów, 219 Z zagnieżdżanie bloków try…catch, 180, 186 zakończenie działania programu, 274 zakres wartości zmiennej, 40 typów arytmetycznych, 18 typów zmiennoprzecinkowych, 19 zapis do pliku, 310, 318 zaznaczanie pozycji, 399 zdarzenia myszy, 377 związane z oknem, 387 zgłaszanie wyjątku, 182 ponowne wyjątku, 185 zmiana czcionki, 31 strony kodowej, 30 zmienna środowiskowa CLASSPATH, 139, 140 PATH, 13 zmienne, 23, 102 referencyjne, 94 typów odnośnikowych, 26 znacznik <applet>, 353 <object>, 353 znaczniki formatujące printf, 290 znak %, 289 znaki polskie, 29 specjalne, 32 zwracanie wielu wartości, 346 wyniku, 97