Java. Przygotowanie do programowania na platformę Android

Zdobądź wiedzę i umiejętności, które pomogą Ci tworzyć efektywne aplikacje dla systemu Android! Odkryj Javę - od podstaw

621 131 3MB

Polish Pages [619] Year 2011

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Java. Przygotowanie do programowania na platformę Android

Table of contents :
Spis treści
O autorze
O recenzencie technicznym
Wprowadzenie
Rozdział 1. Pierwsze kroki w języku Java
Czym jest Java?
Java jest językiem programowania
Java jest platformą
Java SE, Java EE, Java ME i Android
Instalacja i poznawanie możliwości JDK
Instalacja i poznawanie możliwości dwóch najpopularniejszych środowisk IDE
Zintegrowane środowisko programistyczne NetBeans
Zintegrowane środowisko programistyczne Eclipse
Gra karciana Kareta
Reguły gry w Karetę
Model gry Kareta w pseudokodzie
Przekształcenie pseudokodu na kod języka Java
Kompilowanie, uruchamianie i udostępnianie aplikacji FourOfAKind
Podsumowanie
Rozdział 2. Podstawy języka Java
Klasy
Deklarowanie klas
Pola
Metody
Konstruktory
Inne konstrukcje inicjalizujące
Interfejs a implementacja
Obiekty
Tworzenie obiektów i tablic
Uzyskiwanie dostępu do pól
Wywoływanie metod
Odśmiecanie
Podsumowanie
Rozdział 3. Mechanizmy języka zorientowane obiektowo
Dziedziczenie
Rozszerzanie klas
Najwyższa klasa przodka
Kompozycja
Problemy z dziedziczeniem implementacji
Wielopostaciowość
Rzutowanie w górę i późne wiązanie
Klasy i metody abstrakcyjne
Rzutowanie w dół i identyfikacja typów w fazie wykonania
Kowariantne typy zwracanych wartości
Interfejsy
Deklarowanie interfejsów
Implementowanie interfejsów
Rozszerzanie interfejsów
Po co używać interfejsów?
Podsumowanie
Rozdział 4. Zaawansowane mechanizmy języka — część I
Typy zagnieżdżone
Statyczne klasy składowe
Niestatyczne klasy składowe
Klasy anonimowe
Klasy lokalne
Interfejsy wewnątrz klas
Pakiety
Czym są pakiety?
Instrukcja pakietu
Instrukcja importu
Wyszukiwanie pakietów i typów
Korzystanie z pakietów
Pakiety i pliki JAR
Importy statyczne
Wyjątki
Czym są wyjątki?
Reprezentowanie wyjątków w kodzie źródłowym
Rzucanie wyjątków
Obsługa wyjątków
Wykonywanie czynności sprzątających
Podsumowanie
Rozdział 5. Zaawansowane mechanizmy języka — część II
Asercje
Deklarowanie asercji
Korzystanie z asercji
Unikanie korzystania z asercji
Włączanie i wyłączanie asercji
Adnotacje
Działanie adnotacji
Deklarowanie typów adnotacji i wstawianie adnotacji do kodu źródłowego
Przetwarzanie adnotacji
Mechanizmy ogólne
Kolekcje i potrzeba bezpieczeństwa typologicznego
Typy ogólne
Metody ogólne
Typy wyliczeniowe
Problem z tradycyjnymi typami wyliczeniowymi
Enum — alternatywa dla tradycyjnego typu wyliczeniowego
Klasa Enum
Podsumowanie
Rozdział 6. Podstawowe interfejsy API — część I
Interfejsy API do wykonywania obliczeń matematycznych
Klasy Math i StrictMath
Klasa BigDecimal
Klasa BigInteger
Informacje na temat pakietów
Podstawowe klasy opakowujące
Klasa Boolean
Klasa Character
Klasy Float i Double
Klasy Integer, Long, Short i Byte
Klasa Number
API References
Podstawowe pojęcia
Klasy Reference i ReferenceQueue
Klasa SoftReference
Klasa WeakReference
Klasa PhantomReference
Podsumowanie
Rozdział 7. Podstawowe interfejsy API — część II
API Reflection
Zarządzanie ciągami znaków
Klasa String
Klasa StringBuffer
Klasa System
API Threading
Interfejs Runnable i klasa Thread
Synchronizacja wątków
Podsumowanie
Rozdział 8. Biblioteka kolekcji
Przegląd biblioteki
Interfejs Comparable kontra Comparator
Interfejsy Iterable i Collection
Iterator i nowa pętla for
Automatyczne pakowanie i rozpakowywanie
Interfejs List
Klasa ArrayList
Klasa LinkedList
Interfejs Set
Klasa TreeSet
Klasa HashSet
Klasa EnumSet
Interfejs SortedSet
Interfejs Queue
Klasa PriorityQueue
Interfejs Map
Klasa TreeMap
HashMap
Klasa IdentityHashMap
Klasa WeakHashMap
Klasa EnumMap
Interfejs SortedMap
Narzędzia
Klasyczne klasy kolekcji
Podsumowanie
Rozdział 9. Dodatkowe biblioteki klas narzędziowych
Narzędzia wspomagające współbieżność
Wykonawcy
Synchronizatory
Współbieżne kolekcje
Blokady
Zmienne atomowe
Internacjonalizacja
Lokalizatory
Paczki zasobów
Iteratory operujące na tekście
Porównywanie tekstów — klasa Collator
Daty, strefy czasowe i kalendarze
Formatery
Biblioteka klas preferencji
Generowanie liczb pseudolosowych
Wyrażenia regularne
Podsumowanie
Rozdział 10. Operacje wejścia-wyjścia
Klasa File
Klasa RandomAccessFile
Strumienie
Przegląd klas strumieni
Klasy OutputStream i InputStream
Klasy ByteArrayOutputStream i ByteArrayInputStream
Klasy FileOutputStream i FileInputStream
Klasy PipedOutputStream i PipedInputStream
Klasy FilterOutputStream i FilterInputStream
Klasy BufferedOutputStream i BufferedInputStream
Klasy DataOutputStream i DataInputStream
Serializacja i deserializacja obiektów
Klasa PrintStream
Klasy Writer i Reader
Przegląd klas Writer i Reader
Klasy bazowe Writer i Reader
Klasy OutputStreamWriter i InputStreamReader
Klasy FileWriter i FileReader
Podsumowanie
Na tym nie koniec
Dodatek A. Odpowiedzi do ćwiczeń
Rozdział 1. Pierwsze kroki w języku Java
Rozdział 2. Podstawy języka Java
Rozdział 3. Mechanizmy języka zorientowane obiektowo
Rozdział 4. Zaawansowane mechanizmy języka — część I
Rozdział 5. Zaawansowane mechanizmy języka — część II
Rozdział 6. Podstawowe interfejsy API — część I
Rozdział 7. Podstawowe interfejsy API — część II
Rozdział 8. Biblioteka kolekcji
Rozdział 9. Dodatkowe biblioteki klas narzędziowych
Rozdział 10. Operacje wejścia-wyjścia
Skorowidz

Citation preview

Podziękowania Dziękuję Steve’owi Anglinowi za to, że skontaktował się ze mną i zaproponował, bym napisał tę książkę, Debrze Kelly za to, że przybliżała mi różnorodne aspekty tego projektu, i Tomowi Welshowi, który pomógł mi w opracowywaniu kolejnych rozdziałów. Podziękowania należą się także Paulowi Connolly’emu za wykrycie różnych błędów, które bez niego niechybnie trafiłyby do ostatecznego wydania książki, oraz Billowi McManusowi i całemu zespołowi odpowiedzialnemu za produkcję, dzięki którym książka zyskała tak atrakcyjny wygląd. Piszę już o Javie wiele lat i dlatego chciałbym podziękować całej rzeszy redaktorów, którzy pomagali mi dzielić się moją wiedzą z czytelnikami. Są to: Chris Adamson, Bridget Collins, Richard Dal Porto, Sean Dixon, Victoria Elzey, Kevin Farnham, Todd Green, Jennifer Orr, Athen O’Shea, Esther Schindler, Daniel Steinberg, Jill Steinberg, Dustin Sullivan i Atlanta Wilson.

Spis treści

O autorze ....................................................................................................11 O recenzencie technicznym .........................................................................12 Wprowadzenie ...........................................................................................13 Rozdział 1.

Pierwsze kroki w języku Java .....................................................................17 Czym jest Java? ....................................................................................................................... 17 Java jest językiem programowania ............................................................................... 18 Java jest platformą .......................................................................................................... 19 Java SE, Java EE, Java ME i Android ............................................................................ 21 Instalacja i poznawanie możliwości JDK ........................................................................... 22 Instalacja i poznawanie możliwości dwóch najpopularniejszych środowisk IDE ....... 27 Zintegrowane środowisko programistyczne NetBeans ............................................. 28 Zintegrowane środowisko programistyczne Eclipse ................................................. 32 Gra karciana Kareta ............................................................................................................... 35 Reguły gry w Karetę ........................................................................................................ 36 Model gry Kareta w pseudokodzie ............................................................................... 36 Przekształcenie pseudokodu na kod języka Java ........................................................ 38 Kompilowanie, uruchamianie i udostępnianie aplikacji FourOfAKind ................ 51 Podsumowanie ....................................................................................................................... 55

Rozdział 2.

Podstawy języka Java .................................................................................57 Klasy ......................................................................................................................................... 57 Deklarowanie klas ........................................................................................................... 58 Pola .................................................................................................................................... 59 Metody .............................................................................................................................. 73 Konstruktory ................................................................................................................... 91 Inne konstrukcje inicjalizujące ..................................................................................... 93 Interfejs a implementacja .............................................................................................. 98 Obiekty .................................................................................................................................. 102 Tworzenie obiektów i tablic ........................................................................................ 102 Uzyskiwanie dostępu do pól ....................................................................................... 104 Wywoływanie metod .................................................................................................... 106 Odśmiecanie .................................................................................................................. 109 Podsumowanie ..................................................................................................................... 111

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Rozdział 3.

Mechanizmy języka zorientowane obiektowo ..........................................115 Dziedziczenie ........................................................................................................................ 115 Rozszerzanie klas .......................................................................................................... 116 Najwyższa klasa przodka ............................................................................................. 121 Kompozycja ................................................................................................................... 130 Problemy z dziedziczeniem implementacji .............................................................. 130 Wielopostaciowość .............................................................................................................. 134 Rzutowanie w górę i późne wiązanie ......................................................................... 135 Klasy i metody abstrakcyjne ........................................................................................ 138 Rzutowanie w dół i identyfikacja typów w fazie wykonania .................................. 140 Kowariantne typy zwracanych wartości .................................................................... 142 Interfejsy ................................................................................................................................ 144 Deklarowanie interfejsów ............................................................................................ 144 Implementowanie interfejsów .................................................................................... 145 Rozszerzanie interfejsów .............................................................................................. 149 Po co używać interfejsów? ........................................................................................... 150 Podsumowanie ..................................................................................................................... 156

Rozdział 4.

Zaawansowane mechanizmy języka — część I .........................................157 Typy zagnieżdżone .............................................................................................................. 157 Statyczne klasy składowe ............................................................................................. 157 Niestatyczne klasy składowe ....................................................................................... 160 Klasy anonimowe .......................................................................................................... 164 Klasy lokalne .................................................................................................................. 166 Interfejsy wewnątrz klas ............................................................................................... 168 Pakiety ................................................................................................................................... 169 Czym są pakiety? ........................................................................................................... 169 Instrukcja pakietu ......................................................................................................... 171 Instrukcja importu ........................................................................................................ 171 Wyszukiwanie pakietów i typów ................................................................................ 172 Korzystanie z pakietów ................................................................................................ 174 Pakiety i pliki JAR ......................................................................................................... 178 Importy statyczne ................................................................................................................ 178 Wyjątki .................................................................................................................................. 180 Czym są wyjątki? ........................................................................................................... 181 Reprezentowanie wyjątków w kodzie źródłowym ................................................... 181 Rzucanie wyjątków ....................................................................................................... 185 Obsługa wyjątków ......................................................................................................... 188 Wykonywanie czynności sprzątających .................................................................... 192 Podsumowanie ..................................................................................................................... 198

Rozdział 5.

Zaawansowane mechanizmy języka — część II ........................................199 Asercje ................................................................................................................................... 199 Deklarowanie asercji .................................................................................................... 200 Korzystanie z asercji ..................................................................................................... 201 Unikanie korzystania z asercji .................................................................................... 207 Włączanie i wyłączanie asercji .................................................................................... 207

6

SPIS TREŚCI

Adnotacje .............................................................................................................................. 208 Działanie adnotacji ....................................................................................................... 209 Deklarowanie typów adnotacji i wstawianie adnotacji do kodu źródłowego ...... 212 Przetwarzanie adnotacji ............................................................................................... 216 Mechanizmy ogólne ............................................................................................................ 218 Kolekcje i potrzeba bezpieczeństwa typologicznego ............................................... 219 Typy ogólne ................................................................................................................... 221 Metody ogólne ............................................................................................................... 232 Typy wyliczeniowe .............................................................................................................. 233 Problem z tradycyjnymi typami wyliczeniowymi .................................................... 234 Enum — alternatywa dla tradycyjnego typu wyliczeniowego ............................... 235 Klasa Enum .................................................................................................................... 240 Podsumowanie ..................................................................................................................... 244

Rozdział 6.

Podstawowe interfejsy API — część I .......................................................247 Interfejsy API do wykonywania obliczeń matematycznych .......................................... 247 Klasy Math i StrictMath ............................................................................................... 247 Klasa BigDecimal .......................................................................................................... 254 Klasa BigInteger ............................................................................................................ 259 Informacje na temat pakietów ........................................................................................... 263 Podstawowe klasy opakowujące ........................................................................................ 267 Klasa Boolean ................................................................................................................ 268 Klasa Character ............................................................................................................. 270 Klasy Float i Double ..................................................................................................... 271 Klasy Integer, Long, Short i Byte ................................................................................ 275 Klasa Number ................................................................................................................ 277 API References ..................................................................................................................... 277 Podstawowe pojęcia ...................................................................................................... 277 Klasy Reference i ReferenceQueue ............................................................................. 279 Klasa SoftReference ...................................................................................................... 280 Klasa WeakReference ................................................................................................... 283 Klasa PhantomReference ............................................................................................. 284 Podsumowanie ..................................................................................................................... 288

Rozdział 7.

Podstawowe interfejsy API — część II ......................................................289 API Reflection ...................................................................................................................... 289 Zarządzanie ciągami znaków ............................................................................................. 297 Klasa String .................................................................................................................... 298 Klasa StringBuffer ......................................................................................................... 301 Klasa System ......................................................................................................................... 304 API Threading ...................................................................................................................... 307 Interfejs Runnable i klasa Thread ............................................................................... 307 Synchronizacja wątków ................................................................................................ 317 Podsumowanie ..................................................................................................................... 333

Rozdział 8.

Biblioteka kolekcji ....................................................................................335 Przegląd biblioteki ............................................................................................................... 335 Interfejs Comparable kontra Comparator ................................................................ 336

7

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Interfejsy Iterable i Collection ........................................................................................... 338 Iterator i nowa pętla for ............................................................................................... 341 Automatyczne pakowanie i rozpakowywanie .......................................................... 342 Interfejs List .......................................................................................................................... 344 Klasa ArrayList .............................................................................................................. 348 Klasa LinkedList ............................................................................................................ 349 Interfejs Set ........................................................................................................................... 351 Klasa TreeSet ................................................................................................................. 351 Klasa HashSet ................................................................................................................ 353 Klasa EnumSet .............................................................................................................. 356 Interfejs SortedSet ................................................................................................................ 358 Interfejs Queue ..................................................................................................................... 365 Klasa PriorityQueue ..................................................................................................... 366 Interfejs Map ........................................................................................................................ 369 Klasa TreeMap .............................................................................................................. 373 HashMap ........................................................................................................................ 374 Klasa IdentityHashMap ............................................................................................... 380 Klasa WeakHashMap ................................................................................................... 382 Klasa EnumMap ............................................................................................................ 383 Interfejs SortedMap ............................................................................................................. 384 Narzędzia .............................................................................................................................. 387 Klasyczne klasy kolekcji ...................................................................................................... 389 Podsumowanie ..................................................................................................................... 396

Rozdział 9.

Dodatkowe biblioteki klas narzędziowych ...............................................397 Narzędzia wspomagające współbieżność ......................................................................... 397 Wykonawcy ................................................................................................................... 397 Synchronizatory ............................................................................................................ 406 Współbieżne kolekcje ................................................................................................... 408 Blokady ........................................................................................................................... 410 Zmienne atomowe ........................................................................................................ 413 Internacjonalizacja ............................................................................................................... 414 Lokalizatory ................................................................................................................... 414 Paczki zasobów .............................................................................................................. 416 Iteratory operujące na tekście ..................................................................................... 425 Porównywanie tekstów — klasa Collator .................................................................. 429 Daty, strefy czasowe i kalendarze ............................................................................... 430 Formatery ....................................................................................................................... 436 Biblioteka klas preferencji .................................................................................................. 443 Generowanie liczb pseudolosowych ................................................................................. 446 Wyrażenia regularne ........................................................................................................... 449 Podsumowanie ..................................................................................................................... 460

Rozdział 10. Operacje wejścia-wyjścia ..........................................................................463 Klasa File ............................................................................................................................... 463 Klasa RandomAccessFile .................................................................................................... 474 Strumienie ............................................................................................................................. 485 Przegląd klas strumieni ................................................................................................ 485 Klasy OutputStream i InputStream ............................................................................ 487 8

SPIS TREŚCI

Klasy ByteArrayOutputStream i ByteArrayInputStream ....................................... 489 Klasy FileOutputStream i FileInputStream .............................................................. 491 Klasy PipedOutputStream i PipedInputStream ....................................................... 494 Klasy FilterOutputStream i FilterInputStream ......................................................... 497 Klasy BufferedOutputStream i BufferedInputStream ............................................. 504 Klasy DataOutputStream i DataInputStream ........................................................... 505 Serializacja i deserializacja obiektów .......................................................................... 508 Klasa PrintStream ......................................................................................................... 519 Klasy Writer i Reader .......................................................................................................... 523 Przegląd klas Writer i Reader ...................................................................................... 524 Klasy bazowe Writer i Reader ..................................................................................... 524 Klasy OutputStreamWriter i InputStreamReader ................................................... 525 Klasy FileWriter i FileReader ...................................................................................... 529 Podsumowanie ..................................................................................................................... 540 Na tym nie koniec ......................................................................................................... 541

Dodatek A

Odpowiedzi do ćwiczeń ............................................................................543 Rozdział 1. Pierwsze kroki w języku Java ......................................................................... 543 Rozdział 2. Podstawy języka Java ...................................................................................... 548 Rozdział 3. Mechanizmy języka zorientowane obiektowo ............................................ 551 Rozdział 4. Zaawansowane mechanizmy języka — część I ........................................... 558 Rozdział 5. Zaawansowane mechanizmy języka — część II .......................................... 564 Rozdział 6. Podstawowe interfejsy API — część I ........................................................... 569 Rozdział 7. Podstawowe interfejsy API — część II ......................................................... 572 Rozdział 8. Biblioteka kolekcji ........................................................................................... 578 Rozdział 9. Dodatkowe biblioteki klas narzędziowych .................................................. 583 Rozdział 10. Operacje wejścia-wyjścia .............................................................................. 589

Skorowidz .................................................................................................601

9

O autorze

Jeff „JavaJeff” Friesen jest aktywnym programistą Javy od końca lat 90. Jeff programował w Javie, pracując dla różnych firm, między innymi dla firmy doradztwa w zakresie ochrony zdrowia, dla której zaimplementował według własnego projektu kompletny system do obsługi kart z czipami. Jest także autorem licznych artykułów na temat języka Java, które były publikowane w serwisach JavaWorld (www.javaworld.com), informIT (www.informit.com) oraz java.net (http://java.net). Autor kilku książek na temat Javy, w tym książki Beginning Java SE 6 Platform: From Novice to Professional (Apress, New York 2007), w której skupił się wyłącznie na wersji 6 języka Java i jego ulepszonych elementach. Jeff uczył Javy na uniwersytecie oraz w college’u. Posiada tytuł magistra matematyki i informatyki Uniwersytetu Brandon w mieście Brandon, Manitoba w Kanadzie. Aktualnie prowadzi własną działalność i zajmuje się tworzeniem rozwiązań w Javie i innych technologiach informatycznych.

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

O recenzencie technicznym

Paul Connolly jest głównym inżynierem odpowiedzialnym za rozwój linii produktów RightSuite w firmie Atypon Systems. RightSuite to zaawansowany system kontroli dostępu i obsługi klientów używany przez wiele spośród największych światowych wydawnictw i mediów. Paul pasjonuje się projektowaniem i implementowaniem wysoko wydajnych, przemysłowych systemów informatycznych. Aktywnie bierze także udział w życiu społeczności rozwijającej oprogramowanie o otwartym dostępie do kodu źródłowego. Zanim rozpoczął pracę w Atypon Systems, Paul był starszym programistą w firmie Standard and Poor’s, w której projektował architekturę i implementował kluczowe systemy komunikacji. Paul posiada certyfikaty Sun Certified Java Programmer, Sun Business Component Developer oraz Sun Certified Web Component Developer. Mieszka w Nowym Jorku z żoną Mariną.

12

Wprowadzenie

Smartfony i inne przenośne urządzenia dotykowe zdobywają obecnie coraz większą popularność. Wynika ona głównie z możliwości uruchamiania na nich aplikacji. Wprawdzie całej stawce urządzeń przewodzą iPhone i iPad wraz z przeznaczoną dla nich, stale rosnącą kolekcją aplikacji opartych na Objective-C, jednak depczą im po piętach smartfony z systemem Android, dla których również powstają coraz to nowsze aplikacje zaimplementowane w języku Java. Nie tylko programiści aplikacji dla urządzeń iPhone i iPad zarabiają na sprzedawaniu swoich aplikacji; podobnie postępują twórcy aplikacji dla systemu Android. Zgodnie z informacjami dostępnymi na fachowych witrynach internetowych, takich jak The Register (www.theregister.co.uk/), niektórzy programiści aplikacji dla systemu Android potrafią zarobić naprawdę duże kwoty (www.theregister.co.uk/2010/03/02/android_app_profit/). W dzisiejszych, dość trudnych czasach wiele osób pragnie spróbować swoich sił jako programiści aplikacji dla systemu Android i zarabiać na tym pieniądze. Jeśli ma się ciekawe pomysły, trochę talentu (lub znajomych z takimi zdolnościami), a dodatkowo jest się odpowiednio wytrwałym, jest się już co najmniej w połowie drogi do osiągnięcia celu. „ Wskazówka • Istotnym argumentem przemawiającym na korzyść tworzenia aplikacji dla systemu Android

zamiast dla iPhone’ów i iPadów jest niższy koszt początkowy, jaki trzeba ponieść, aby implementować rozwiązania dla Androida. Nie trzeba wówczas kupować komputera Mac, aby tworzyć aplikacje działające w Androidzie (komputer taki jest niezbędny do programowania aplikacji dla iPhone’ów i iPadów). W zupełności wystarczy do tego celu komputer z systemem Windows, Linux lub Unix.

Przede wszystkim jednak przed rozpoczęciem pracy z Androidem trzeba dogłębnie poznać język Java oraz najważniejsze interfejsy programowania aplikacji (API). Trzeba bowiem pamiętać, że aplikacje dla systemu Android są napisane w języku Java i korzystają z różnorodnych standardowych API tego języka, jak choćby API do obsługi wątków albo wykonywania operacji wejścia-wyjścia. W zamyśle książka Java. Przygotowanie do programowania na platformę Android ma stanowić źródło podstawowych informacji na temat języka Java, które potem można rozszerzyć o znajomość architektury systemu Android, interfejsów API i konkretnych narzędzi.

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Niniejsza książka zawiera szczegółowe informacje na temat Javy i wielu jej interfejsów API, które są niezbędne do tworzenia aplikacji dla Androida oraz innych aplikacji w Javie. Zostały w niej także zaprezentowane najważniejsze narzędzia programistyczne. Książka Java. Przygotowanie do programowania na platformę Android została podzielona na dziesięć rozdziałów i jeden dodatek. Każdy rozdział prezentuje wybrane, powiązane ze sobą zagadnienia oraz zawiera zbiór zadań, które należy wykonać, aby jak najlepiej przyswoić sobie informacje zawarte w rozdziale. Dodatek zawiera natomiast rozwiązania wszystkich zadań z poszczególnych rozdziałów. „ Uwaga • Kod źródłowy przykładów prezentowanych w książce można pobrać za pomocą przeglądarki

internetowej ze strony pod adresem ftp://ftp.helion.pl/przyklady/jappan.zip. Wystarczy kliknąć łącze Przykłady na ftp. Większość kodu źródłowego może być kompilowana w wersji 6 języka Java; tylko jedna z przykładowych aplikacji będzie wymagać wersji 7 języka.

Rozdział 1. zawiera wprowadzenie do języka Java. Najpierw skupimy się w nim na dualnej naturze języka (na który składa się język właściwy i platforma). Następnie krótko omówimy wersje Java SE firmy Sun/Oracle, Java EE i Java ME, a także system Android firmy Google. W dalszej kolejności jest opisywany sposób pozyskiwania i instalowania pakietu Java SE Development Kit (JDK), po czym na podstawie dwóch zaimplementowanych przykładowych aplikacji zaprezentowano podstawy języka Java. Po krótkim przedstawieniu zintegrowanych środowisk programowania NetBeans i Eclipse ponownie odwołujemy się do przykładowej aplikacji (tym razem jest to działający na konsoli odpowiednik karcianej gry w karetę), by na tej podstawie zaprezentować sposób tworzenia aplikacji w Javie. Rozdział 2. zaczyna się od bardziej szczegółowych informacji na temat Javy. Najpierw skupimy się na podstawowych elementach języka (typach, wyrażeniach, zmiennych i instrukcjach) w kontekście klas i obiektów. Ponieważ kod aplikacji bazuje głównie na klasach, niezwykle istotne jest poznanie sposobu prawidłowej konstrukcji klas. Celem rozdziału jest przedstawienie reguł rządzących tym procesem. Rozdział 3. rozszerza zakres informacji na temat obiektów zaprezentowanych w rozdziale 2. i płynnie przechodzi do opisu tych elementów języka, dzięki którym możliwe jest tworzenie aplikacji już nie opartych o obiekty, lecz zorientowanych obiektowo. Prezentowane są zwłaszcza zagadnienia związane z dziedziczeniem, wielopostaciowością (polimorfizmem) i interfejsami. W trakcie rozważań na temat dziedziczenia poznamy główną superklasę języka Java. Z kolei gdy będą prezentowane interfejsy, poznamy rzeczywiste powody, dla których zostały one udostępnione w Javie. Interfejsy nie są bowiem tylko rozwiązaniem stanowiącym zastępstwo dla dziedziczenia wielokrotnego, które w Javie nie jest obecne, lecz są stworzone do znacznie wyższych celów. W rozdziale 4. zostały zaprezentowane cztery kategorie zaawansowanych elementów języka: typy zagnieżdżone, pakiety, importy statyczne i wyjątki. W trakcie prezentacji typów zagnieżdżonych krótko powiemy o domknięciach (ang. closure), które mają się pojawić w wersji 7 języka. „ Uwaga • Książka ta była pisana kilka miesięcy przed spodziewanym udostępnieniem wersji 7 języka

Java, które było oczekiwane jesienią 2010 roku. Wprawdzie informacje mają jak najlepiej odzwierciedlać stan wersji 7 języka, lecz rzeczywista składnia niektórych elementów może być inna niż składnia prezentowana 14

WPROWADZENIE

w tej książce. Ponadto tylko pokrótce zostały opisane domknięcia, ponieważ ten element języka był w trakcie powstawania książki dopiero na wczesnym etapie rozwoju. Więcej informacji na temat domknięć i innych funkcjonalnych elementów języka (takich jak wyrażenia lambda), które mają zostać udostępnione w wersji 7 Javy, można znaleźć w artykułach, takich jak „Functional Programming Concepts In JDK 7”, którego autorem jest Alex Collins (artykuł jest dostępny na stronie pod adresem http://java.dzone.com/ articles/lambdas-closures-jdk-7).

W rozdziale 5. zostały opisane kolejne zaawansowane elementy języka: asercje, adnotacje, typy ogólne i typy wyliczeniowe. Wprawdzie typy ogólne często budzą wiele wątpliwości wśród programistów, jednak zaprezentowane tu wyjaśnienie tego zagadnienia powinno rozwiać większość z nich. Ponadto zostanie omówiony sposób interpretacji deklaracji typu Enum. Rozdział 6. rozpoczyna serię zagadnień, które dotyczą już bardziej interfejsów API niż samego języka. W rozdziale najpierw prezentujemy liczne typy języka Java związane z obliczeniami matematycznymi (w tym takie typy, jak Math, StrictMath, BigDecimal i BigInteger), a następnie przybliżamy znaczenie słowa zastrzeżonego strictfp. W dalszej kolejności zostały zaprezentowane klasa Package, prymitywne klasy opakowaniowe i API References. W rozdziale 7. kontynuujemy zgłębianie podstawowych interfejsów API Javy. W rozdziale są opisywane refleksje, zarządzanie ciągami znaków, klasa System oraz działanie wątków. Rozdział 8. jest poświęcony wyłącznie kolekcjom języka Java, które stanowią narzędzie do organizowania obiektów w listy, zbiory, kolejki i mapy. Rozdział 9. dalej opisuje narzędziowe API Javy. Tym razem przedmiotem rozważań są narzędzia umożliwiające pracę współbieżną, internacjonalizację, obsługę preferencji, generowanie liczb losowych i obsługę wyrażeń regularnych. W rozdziale 10. zajmiemy się operacjami wejścia-wyjścia. Najpierw zostały zaprezentowane klasyczne mechanizmy obsługi wejścia-wyjścia, czyli klasy File i RandomAccessFile, różnorodne klasy strumieni oraz klasy obiektów zapisu i odczytu. W ramach opisu strumieni wejścia-wyjścia zostały omówione także mechanizmy serializacji i deserializacji obiektów języka Java. „ Uwaga • W książce są prezentowane przede wszystkim interfejsy API wspólne dla Java SE i systemu Android.

Wyjątkiem jest rozdział 9., w którym za pomocą pakietu narzędziowego Swing implementowany jest graficzny interfejs użytkownika dla przykładowych aplikacji ilustrujących sposoby internacjonalizacji, podczas gdy Android nie obsługuje pakietu Swing.

Po przeczytaniu tej książki warto sięgnąć w dalszej kolejności po książkę Beginning Android 2, której autorem jest Mark L. Murphy (Apress, New York 2010), aby poznać sposoby implementowania aplikacji dla systemu Android. W książce tej zostały zaprezentowane metody tworzenia aplikacji dla urządzeń przenośnych z systemem operacyjnym Android 2.x, ilustrowane prostymi przykładami gotowymi do uruchomienia w JDK. „ Uwaga • W ciągu kilku następnych miesięcy zamierzam udostępnić na mojej stronie pod adresem

java-jeff.mb.ca sześć dodatkowych rozdziałów książki w formacie PDF. W rozdziałach tych zaprezentuję kolejne interfejsy API służące do pracy w sieci oraz z bazami danych, których nie mogłem zawrzeć w tej książce, 15

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

ponieważ wówczas jeszcze bardziej przekroczyłbym założoną dla niej początkowo około 400-stronicową objętość (i tak dobrze, że redaktorzy pozwolili mi ten limit przekroczyć, ale w którymś momencie trzeba powiedzieć w końcu „stop”). Więcej informacji na temat wspomnianych dodatkowych plików PDF znajduje się w rozdziale 10., w punkcie „Podsumowanie”.

Dziękuję za zakup mojej książki. Mam nadzieję, że dzięki niej każdy będzie mógł rozpocząć pełną sukcesów, ale też lukratywną karierę programisty aplikacji dla systemu Android, czego wszystkim Czytelnikom gorąco życzę. Jeff „JavaJeff” Friesen, sierpień 2010

16

ROZDZIAŁ 1

Pierwsze kroki w języku Java

Android to wyprodukowany przez firmę Google pakiet oprogramowania dla urządzeń przenośnych, na który składają się system operacyjny oraz warstwa pośrednia. Za pomocą Javy system operacyjny wykonuje specjalnie zaprojektowane aplikacje języka Java, czyli tak zwane aplikacje dla Androida. Ponieważ aplikacje te są zaimplementowane w Javie, warto poznać ten język, zanim zacznie się tworzyć aplikacje dla Androida. „ Uwaga • W niniejszej książce elementy języka Java są prezentowane za pomocą przykładowych aplikacji,

które nie są aplikacjami przeznaczonymi dla systemu Android.

Ten rozdział stanowi punkt wyjścia do poznawania kluczowych elementów języka Java, których działanie należy zrozumieć, aby rozpocząć karierę jako programista aplikacji dla Androida. Najpierw postaramy się odpowiedzieć na pytanie „Czym jest Java?”. Następnie zostaną opisane procedura instalowania Java SE Development Kit oraz narzędzia JDK przeznaczone do kompilowania i uruchamiania aplikacji Javy. Po przedstawieniu sposobu instalacji oraz metod pracy ze środowiskami IDE NetBeans i Eclipse o otwartym dostępie do kodu źródłowego, pozwalającymi znacznie szybciej implementować aplikacje, zostanie zaprezentowana aplikacja do gry w karty o nazwie Kareta. Aplikacja ta powinna lepiej przybliżyć istotę języka Java i będzie stanowić oś dalszych rozważań na temat implementowania aplikacji.

Czym jest Java? Java to język programowania i platforma opracowana przez firmę Sun Microsystems. Celem tego punktu jest krótkie przedstawienie języka i wyjaśnienie, co to właściwie oznacza, że Java jest platformą. Wychodząc naprzeciw różnorodnym potrzebom, firma Sun opracowała trzy główne wydania Javy: Java SE, Java EE i Java ME. W tym punkcie zostaną pokrótce opisane poszczególne wydania, a także sam system Android.

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

„ Uwaga • Java ma ciekawą historię, która rozpoczęła się w grudniu 1990 roku. Wtedy to właśnie James

Gosling, Patrick Naughton i Mike Sheridan (wszyscy byli wówczas pracownikami firmy Sun Microsystems) otrzymali zadanie zidentyfikowania najważniejszych trendów w obszarze rozwoju technologii informatycznych. Cała trójka doszła do wniosku, że jednym z takich trendów jest przenikanie się w coraz większym stopniu urządzeń obliczeniowych i urządzeń codziennego użytku przeznaczonych dla zwykłych konsumentów. W ten sposób narodził się projekt Green. W wyniku realizacji projektu Green powstało Star7 — przenośne urządzenie bezprzewodowe z ekranem LCD o przekątnej wynoszącej pięć cali, procesorem SPARC, zaawansowanymi funkcjami graficznymi i dostosowaną do niego wersją systemu Unix. Oprócz tego w ramach projektu James Gosling opracował język Oak, przeznaczony do tworzenia aplikacji właśnie dla urządzenia Star7. Nazwa języka, która oznacza w języku angielskim „dąb”, wzięła się od drzewa rosnącego za oknem biura firmy Sun. Aby uniknąć konfliktu nazewnictwa z innym językiem programowania o tej samej nazwie, dr Gosling wkrótce zmienił nazwę swojego języka na Java. Sun Microsystems intensywnie rozwijał język i platformę Java aż do momentu, gdy firma ta została przejęta przez Oracle na początku 2010 roku. Najnowsze informacje na temat stanu prac nad Javą prowadzonych przez Oracle można znaleźć na stronie pod adresem http://java.sun.com/.

Java jest językiem programowania Java to język, za pomocą którego programiści wyrażają kod źródłowy (czyli tekst programu). Składnia Javy (a zatem zasady łączenia symboli w elementy języka) wywodzi się po części z C i C++, dzięki czemu programistom tych języków nieco łatwiej jest nauczyć się programowania w Javie. Na poniższej liście zawarto kilka podobieństw między językami Java i C/C++: • W Javie i C/C++ stosuje się takie same style komentarzy jedno- i wielowierszowych.

Za pomocą komentarzy dokumentuje się kod źródłowy. • Wiele słów zastrzeżonych języka Java jest identycznych jak ich odpowiedniki z C/C++ (na przykład for, if, switch i while) oraz odpowiedniki z C++ (na przykład catch, class, public i try). • Java także obsługuje typy podstawowe: znaki, liczby zmiennoprzecinkowe o podwójnej

precyzji, liczby zmiennoprzecinkowe, liczby całkowite, długie liczby całkowite i krótkie liczby całkowite, a także używa do tego celu tych samych słów zastrzeżonych: char, double, float, int, long i short. • Java także obsługuje wiele tych samych operatorów, w tym operatory arytmetyczne (+, -, *, / i %) oraz warunkowe (?:). • Java także używa znaków nawiasów klamrowych ({ i }) do separowania bloków in-

strukcji. Natomiast poniższa lista przytacza kilka różnic między Javą i C/C++: • Java obsługuje dodatkowy styl komentarzy, tak zwany styl Javadoc (styl ten zostanie

pokrótce opisany w dalszej części rozdziału). • Java zawiera słowa zastrzeżone, które nie są obecne w C/C++ (na przykład extends, strictfp, synchronized i transient). 18

ROZDZIAŁ 1. „ PIERWSZE KROKI W JĘZYKU JAVA

• Java obsługuje bajtowy typ całkowitoliczbowy, natomiast nie obsługuje typu znakowego

ze znakiem oraz wersji bez znaku typów całkowitoliczbowego, długiego całkowitoliczbowego i krótkiego całkowitoliczbowego. Co więcej, wszystkie typy podstawowe Javy mają gwarantowany rozmiar implementacji, co jest niezwykle istotne z punktu widzenia przenośności (więcej o tym za chwilę). Tego samego nie można natomiast powiedzieć o odpowiadających im typach podstawowych języków C i C++. • Java udostępnia operatory, których nie ma w C/C++. Takimi operatorami są instanceof i >>> (operator przesunięcia w prawo z wypełnieniem zerami). • Java udostępnia instrukcje przerywania i kontynuacji z etykietami, których nie

uświadczy się w C/C++. Komentarze jedno- i wielowierszowe zostaną przedstawione w rozdziale 2. W tym samym rozdziale zajmiemy się także słowami zastrzeżonymi, typami podstawowymi, operatorami, blokami i instrukcjami (w tym instrukcjami przerwania i kontynuacji z etykietami). Javę zaprojektowano z myślą o tym, by była ona językiem bezpieczniejszym niż C/C++. Wyższy stopień bezpieczeństwa osiągnięto między innymi dzięki rezygnacji z niektórych rozwiązań dostępnych w C/C++. Na przykład Java nie obsługuje wskaźników (czyli zmiennych zawierających adresy), a także nie pozwala na przeciążanie operatorów. Wyższy poziom bezpieczeństwa Javy osiągnięto także przez zmodyfikowanie niektórych elementów znanych z C/C++. Na przykład pętle muszą być sterowane wyrażeniami logicznymi, a nie wyrażeniami całkowitoliczbowymi, w których 0 oznacza fałsz, a wartość niezerowa — prawdę. (Więcej informacji na temat pętli i wyrażeń zamieszczono w rozdziale 2.). Załóżmy, że w C/C++ trzeba zaimplementować pętlę while, która ma się wykonywać nie więcej niż dziesięć razy. Znużony programista mógłby napisać while (x) x++; (przy założeniu, że x to zmienna całkowitoliczbowa o wartości początkowej 0 — więcej na temat zmiennych powiemy w rozdziale 2.), gdzie x++ oznacza zwiększenie wartości x o jeden. Jednak tak skonstruowana pętla nie zakończy się, gdy x osiągnie wartość 10, mamy więc do czynienia z błędem (defektem) w kodzie. W Javie ryzyko popełnienia takiego błędu jest zdecydowanie mniejsze, ponieważ kompilator zwróci błąd, gdy natknie się na fragment while (x). W ten sposób programista zostanie zmuszony do ponownego sprawdzenia wyrażenia, które najprawdopodobniej zmieni na while (x != 10). W ten sposób wzrasta nie tylko bezpieczeństwo (ponieważ nie wystarczy po prostu wskazać zmiennej x), ale także czytelność kodu — wszak while (x != 10) jest zdecydowanie bardziej zrozumiałe niż konstrukcja while (x). Do podstawowych elementów języka należą klasy, obiekty, dziedziczenie, wielopostaciowość i interfejsy. Java udostępnia także zaawansowane rozwiązania związane z obsługą typów zagnieżdżonych, pakietów, importów statycznych, wyjątków, asercji, adnotacji, typów ogólnych, typów wyliczeniowych i innych. Wszystkie te elementy zostaną opisane w kolejnych rozdziałach książki.

Java jest platformą Java to platforma do wykonywania programów. W odróżnieniu od platform, które zawierają fizyczne procesory (na przykład procesory Intel) i systemy operacyjne (na przykład Linux), platforma Java zawiera maszynę wirtualną i związane z nią środowisko wykonawcze. 19

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Maszyna wirtualna to program, który pełni rolę procesora i posiada własny zbiór instrukcji. Powiązane z nią środowisko wykonawcze zawiera biblioteki przeznaczone do uruchamiania programów i prowadzenia interakcji z systemem operacyjnym. Środowisko wykonawcze posiada bogatą bibliotekę prekompilowanych plików klas, które wykonują podstawowe zadania, takie jak działania matematyczne (na przykład obliczenia trygonometryczne) czy operacje sieciowe. Tego typu biblioteka jest zwykle określana jako standardowa biblioteka klas. Specjalny program Javy, tak zwany kompilator Javy, tłumaczy kod źródłowy na instrukcje (i powiązane z nimi dane), które są następnie wykonywane przez maszynę wirtualną. Takie instrukcje to tak zwany kod bajtowy. Kompilator zapisuje kod bajtowy programu oraz jego dane w plikach z rozszerzeniami .class. Są to tak zwane pliki klas, ponieważ zwykle zawierają one skompilowane odpowiedniki klas; więcej o klasach powiemy w rozdziale 2. Program napisany w języku Java wykonuje się za pomocą narzędzia (na przykład programu java), które ładuje i uruchamia maszynę wirtualną oraz przekazuje do niej główny plik klasy programu. Maszyna wirtualna używa specjalnego programu zwanego classloader (komponentu maszyny wirtualnej lub środowiska wykonawczego), który odpowiada za załadowanie pliku klas. Po załadowaniu pliku klas kolejny komponent maszyny wirtualnej, tym razem weryfikator kodu bajtowego, sprawdza poprawność kodu bajtowego i ewentualne naruszenia reguł bezpieczeństwa. Jeżeli weryfikator stwierdzi problem w kodzie bajtowym, natychmiast zakończy działanie maszyny wirtualnej. Jeśli w kodzie bajtowym z pliku klas nie zostanie wykryty żaden problem, interpreter wchodzący w skład maszyny wirtualnej rozpocznie interpretowanie kodu bajtowego instrukcja po instrukcji. Interpretacja polega na identyfikowaniu instrukcji kodu bajtowego i wykonywaniu odpowiadających im instrukcji natywnych. „ Uwaga • Instrukcje natywne (zwane również kodem natywnym) to instrukcje rozumiane przez fizycz-

ny procesor obecny na platformie.

Gdy interpreter stwierdzi, że sekwencja instrukcji kodu bajtowego jest wykonywana w sposób powtarzalny, wyda kompilatorowi Just In Time (JIT) maszyny wirtualnej polecenie skompilowania tych instrukcji do postaci kodu natywnego. Kompilacja JIT jest wykonywana tylko jeden raz dla danej sekwencji instrukcji kodu bajtowego. Dzięki temu, że instrukcje natywne są wykonywane zamiast odpowiadającej im sekwencji instrukcji kodu bajtowego, cały program jest wykonywany zdecydowanie szybciej. W trakcie wykonywana programu interpreter może natknąć się na żądanie wykonania kodu bajtowego pochodzącego z innego pliku klas. W takim przypadku interpreter zażąda od programu classloader, aby załadował on plik klas, zaś weryfikatorowi kodu bajtowego wyda polecenie zweryfikowania poprawności kodu, zanim go wykona. Dzięki temu, że Java ma charakter platformy, uzyskuje się efekt przenośności, wynikający z tego, że platforma stanowi warstwę abstrakcji oddzielającą od rzeczywistej platformy sprzętowej. Dzięki temu ten sam kod bajtowy może być bez żadnych zmian wykonywany na platformach z systemem Windows, Linux, Mac OS X i innymi. 20

ROZDZIAŁ 1. „ PIERWSZE KROKI W JĘZYKU JAVA

„ Uwaga • Hasłem przewodnim dla Javy było: „Napisz raz, uruchamiaj wszędzie”. Wprawdzie twórcy

Javy dokładają wszelkich starań, by zapewnić pełną przenośność, jednak nie zawsze im się to udaje. Znakomita większość elementów języka działa niezależnie od platformy, jednak niektóre rozwiązania obecne w Javie (na przykład harmonogramowanie wątków, o którym więcej powiemy w rozdziale 7.) działają w różny sposób, zależnie od platformy.

Fakt, że Java jest platformą, pozytywnie wpływa także na bezpieczeństwo programów, bowiem platforma ta stanowi bezpieczne środowisko wykonywania programów. Celem takiego rozwiązania jest zapobieganie sytuacjom, w których złośliwy kod mógłby zaszkodzić platformie (i na przykład przechwycić wrażliwe dane). „ Uwaga • Według niektórych programistów język Java nie jest najlepszym narzędziem, a mimo to przy-

wiązują oni dużą wagę do Javy jako do platformy, dlatego tworzą nowe języki (na przykład Groovy), które działają właśnie na platformie Java. Co więcej, Java w wersji 7 zawiera rozszerzoną maszynę wirtualną, która uprości adaptowanie do platformy kolejnych dynamicznych języków programowania (czyli języków, w których dyscyplina tworzenia kodu nie jest tak restrykcyjna — nie trzeba w nich na przykład definiować typu zmiennej, zanim się jej użyje).

Java SE, Java EE, Java ME i Android Programiści używają różnych wersji platformy Java, aby tworzyć programy, które mają działać na komputerach biurkowych, w przeglądarkach internetowych, na serwerach WWW, urządzeniach przenośnych (na przykład telefonach komórkowych) oraz urządzeniach wbudowanych (takich jak przystawki set-top box): • Java Platform, Standard Edition (Java SE) — platforma Javy przeznaczona do two-

rzenia aplikacji, czyli programów działających samodzielnie na komputerach biurkowych. Java SE jest także przeznaczona do implementowania apletów, czyli programów, które są uruchamiane w przeglądarkach internetowych.

• Java Platform, Enterprise Edition (Java EE) — platforma Javy przeznaczona do

tworzenia aplikacji do zastosowań przemysłowych oraz serwletów. Serwlety to programy działające na serwerze, zgodne z Servlet API platformy Java EE. Java EE stanowi rozwinięcie Javy SE.

• Java Platform, Micro Edition (Java ME) — platforma Javy przeznaczona do imple-

mentowania MIDletów, czyli programów działających na urządzeniach przenośnych, oraz Xletów, czyli programów działających na urządzeniach wbudowanych.

Ponadto niektórzy programiści wykorzystują platformę Javy udostępnioną przez firmę Google (więcej informacji na ten temat znajduje się na stronie pod adresem http://developer. android.com/index.html) i przeznaczoną do tworzenia aplikacji dla systemu Android, działających na urządzeniach z tym systemem operacyjnym. Ta wersja Javy to tak zwana platforma Android.

21

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Platforma Android firmy Google składa się w głównej mierze z podstawowych bibliotek języka Java (wywodzących się po części z Javy SE) oraz maszyny wirtualnej znanej pod nazwą Dalvik. Tak skonstruowany zestaw oprogramowania działa na specjalnie zmodyfikowanym jądrze Linuksa. „ Uwaga • Aby uzyskać więcej informacji na temat systemu operacyjnego, warto sprawdzić definicję hasła

„Android (operating system)” pod adresem http://en.wikipedia.org/wiki/Android_(operating_system). Z kolei dodatkowe informacje na temat maszyny wirtualnej Dalvik można znaleźć w Wikipedii pod hasłem „Dalvik (software)”, na stronie http://en.wikipedia.org/wiki/Dalvik_(software).

W tej książce zostaną opisane język Java (obsługiwany przez Java SE i Android) oraz API Javy SE (również obsługiwane przez Android). Ponadto zostaną zaprezentowane kody źródłowe (a właściwie ich fragmenty) aplikacji przeznaczonych dla Java SE.

Instalacja i poznawanie możliwości JDK Java Runtime Environment (JRE) implementuje platformę Java SE i pozwala na wykonywanie programów Javy. Publicznie dostępne JRE można pobrać ze strony Java SE Downloads (http://java.sun.com/javase/downloads/index.jsp). Jednak JRE dostępne publicznie nie pozwala na implementowanie programów Javy. Aby zyskać taką możliwość, trzeba pobrać i zainstalować Java SE Development Kit (JDK), które zawiera narzędzia programistyczne (w tym kompilator języka Java) oraz własne JRE. „ Uwaga • JDK 1.0 to pierwsza opublikowana wersja JDK, która pochodzi z maja 1995 roku. Przed udo-

stępnieniem JDK w wersji 6 skrót JDK oznaczał jedynie Java Development Kit (nie zawierał on symbolu SE). Z biegiem lat pojawiały się kolejne wersje JDK, a w trakcie powstawania tej książki najnowszą była JDK 7 planowana do udostępnienia jesienią lub zimą 2010 roku1. Numer wersji JDK wskazuje numer wersji języka Java. Na przykład JDK 1.0 wskazuje Javę w wersji 1.0, zaś JDK 5 zawiera Javę w wersji 5.0. JDK 5 było jednocześnie pierwszym wydaniem, które posiadało także wewnętrzny numer wersji 1.5.0.

Na stronie Java SE Downloads można także pobrać aktualne JDK, którym w momencie powstawania tej książki było JDK 6 Update 20. Aby pobrać program instalacyjny przeznaczony dla używanej platformy, należy kliknąć łącze Download JDK. „ Uwaga • Niektóre kody źródłowe prezentowane w książce wymagają obecności JDK 7, które w mo-

mencie powstawania książki było dostępne jedynie w wersji zapoznawczej (ang. preview release) — http://java.sun.com/javase/downloads/ea.jsp.

Instalator JDK instaluje pakiet JDK w swoim katalogu macierzystym (w innym katalogu może być zainstalowane jedynie JRE). W systemie Windows XP katalogiem macierzystym 1

Wbrew planom w momencie publikacji niniejszej książki JDK7 wciąż nie było dostępne — przyp. tłum.

22

ROZDZIAŁ 1. „ PIERWSZE KROKI W JĘZYKU JAVA

jest C:\Program Files\Java\jdk1.6.0_16, ponieważ w momencie rozpoczęcia pisania książki aktualną wersją JDK była wersja JDK 6 Update 16. „ Wskazówka • Gdy JDK zostanie już zainstalowane, do zmiennej środowiskowej PATH systemu opera-

cyjnego powinno się dodać ścieżkę dostępu do podkatalogu bin katalogu macierzystego JDK. Pozwoli to na uruchamianie narzędzi JDK z dowolnego katalogu systemu plików. W katalogu macierzystym JDK można także utworzyć podkatalog projekty przeznaczony do przechowywania projektów Javy, a następnie w podkatalogu tym tworzyć odrębne podkatalogi dla każdego projektu.

W katalogu macierzystym znajdują się różnorodne pliki (w tym README.html, który zawiera informacje na temat JDK, oraz src.zip, który zawiera kody źródłowe standardowej biblioteki klas), a także podkatalogi, w tym trzy najbardziej istotne: • bin — podkatalog zawiera wybrane narzędzia JDK, w tym kompilator języka Java.

Część tych narzędzi zostanie opisana w dalszej części książki. • jre — podkatalog zawiera prywatną dla JDK wersję JRE. JRE pozwala na wykonywa-

nie programów Javy bez konieczności pobierania i instalowania JRE publicznego. • lib — podkatalog zawiera pliki bibliotek używanych przez narzędzia JDK. Na przy-

kład biblioteka tools.jar zawiera pliki klas kompilatora Javy, ponieważ sam kompilator również został zaimplementowany w języku Java. W książce będziemy używać tylko niektórych narzędzi znajdujących się w katalogu bin. W szczególności chodzi o javac (kompilator języka Java), java (program do uruchamiania aplikacji Javy), javadoc (generator dokumentacji Javy) oraz jar (program do tworzenia, zmiany i rozpakowywania archiwów Javy). „ Uwaga • Program javac nie jest sam w sobie kompilatorem języka Java. Jest to jedynie narzędzie, które

ładuje i uruchamia maszynę wirtualną, wskazuje maszynie wirtualnej główny plik klas kompilatora (zlokalizowany w pliku tools.jar) i przekazuje do niego nazwę pliku źródłowego, który ma zostać skompilowany.

Narzędzia JDK wykonuje się w wierszu poleceń i przekazuje się do nich argumenty wiersza poleceń. Więcej informacji na temat wiersza poleceń można znaleźć w Wikipedii pod hasłem „Wiersz poleceń” na stronie pod adresem http://pl.wikipedia.org/wiki/Wiersz_poleceń. Gdy JDK został już zainstalowany i posiadamy już najważniejsze informacje na temat wchodzących w jego skład narzędzi, możemy przeanalizować prostą aplikację DumpArgs, która wypisuje argumenty wiersza poleceń w standardowym wyjściu. „ Uwaga • Standardowe wyjście to element mechanizmu zwanego standardowym wejściem-wyjściem.

Mechanizm ten, na który składają się standardowe wejście, standardowe wyjście oraz standardowy błąd i który pochodzi z systemu operacyjnego Unix, umożliwia wczytywanie tekstu z różnych źródeł (na przykład z klawiatury lub pliku) i zapisywanie tekstu do różnych urządzeń docelowych (na przykład na ekran albo do pliku). Tekst jest wczytywany ze standardowego wejścia, którym domyślnie jest klawiatura, lecz można je zmienić na plik. Tekst jest przesyłany do standardowego wyjścia, którym domyślnie jest ekran, lecz można go przekierować również do pliku. Komunikat błędu jest przekazywany do standardowego błędu, którym domyślnie jest ekran, lecz można go przekierować do pliku innego niż plik będący standardowym wyjściem. 23

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Na listingu 1.1 przedstawiono kod źródłowy przykładowej aplikacji DumpArgs. Listing 1.1. Wyświetlanie argumentów wiersza poleceń z tablicy args metody main() na standardowym wyjściu public class DumpArgs { public static void main(String[] args) { System.out.println("Przekazane argumenty:"); for (int i = 0; i < args.length; i++) System.out.println(args[i]); } }

Widoczna na listingu 1.1 aplikacja DumpArgs zawiera klasę o nazwie DumpArgs, zaś w klasie tej zdefiniowano metodę o nazwie main(). Metoda main() stanowi punkt początkowy aplikacji i zawiera kod, który należy wykonać. (Więcej informacji na temat klas i metod znajduje się w rozdziale 2.). Do metody main() jest przekazywana tablica ciągów znaków (czyli sekwencji znaków), która reprezentuje argumenty wiersza poleceń dla aplikacji. Ciągi znaków są przechowywane w zmiennej tablicowej args, której elementy są typu String. (Wywoływanie metod, tablice i zmienne zostaną opisane w rozdziale 2.). „ Uwaga • Nazwą zmiennej tablicowej jest args, jednak nazwa ta nie jest w żaden sposób wyjątkowa.

Zmiennej można nadać dowolną inną nazwę.

Metoda main() najpierw wykonuje instrukcję System.out.println("Przekazane argu ´menty:");, która wywołuje metodę println() strumienia wyjścia System.out i przekazuje do niej ciąg znaków "Przekazane argumenty:". Metoda przekazuje ciąg "Przekazane argumenty:" do standardowego wyjścia, a następnie kończy bieżący wiersz, aby następne dane wyjściowe znalazły się już w nowym wierszu. (Więcej informacji na temat System.out można znaleźć się w rozdziale 7.). „ Uwaga • System.out udostępnia całą gamę metod println(), a także wiele metod print(), które

służą do zwracania różnego rodzaju danych (na przykład sekwencji znaków albo liczb całkowitych). W odróżnieniu od metod println(), metody print() nie kończą bieżącego wiersza, dlatego kolejne porcje danych wyjściowych trafiają do tego samego wiersza. Każda metoda println() kończy wiersz przez zwrócenie dodatkowo ciągu separatora wiersza, zdefiniowanego przez systemową właściwość line.separator. Właściwość ta nie musi zawierać tylko pojedynczego znaku nowego wiersza (który w kodzie źródłowym zapisuje się za pomocą literału znakowego '\n'). (Właściwości systemowe zostaną opisane w rozdziale 7., właściwość line.separator w rozdziale 10., zaś literały znakowe w rozdziale 2.). Na przykład w systemach z rodziny Windows na ciąg separatora wierszy składa się znak powrotu karetki (którego kodem całkowitoliczbowym jest 13) oraz następujący po nim znak nowego wiersza (o kodzie całkowitoliczbowym 10).

W metodzie main() użyto pętli for, aby kilkakrotnie wykonać metodę System.out. ´println(args[i]);. Pętla jest wykonywana args.length razy, czyli tyle razy, ile ciągów 24

ROZDZIAŁ 1. „ PIERWSZE KROKI W JĘZYKU JAVA

znaków znajduje się w tablicy args. (Więcej informacji na temat pętli zostanie przedstawionych w rozdziale 2.). Metoda System.out.println(args[i]); odczytuje ciąg znaków, który znajduje się na i-tej pozycji w tablicy args. Pierwszy element tej tablicy znajduje się pod indeksem (lokalizacją) 0, zaś indeksem ostatniego elementu tablicy jest args.length-1. W efekcie wywołanie metody prowadzi do zwrócenia ciągu znaków z określonej pozycji tablicy do standardowego wyjścia. Jeżeli aktualnie otwarty jest interfejs wiersza poleceń, należy przejść do katalogu DumpArgs i skopiować kod źródłowy z listingu 1.1 do pliku o nazwie DumpArgs.java. Następnie trzeba skompilować plik przez wykonanie w wierszu poleceń następującego polecenia: javac DumpArgs.java

Jeżeli dla pliku zdefiniowano rozszerzenie .java wymagane przez narzędzie javac oraz jeżeli plik DumpArgs.java został skompilowany prawidłowo, w katalogu bieżącym powinien pojawić się plik o nazwie DumpArgs.class. Jest to aplikacja, którą można wykonać przez wpisanie następującego polecenia w wierszu poleceń: java DumpArgs

Jeżeli nie wystąpił żaden błąd, na ekranie powinien się pojawić następujący wynik: Przekazane argumenty:

Aby nieco uatrakcyjnić wynik działania aplikacji DumpArgs, można do niej przekazać jakieś konkretne argumenty wejściowe. Na przykład poniższe polecenie wykona aplikację DumpArgs i przekaże do niej trzy argumenty o wartościach Karol, Asia i Laura: java DumpArgs Karol Asia Laura

Tym razem wynik widoczny na ekranie będzie już nieco bardziej rozbudowany: Przekazane argumenty: Karol Asia Laura

Dane wynikowe można przekierować do innego wyjścia — do pliku wyjściowego. W tym celu należy wpisać zamykający nawias kątowy (>) oraz nazwę pliku wyjściowego. Na przykład polecenie java DumpArgs Karol Asia Laura >out.txt spowoduje, że aplikacja zwróci wynik swojego działania do pliku o nazwie out.txt. „ Uwaga • Zamiast wywoływać metodę System.out.println(), można by wywołać metodę System.

´err.println(), aby za jej pomocą zwrócić znaki do standardowego błędu (System.err udostępnia te same rodziny metod println() i print() co System.out). Jednak System.err powinno się używać zamiast System.out jedynie wówczas, gdy konieczne jest zwrócenie komunikatu błędu i jego wyświetlenie na ekranie, nawet jeżeli standardowe wyjście jest przekierowane do pliku.

W ten sposób szczęśliwie skompilowaliśmy kod źródłowy swojej pierwszej aplikacji i ją uruchomiliśmy! Na listingu 1.2 przedstawiono kod źródłowy drugiej przykładowej aplikacji, która przesyła tekst uzyskany ze standardowego wejścia do standardowego wyjścia. 25

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Listing 1.2. Przekazywanie tekstu pochodzącego ze standardowego wejścia do standardowego wyjścia public class EchoText { public static void main(String[] args) throws java.io.IOException { System.out.println("Proszę wpisać jakiś tekst i nacisnąć Enter!"); int ch; while ((ch = System.in.read()) != 13) System.out.print((char) ch); System.out.println(); } }

Po wyświetleniu tekstu zachęty do wpisania jakiegoś przykładowego tekstu metoda main() tworzy zmienną ch typu int, aby przechowywać w niej całkowitoliczbową reprezentację kolejnych znaków. (Więcej informacji na temat liczb całkowitych i typu int zostanie przedstawionych w rozdziale 2.). Następnie metoda main() wykonuje pętlę while (o której więcej w rozdziale 2.), aby odczytywać i zwracać znaki. W pętli najpierw wykonywana jest metoda System.in.read(), która wczytuje znak i przypisuje jego wartość całkowitoliczbową zmiennej ch. Wykonanie pętli dobiega końca, gdy wpisany zostanie znak o wartości całkowitoliczbowej 13 (jest to wartość odpowiadająca klawiszowi Enter). „ Uwaga • Jeżeli standardowe wejście nie jest przekierowane do pliku, System.in.read() zwraca

wartość 13, gdy zostanie wciśnięty klawisz Enter. W systemach z rodziny Windows kolejne wywołanie metody System.in.read() spowoduje zwrócenie kolejnego znaku o wartości całkowitoliczbowej 10, która wskazuje przejście do nowego wiersza. Z kolei bez względu na to, czy standardowe wejście jest przekierowane, czy nie, System.in.read() zwróci –1, gdy nie będzie już żadnego znaku do wczytania.

Jeżeli zmienna ch będzie mieć wartość inną niż 13, zostanie ona przekształcona na odpowiadający jej znak przez operator char, który jest przykładem dostępnych w Javie operatorów rzutowania (więcej na ten temat w rozdziale 2.). Tak uzyskany znak jest następnie zwracany przez metodę System.out.println(). Ostatnie wywołanie System.out.println(); kończy bieżący wiersz bez zwracania do niego żadnych dodatkowych znaków. „ Uwaga • Gdy standardowe wejście jest przekierowane do pliku i System.in.read() nie może wczytać

z tego pliku tekstu (czyli gdy na przykład plik jest przechowywany na przenośnym dysku i dysk ten został usunięty z systemu przed wykonaniem odczytu), wówczas wykonanie System.in.read() nie powiedzie się i rzucony zostanie obiekt opisujący zaistniały problem. Potwierdzeniem tego jest fragment throws java.io.IOException, który znajduje się na końcu nagłówka metody main(). Instrukcja throws zostanie opisana w rozdziale 4., zaś obiekt java.io.IOException zostanie opisany w rozdziale 10.

Kod źródłowy z listingu 1.2 należy skompilować poleceniem javac EchoText.java, a następnie uruchomić aplikację poleceniem java EchoText. Na ekranie pojawi się prośba o wpisanie jakiegoś tekstu. Gdy tekst zostanie wpisany i naciśnięty zostanie klawisz Enter, tekst zostanie przesłany do standardowego wyjścia. Wynik działania programu może przedstawiać się następująco: 26

ROZDZIAŁ 1. „ PIERWSZE KROKI W JĘZYKU JAVA Proszę wpisać jakiś tekst i nacisnąć Enter! Cześć Java Cześć Java

Jako źródło danych wejściowych można wskazać plik. W tym celu należy użyć otwierającego nawiasu kątowego ( hCard.rank().ordinal()) curPlayer = COMPUTER; deck.putBack(hCard); hCard = null; deck.putBack(cCard); cCard = null; Card[] hCards = new Card[4]; Card[] cCards = new Card[4]; if (curPlayer == HUMAN) for (int i = 0; i < 4; i++) { cCards[i] = deck.deal(); hCards[i] = deck.deal(); } else for (int i = 0; i < 4; i++) { hCards[i] = deck.deal(); cCards[i] = deck.deal(); } while (true) { if (curPlayer == HUMAN) { showHeldCards(hCards); int choice = 0; while (choice < 'A' || choice > 'D') { choice = prompt("Której karty chcesz się pozbyć (A, B, " + "C, D)? "); switch (choice) { case 'a': choice = 'A'; break; case 'b': choice = 'B'; break; case 'c': choice = 'C'; break; case 'd': choice = 'D'; } } discardPile.setTopCard(hCards[choice-'A']); hCards[choice-'A'] = deck.deal(); if (isFourOfAKind(hCards)) { System.out.println(); System.out.println("Człowiek wygrywa!"); System.out.println(); putDown("Karty człowieka:", hCards); System.out.println(); putDown("Karty komputera:", cCards); return; // wyjście z aplikacji przez zakończenie metody main() } curPlayer = COMPUTER; } else { int choice = leastDesirableCard(cCards); discardPile.setTopCard(cCards[choice]); cCards[choice] = deck.deal(); if (isFourOfAKind(cCards))

46

ROZDZIAŁ 1. „ PIERWSZE KROKI W JĘZYKU JAVA { System.out.println(); System.out.println("Komputer wygrywa!"); System.out.println(); putDown("Karty komputera:", cCards); return; // wyjście z aplikacji przez zakończenie metody main() } curPlayer = HUMAN; } if (deck.isEmpty()) { while (discardPile.topCard() != null) deck.putBack(discardPile.getTopCard()); deck.shuffle(); } } } /** * Sprawdza, czy wszystkie obiekty kart Card * przekazane do metody mają tę samą rangę. * * @param cards tablica obiektów Card przekazana do metody. * * @return true, jeśli wszystkie obiekty Card mają tę samą rangę; * w przeciwnym razie false. */ static boolean isFourOfAKind(Card[] cards) { for (int i = 1; i < cards.length; i++) if (cards[i].rank() != cards[0].rank()) return false; return true; } /** * Wskazuje jedną kartę Card spośród kart przekazanych do metody, * która jest kartą najmniej pożądaną do dalszego przetrzymywania. * * @param cards tablica obiektów Card przekazana do metody. * * @return najmniej pożądana karta (0 to as, król to 13). */ static int leastDesirableCard(Card[] cards) { int[] rankCounts = new int[13]; for (int i = 0; i < cards.length; i++) rankCounts[cards[i].rank().ordinal()]++; int minCount = Integer.MAX_VALUE; int minIndex = -1; for (int i = 0; i < rankCounts.length; i++) if (rankCounts[i] < minCount && rankCounts[i] != 0) { minCount = rankCounts[i]; minIndex = i; } for (int i = 0; i < cards.length; i++) if (cards[i].rank().ordinal() == minIndex) return i;

47

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID return 0; // wyłącznie dla kompilatora (instrukcja nigdy nie powinna zostać wykonana) } /** * Prośba do gracza, aby wpisał znak. * * @param msg komunikat prezentowany graczowi. * * @return wartość całkowitoliczbowa znaku wpisanego przez gracza. */ static int prompt(String msg) { System.out.print(msg); try { int ch = System.in.read(); // Usunięcie wszystkich kolejnych znaków, w tym kończącego znaku \n, // aby nie wpływały one na kolejne wywołanie metody prompt(). while (System.in.read() != '\n'); return ch; } catch (java.io.IOException ioe) { } return 0; } /** * Wyświetlenie komunikatu i wszystkich kart posiadanych przez gracza. * Odpowiada rzuceniu wszystkich kart na stół. * * @param msg komunikat wyświetlany graczowi. * @param cards tablica obiektów Card rzucanych na stół. */ static void putDown(String msg, Card[] cards) { System.out.println(msg); for (int i = 0; i < cards.length; i++) System.out.println(cards[i]); } /** * Wyświetlenie posiadanych kart Card w oddzielnych wierszach. * Każdy wiersz rozpoczyna się od litery A. * * @param cards tablica obiektów Card. */ static void showHeldCards(Card[] cards) { System.out.println(); System.out.println("Posiadane karty:"); for (int i = 0; i < cards.length; i++) if (cards[i] != null) System.out.println((char) ('A'+i) + ". " + cards[i]); System.out.println(); } }

48

ROZDZIAŁ 1. „ PIERWSZE KROKI W JĘZYKU JAVA

Kod z listingu 1.8 wykonuje kolejne kroki uwzględnione już wcześniej w pseudokodzie z listingu 1.4. Ponieważ kod źródłowy zawiera dość rozbudowane komentarze, nie ma potrzeby dodatkowego objaśniania sposobu jego działania. Jest tylko kilka zagadnień, którym warto jeszcze poświęcić trochę uwagi: • Typ wyliczeniowy Rank zagnieżdżony w klasie Card przechowuje sekwencję 13 obiektów, która rozpoczyna się od AS, a kończy na KRÓL. Obiektów nie można porównywać ze sobą bezpośrednio za pomocą operatora >, aby sprawdzić, który z nich ma wyższą

rangę. Można jednak porównywać odpowiadające im całkowitoliczbowe wartości porządkowe (pozycyjne) — w tym celu wykorzystywana jest metoda ordinal() obiektu Rank. Na przykład metoda Card.AS_PIK.rank().ordinal() zwraca wartość 0, ponieważ na liście obiektów Rank AS znajduje się na pozycji 0. Z kolei Card.KRÓL_KARO.rank().ordinal() zwraca wartość 12, ponieważ KRÓL znajduje się na liście na ostatniej pozycji. • Metoda leastDesirableCard() odczytuje wartości kart Card z tablicy obiektów Card przekazanej do tej metody i umieszcza te wartości w tablicy rankCounts. Na przykład

jeśli gracz ma aktualnie w ręku dwójkę karo, asa trefl, trójkę karo i asa karo, tablica będzie zawierać informację o jednej dwójce, dwóch asach i jednej trójce. Metoda przeszukuje następnie tablicę rankCounts od indeksu najniższego (który reprezentuje asa) do najwyższego (króla), aby znaleźć pierwszą niezerową liczbę kart o danej randze (ponieważ liczba kart poszczególnych rang może być taka sama, jak choćby jedna dwójka i jedna trójka). Wartość zero oznacza w tym przypadku, że w tablicy obiektów Card nie ma żadnej karty o danej randze. Na koniec metoda przeszukuje tablicę obiektów Card i znajduje obiekt, którego całkowitoliczbowa wartość porządkowa odpowiada indeksowi najmniejszej, niezerowej liczebności, po czym zwraca indeks tak zidentyfikowanego obiektu Card. Taki sposób działania metody powoduje, że kartą najmniej pożądaną jest zawsze karta o najniższej randze. Co więcej, jeżeli w tablicy znajduje się więcej niż jedna karta o takiej randze i ranga ta jest niższa niż ranga kolejnej karty w tej tablicy, metoda wybierze pierwszą kartę (licząc od lewej) spośród kart tej samej rangi i wskaże ją jako niepożądaną. Z kolei jeśli ranga większej liczby kart będzie wyższa niż ranga karty kolejnej, to jako niepożądana zostanie wskazana następna karta. Jak wspomniano wcześniej, kod na listingu 1.5 rozpoczyna się od komentarza Javadoc, który opisuje klasę Card i wskazuje autora kodu klasy. Komentarz Javadoc jest elementem, który dokumentuje klasę, metodę lub inny element programu. Komentarz Javadoc rozpoczyna się sekwencją znaków /**, a kończy znakami */. Między tymi separatorami (czyli parą znaków, które wskazują początek i koniec jakiejś sekcji) może się znajdować tekst, znaczniki HTML (na przykład

albo ) oraz znaczniki Javadoc poprzedzone znakiem @. Najczęściej używanymi znacznikami Javadoc są: • @author — wskazuje autora kodu źródłowego. • @param — wskazuje jeden z parametrów metody (o których więcej powiemy w roz-

dziale 2.). • @return — wskazuje typ wartości zwracanej przez metodę. 49

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

JDK zawiera narzędzie javadoc, które odczytuje wszystkie komentarze Javadoc z jednego lub większej liczby plików źródłowych i generuje pliki HTML prezentujące odczytane komentarze w czytelnym formacie. Tak wygenerowane pliki pełnią funkcję dokumentacji programu. Załóżmy na przykład, że w bieżącym katalogu znajdują się pliki Card.java, Deck.java, DiscardPile.java i FourOfAKind.java. Aby wyodrębnić wszystkie komentarze Javadoc obecne w tych plikach, należy wykonać następujące polecenie: javadoc *.java

Narzędzie javadoc wyświetli w odpowiedzi następujące komunikaty: Loading source file Card.java... Loading source file Deck.java... Loading source file DiscardPile.java... Loading source file FourOfAKind.java... Constructing Javadoc information... Standard Doclet version 1.6.0_16 Building tree for all the packages and classes... Generating Card.html... Generating Card.Rank.html... Generating Card.Suit.html... Generating Deck.html... Generating DiscardPile.html... Generating FourOfAKind.html... Generating package-frame.html... Generating package-summary.html... Generating package-tree.html... Generating constant-values.html... Building index for all the packages and classes... Generating overview-tree.html... Generating index-all.html... Generating deprecated-list.html... Building index for all classes... Generating allclasses-frame.html... Generating allclasses-noframe.html... Generating index.html... Generating help-doc.html... Generating stylesheet.css...

Narzędzie generuje także pliki dodatkowe, w tym index.html, który jest początkowym plikiem dokumentacji. Jeśli plik index.html zostanie otwarty w przeglądarce, na ekranie powinna zostać wyświetlona strona podobna do przedstawionej na rysunku 1.8. Domyślnie narzędzie javadoc generuje dokumentację w postaci plików HTML dla klas publicznych oraz składowych publicznych i chronionych tych klas. Więcej informacji na temat klas publicznych oraz składowych publicznych i chronionych zostanie przedstawionych w rozdziale 2. Dokumentacja aplikacji FourOfAKind opisuje zatem wyłącznie publiczną metodę main(). Nie ma w niej natomiast opisu metody isFourOfAKind() oraz innych metod prywatnych wchodzących w skład pakietu. Aby zapewnić, że dokumentacja będzie również zawierać opis metod prywatnych, należy ją wygenerować poleceniem javadoc z parametrem -package: javadoc -package *.java

„ Uwaga • Dokumentacja standardowej biblioteki klas również została wygenerowana przez narzędzie

javadoc i posiada taki sam format.

50

ROZDZIAŁ 1. „ PIERWSZE KROKI W JĘZYKU JAVA

Rysunek 1.8. Pierwsza strona dokumentacji Javadoc aplikacji FourOfAKind i klas dodatkowych

Kompilowanie, uruchamianie i udostępnianie aplikacji FourOfAKind W odróżnieniu od wcześniej prezentowanych aplikacji DumpArgs i EchoText, które składają się z tylko jednego pliku źródłowego, aplikacja FourOfAKind zawiera pliki Card.java, Deck.java, DiscardPile.java i FourOfAKind.java. Aby skompilować wszystkie te pliki, należy wykonać następujące polecenie: javac FourOfAKind.java

Narzędzie javac uruchamia kompilator języka Java, który w sposób rekurencyjny kompiluje pliki źródłowe poszczególnych klas napotykanych w trakcie kompilacji. Jeżeli kompilacja została wykonana bez błędów, w bieżącym katalogu powinno się pojawić sześć plików klas. „ Wskazówka • Aby skompilować wszystkie pliki źródłowe Javy znajdujące się w bieżącym katalogu,

należy wykonać polecenie javac *.java.

Gdy FourOfAKind.java oraz pozostałe trzy pliki źródłowe zostaną poprawnie skompilowane, aplikację będzie można uruchomić następującym poleceniem: java FourOfAKind

51

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

W wyniku wykonania tego polecenia na ekranie zostanie wyświetlony komunikat powitalny, a następnie zostaną wyświetlone cztery karty wydane graczowi. Poniżej przedstawiono przebieg przykładowej gry: Witaj w grze Kareta! Posiadane karty: A. KRÓLOWA_TREFL B. KRÓL_KIER C. SIÓDEMKA_KARO D. DWÓJKA_KIER Której karty chcesz się pozbyć (A, B, C, D)? B Posiadane karty: A. KRÓLOWA_TREFL B. TRÓJKA_KARO C. SIÓDEMKA_KARO D. DWÓJKA_KIER Której karty chcesz się pozbyć (A, B, C, D)? B Posiadane karty: A. KRÓLOWA_TREFL B. DZIESIĄTKA_TREFL C. SIÓDEMKA_KARO D. DWÓJKA_KIER Której karty chcesz się pozbyć (A, B, C, D)? B Posiadane karty: A. KRÓLOWA_TREFL B. SZÓSTKA_KARO C. SIÓDEMKA_KARO D. DWÓJKA_KIER Której karty chcesz się pozbyć (A, B, C, D)? B Posiadane karty: A. KRÓLOWA_TREFL B. AS_KARO C. SIÓDEMKA_KARO D. DWÓJKA_KIER Której karty chcesz się pozbyć (A, B, C, D)? B Posiadane karty: A. KRÓLOWA_TREFL B. CZWÓRKA_KARO C. SIÓDEMKA_KARO D. DWÓJKA_KIER Której karty chcesz się pozbyć (A, B, C, D)? B Posiadane karty: A. KRÓLOWA_TREFL

52

ROZDZIAŁ 1. „ PIERWSZE KROKI W JĘZYKU JAVA B. DZIEWIĄTKA_TREFL C. SIÓDEMKA_KARO D. DWÓJKA_KIER Której karty chcesz się pozbyć (A, B, C, D)? B Posiadane karty: A. KRÓLOWA_TREFL B. ÓSEMKA_KARO C. SIÓDEMKA_KARO D. DWÓJKA_KIER Której karty chcesz się pozbyć (A, B, C, D)? B Posiadane karty: A. KRÓLOWA_TREFL B. ÓSEMKA_KIER C. SIÓDEMKA_KARO D. DWÓJKA_KIER Której karty chcesz się pozbyć (A, B, C, D)? B Posiadane karty: A. KRÓLOWA_TREFL B. AS_TREFL C. SIÓDEMKA_KARO D. DWÓJKA_KIER Której karty chcesz się pozbyć (A, B, C, D)? B Posiadane karty: A. KRÓLOWA_TREFL B. AS_PIK C. SIÓDEMKA_KARO D. DWÓJKA_KIER Której karty chcesz się pozbyć (A, B, C, D)? B Posiadane karty: A. KRÓLOWA_TREFL B. DZIEWIĄTKA_KIER C. SIÓDEMKA_KARO D. DWÓJKA_KIER Której karty chcesz się pozbyć (A, B, C, D)? B Posiadane karty: A. KRÓLOWA_TREFL B. DZIEWIĄTKA_PIK C. SIÓDEMKA_KARO D. DWÓJKA_KIER Której karty chcesz się pozbyć (A, B, C, D)? B Komputer wygrywa! Karty komputera: KRÓL_PIK

53

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID KRÓL_KARO KRÓL_TREFL KRÓL_KIER

Choć Kareta nie jest typową grą karcianą, na pewno warto będzie udostępnić aplikację FourOfAKind swoim znajomym. Jeśli jednak zapomnimy przekazać im choć jednego spośród

pięciu plików klas dodatkowych, nikt nie będzie w stanie uruchomić aplikacji. Ryzyko to można zminimalizować w ten sposób, że wszystkie sześć plików klas aplikacji FourOfAKind połączy się w jeden plik JAR (Java ARchive). W rzeczywistości jest to plik ZIP, który zawiera specjalny katalog oraz rozszerzenie .jar. W takim przypadku wystarczy przekazać znajomym tylko plik JAR. JDK udostępnia narzędzie jar, które tworzy pliki JAR. Aby połączyć wszystkie sześć plików klas w jeden plik JAR o nazwie FourOfAKind.jar, należy wykonać polecenie widoczne poniżej, w którym parametr c nakazuje narzędziu jar utworzenie pliku JAR, zaś parametr f wskazuje nazwę docelowego pliku JAR: jar cf FourOfAKind.jar *.class

Gdy plik JAR zostanie już utworzony, można spróbować uruchomić aplikację następującym poleceniem: java -jar FourOfAKind.jar

Jednak zamiast uruchomienia aplikacji na ekranie zostanie wyświetlony komunikat o błędzie, który wskazuje, że wśród sześciu plików klas wchodzących w skład pliku JAR java nie potrafi rozpoznać pliku będącego głównym plikiem klas (czyli pliku, którego metoda main() powinna zostać wykonana jako pierwsza). Główny plik klas można wskazać w pliku tekstowym, który zostanie dołączony do manifestu pliku JAR. Plik manifestu to specjalny plik o nazwie MANIFEST.MF, który przechowuje informacje na temat zawartości pliku JAR i znajduje się w katalogu META-INF pliku JAR. Zawartość odpowiedniego pliku tekstowego znajduje się na listingu 1.9. Listing 1.9. Sposób wskazania głównej klasy aplikacji Main-Class: FourOfAKind

Instrukcja przedstawiona na listingu 1.9 wskazuje narzędziu java ten z plików klas obecnych w pliku JAR, który jest głównym plikiem klas. (Po wierszu Main-Class: FourOfAKind musi się jeszcze znajdować pusty wiersz). Poniższe polecenie tworzy plik FourOfAKind.jar, a dodatkowo zawiera parametr m oraz nazwę pliku tekstowego, który zawiera treść manifestu: jar cfm FourOfAKind.jar manifest *.class

Tym razem polecenie java -jar FourOfAKind.jar zostanie wykonane bez błędów, a aplikacja zostanie uruchomiona, ponieważ java będzie już w stanie zidentyfikować główny plik klas aplikacji FourOfAKind.

54

ROZDZIAŁ 1. „ PIERWSZE KROKI W JĘZYKU JAVA

Ćwiczenia Celem poniższych ćwiczeń jest sprawdzenie wiedzy wyniesionej z tego rozdziału, dotyczącej Javy, JDK, NetBeans, Eclipse i sposobu tworzenia aplikacji języka Java. 1. Czym jest Java? 2. Co to jest maszyna wirtualna? 3. Co jestem celem kompilatora języka Java? 4. O instrukcjach zawartych w pliku klas mówi się jako o kodzie bajtowym. Prawda czy fałsz? 5. Co zrobi interpreter maszyny wirtualnej, gdy okaże się, że sekwencja instrukcji kodu bajtowego jest wykonywana w sposób powtarzalny? 6. W jaki sposób platforma Java zapewnia przenośność? 7. W jaki sposób platforma Java zapewnia bezpieczeństwo? 8. Java SE to platforma Javy, która służy do tworzenia serwletów. Prawda czy fałsz? 9. Co to jest JRE? 10. Jaka jest różnica między publicznym a prywatnym JRE? 11. Co to jest JDK? 12. Które narzędzie JDK służy do kompilowania kodu źródłowego napisanego w Javie? 13. Które narzędzie JDK służy do uruchamiania aplikacji Javy? 14. Do czego służy narzędzie JDK o nazwie jar? 15. Co to jest standardowe wejście-wyjście? 16. Co to jest IDE? 17. Należy wskazać dwa najbardziej popularne środowiska IDE. 18. Co to jest pseudokod? 19. W jaki sposób można uzyskać zawartość pliku FourOfAKind.jar (listę katalogów i plików, które znajdują się w pliku JAR)? 20. Należy zmodyfikować aplikację FourOfAKind w taki sposób, aby każdy gracz mógł pobrać kartę ze szczytu talii lub stosu kart odrzuconych (pod warunkiem że stos kart odrzuconych nie jest pusty; stos ten jest pusty, gdy gracz-człowiek gra pierwszy i rozpoczyna swoją pierwszą kolejkę). Dla graczaczłowieka należy wyświetlić pierwszą kartę ze stosu kart odrzuconych (nie należy natomiast wyświetlać pierwszej karty ze szczytu talii).

Podsumowanie Java jest zarówno językiem programowania, jak i platformą. Jako język Java do pewnego stopnia przypomina C i C++, co sprawia, że łatwiej jest się jej nauczyć programistom C/C++. Jako platforma Java składa się z maszyny wirtualnej oraz powiązanego z nią środowiska wykonawczego. Programiści tworzą aplikacje w Javie z wykorzystaniem różnych wersji języka. Aplikacje mogą być przeznaczone na komputery biurkowe, przeglądarki internetowe, serwery WWW, urządzenia przenośne i urządzenia wbudowane, natomiast dostępnymi wersjami Javy są Java SE, Java EE i Java ME. 55

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Programiści korzystają także ze specjalnej wersji platformy Java przygotowanej przez firmę Google i przeznaczonej do tworzenia aplikacji dla systemu Android, które docelowo mają działać na urządzeniach z tym systemem operacyjnym. Wersja ta — tak zwana platforma Android — składa się przede wszystkim z podstawowych bibliotek Javy oraz maszyny wirtualnej o nazwie Dalvik. Publiczne JRE implementuje platformę Java SE i pozwala na uruchamianie programów napisanych w Javie. Z kolei JDK zawiera narzędzia (w tym kompilator języka Java), które służą do tworzenia programów w Javie. JDK zawiera także prywatną kopię JRE. Jeżeli realizowany jest duży projekt, nie zaleca się używania narzędzi JDK w wierszu poleceń. Trudno jest bowiem zarządzać dużymi projektami bez pomocy zintegrowanych środowisk programistycznych. Dwoma najpopularniejszymi środowiskami IDE są NetBeans i Eclipse. Implementowanie aplikacji nie jest prostym zadaniem. Wszystkie aplikacje (oprócz tych najbardziej trywialnych) trzeba najpierw precyzyjnie zaplanować, ponieważ w przeciwnym razie prawdopodobnie straci się dużo czasu i pieniędzy — i to nie tylko własnych, ale też użytkowników aplikacji. Jednym ze sposobów skutecznego tworzenia aplikacji jest tworzenie pseudokodu. Aplikacja FourOfAKind przybliżyła nieco specyfikę języka Java. Na razie zapewne większość kodu źródłowego aplikacji jest jeszcze niezrozumiała. Powinno się to jednak zmienić już po lekturze rozdziału 2., który prezentuje podstawowe informacje na temat języka Java.

56

ROZDZIAŁ 2

Podstawy języka Java

Ambitny programista aplikacji dla systemu Android musi rozumieć język Java. Java jest językiem programowania zorientowanym obiektowo, w którym programiści za pomocą obiektów reprezentują encje (ang. entities — elementy istniejące w rzeczywistości, na przykład samochód, konto bankowe czy też przyciski i inne elementy interfejsu użytkownika). Tematem przewodnim tego rozdziału, jak i rozdziału 3. są właśnie te elementy Javy, które dotyczą orientacji obiektowej. Bardziej zaawansowane elementy języka zostaną przedstawione w rozdziałach 4. i 5. Kolejne elementy zaawansowane, lecz już o nieco mniejszym znaczeniu, będą sukcesywnie prezentowane w dalszych rozdziałach książki.

Klasy W aplikacjach zorientowanych obiektowo encje są reprezentowane za pomocą obiektów (encji abstrakcyjnych). Każdy obiekt łączy w sobie atrybuty i metody encji (jest to tak zwana enkapsulacja, czyli łączenie składowych w jeden obiekt). Na przykład obiekt reprezentujący oszczędnościowe konto w banku może posiadać atrybut salda (o aktualnej wartości 50 PLN) oraz udostępniać czynności wpłaty środków na konto i ich wypłaty. „ Uwaga • Enkapsulacja pozostaje w jawnej sprzeczności z zasadą odseparowywania od siebie atrybutów

i czynności, obecnej w C i innych strukturalnych języka programowania. W języku strukturalnym programista nie ma możliwości łączenia atrybutów i czynności wykonywanych na encji w jeden obiekt, lecz musi oddzielnie deklarować atrybuty, wykorzystując do tego odpowiednie struktury danych (wskazujące sposób organizacji danych), oraz deklarować czynności za pomocą odpowiednio skonstruowanych funkcji, do których przekazywane są instancje struktur danych z odpowiednimi atrybutami.

Obiekty nie biorą się znikąd — trzeba z czegoś stworzyć ich instancję. W językach takich jak C++ i Java tym czymś jest klasa, czyli szablon, na podstawie którego tworzy się obiekty (czasami mówi się także, że jest to instancja klasy albo po prostu instancja). W tym rozdziale zostaną przedstawione te elementy języka Java, które służą do tworzenia klas.

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Deklarowanie klas Ponieważ instancja obiektu nie może powstać z klasy, która nie istnieje, trzeba najpierw tę klasę zadeklarować. Deklaracja klasy składa się z nagłówka i ciała. Nagłówek musi co najmniej zawierać słowo zastrzeżone class i następującą po nim nazwę, która identyfikuje klasę i za pomocą której klasa jest przywoływana w dalszych częściach kodu źródłowego. Ciało klasy rozpoczyna się znakiem otwierającego nawiasu klamrowego ({) i kończy się zamykającym nawiasem klamrowym (}). Pomiędzy tymi separatorami (czyli znakami, które wskazują początek i koniec określonej sekcji) występują pola, metody i inne deklaracje (listing 2.1). Listing 2.1. Deklaracja najprostszej klasy CheckingAccount class CheckingAccount { // deklaracja pól, metod i innych składowych }

Na listingu 2.1 znajduje się deklaracja klasy o nazwie CheckingAccount. Zgodnie z panującą konwencją nazwa klasy zaczyna się wielką literą. Ponadto wielką literą zaczyna się każde kolejne słowo wchodzące w skład nazwy klasy. Jest to tak zwana notacja wielbłądzia (ang. camel-casing). „ Uwaga • Deklaracja klasy to przykład typu, czyli szablonu definiującego zbiór danych i operacji, które

można wykonywać na tych danych. CheckingAccount to przykład identyfikatora, czyli nazwy identyfikującej klasę lub inną encję obecną w kodzie źródłowym. Identyfikatory składają się z liter (A – Z, a – z lub odpowiadające im małe i wielkie litery w innych językach), cyfr (0 – 9 lub odpowiadające im cyfry w innych językach), łączących znaków interpunkcyjnych (na przykład znak podkreślenia) oraz symboli walut (na przykład znak dolara). Identyfikator może się jednak zacząć tylko od litery, symbolu waluty lub łączącego znaku interpunkcyjnego. Ponadto długość identyfikatora nie może przekraczać długości wiersza, w którym ten identyfikator się znajduje. „ Uwaga • Identyfikatory wyraża się w kodzie Unicode (http://en.wikipedia.org/wiki/Unicode). Unicode to

uniwersalny zestaw znaków, w którym są zakodowane różnorodne symbole tworzące obecne na całym świecie języki pisane.

Innymi przykładami identyfikatorów są temperature, Temperature, _class, first$name i loopCounter1. Temperature i temperature to dwa różne identyfikatory, ponieważ Java jest językiem, w którym wielkość liter jest brana pod uwagę. Z kolei 6x i door^color nie są już identyfikatorami, ponieważ pierwsza sekwencja znaków rozpoczyna się od cyfry, zaś druga sekwencja zawiera znak niedozwolony (^). Klasom i innym encjom kodu źródłowego nadaje się nazwy, którymi mogą być niemal dowolne identyfikatory. Jednak w Javie zarezerwowano niektóre nazwy — tak zwane słowa zastrzeżone — które są przeznaczone do specjalnych celów. Słowami zastrzeżonymi są: abstract, assert, boolean, break, byte, case, catch, char, class, const, continue, default, 58

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

do, double, enum, else, extends, false, final, finally, float, for, goto, if, implements, import, instanceof, int, interface, long, native, new, null, package, private, protected, public, return, short, static, strictfp, super, switch, synchronized, this, throw, throws, transient, true, try, void, volatile i while. Jeżeli któreś z tych słów zastrzeżonych zostanie użyte

w kontekście innym niż dla niego przeznaczony, kompilator zwróci błąd. Ciało klasy jest obecnie puste, ponieważ nie poznałeś jeszcze pól, metod ani innych składowych klas. Wskazuje na to treść komentarza jednowierszowego, czyli wiersza dokumentacji rozpoczynającego się sekwencją znaków //. Kompilator ignoruje wszystkie znaki, począwszy od // aż do końca wiersza.

Pola Po zadeklarowaniu klasy w jej treści można zadeklarować zmienne (lokalizacje w pamięci, których wartości mogą się zmieniać). Niektóre zmienne są używane w kontekście obiektu do opisywania atrybutów encji. Inne zmienne z kolei występują w kontekście klas i opisują atrybuty klas (atrybuty współużytkowane przez wszystkie istniejące obiekty, na przykład zmienna, która przechowuje liczbę wszystkich utworzonych obiektów danej klasy). Bez względu na jej przeznaczenie zmienna zadeklarowana w ciele klasy to tak zwane pole. W tym punkcie objaśnimy, jak deklaruje się pola, jak inicjalizuje się je wartościami innymi niż domyślne oraz jak zadeklarować pola przeznaczone tylko do odczytu.

Deklarowanie pól Aby zadeklarować pole w ciele klasy, trzeba co najmniej wskazać typ pola oraz identyfikator pola, po którym musi nastąpić znak średnika (;). Na listingu 2.2 przedstawiono deklaracje dwóch pól. Listing 2.2. Deklaracja pól owner i balance w klasie CheckingAccount class CheckingAccount { String owner; // imię i nazwisko właściciela konta int balance; // ilość pieniędzy, jaką można wypłacić z konta }

Na listingu 2.2 zadeklarowano dwa pola o nazwach owner i balance. Zgodnie z powszechnie stosowaną konwencją nazwa pola zaczyna się małą literą, natomiast pierwsza litera każdego kolejnego słowa występującego w nazwie pola jest już literą wielką. Typ pola wskazuje rodzaj wartości, jakie można przypisywać polu. Pole owner jest typu String; jest to jedna z predefiniowanych klas języka Java. Obiekty klasy String zawierają sekwencje znaków. Sekwencja znaków w dowolnym obiekcie klasy String przypisanym polu owner będzie zawierać imię i nazwisko właściciela konta. Pole balance jest typu całkowitoliczbowego, o czym świadczy zastrzeżone słowo int. Standardowo mówi się, że balance jest typu int. W polu przechowywane jest saldo konta wyrażone liczbą całkowitą, bez wartości ułamkowej.

59

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Typ całkowitoliczbowy jest jednym z kilku obsługiwanych przez Javę typów podstawowych (czyli typów, których wartości nie są obiektami). W tabeli 2.1 opisano wszystkie podstawowe typy języka Java oprócz typu void, którym zajmiemy się nieco później. W tabeli 2.1 wskazano, ile bitów (binarnych cyfr, gdzie każda cyfra to 0 albo 1) zajmuje w pamięci wartość danego typu podstawowego. Grupa ośmiu bitów to bajt. Zastosowany w tabeli zapis „0 w Unicode” oznacza „pierwszy znak Unicode”, natomiast zapis „IEEE 754” (http://pl.wikipedia.org/wiki/IEEE_754) odwołuje się do standardu reprezentacji liczb zmiennopozycyjnych w pamięci. Implementacja każdego typu podstawowego ma określony rozmiar. Wyjątkiem jest typ logiczny, którego rozmiar zależy od implementacji (w jednej implementacji języka Java wartość logiczna może być przechowywana w jednym bicie, w innej implementacji natomiast wymagana jest reprezentacja ośmiobitowa, co wynika z dążenia do zapewnienia jak najwyższej wydajności). „ Uwaga • W odróżnieniu od odpowiadających im typów C/C++, typy podstawowe języka Java mają

takie same rozmiary w każdej implementacji Javy, co po części wynika z dążenia do zapewnienia odpowiedniego stopnia przenośności. Tabela 2.1. Typy podstawowe Typ podstawowy

Słowo zastrzeżone

Rozmiar

Wartość minimalna

Wartość maksymalna

Logiczny

boolean

--

--

--

Znakowy

char

16 bitów

0 w Unicode

216 – 1 w Unicode

Bajtowa liczba całkowita

byte

8 bitów

–128

+127

Krótka liczba całkowita

short

16 bitów

–215

+215 – 1

Liczba całkowita

int

32 bity

–231

+231 – 1

Długa liczba całkowita

long

64 bity

–263

+263 – 1

Liczba zmiennopozycyjna

float

32 bity

IEEE 754

IEEE 754

Liczba zmiennopozycyjna o podwójnej precyzji

double

64 bity

IEEE 754

IEEE 754

Każdy typ podstawowy ma wartość minimalną i maksymalną oprócz typu logicznego, którego jedynymi wartościami są prawda i fałsz. Głębsza analiza wartości maksymalnych i minimalnych prowadzi do wniosku, że typ char jest typem bez znaku (wszystkie jego wartości są dodatnie), natomiast każdy typ liczbowy jest typem ze znakiem (co oznacza, że typ obsługuje zarówno wartości dodatnie, jak i ujemne).

60

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

„ Uwaga • Programiści, którzy stoją na stanowisku, że wszystko w Javie powinno być obiektem, głośno

wyrażają swoje niezadowolenie z obecności w tym języku typów podstawowych. Jednak w Javie typy podstawowe umieszczono po to, by obejść ograniczenia związane z szybkością pracy i dostępną pamięcią, charakterystyczne dla urządzeń z początku lat dziewięćdziesiątych, dla których język pierwotnie zaprojektowano.

Pola owner i balance to przykłady pól, które nie są tablicą, ponieważ obydwa pola mogą przechowywać tylko jedną wartość. Tablica jest zmienną wielowartościową, a każdy element tablicy (jednostka przechowywania danych) zawiera tylko jedną z wartości. Java pozwala także deklarować pola tablicowe, które identyfikuje się za pomocą nawiasów kwadratowych (znak [ i ]). Widać to na listingu 2.3. Listing 2.3. Deklaracja pól tablicowych cities i temperatures w klasie WeatherData class WeatherData { String country; String[] cities; double[][]temperatures; }

Na listingu 2.3 zadeklarowano klasę WeatherData, która przechowuje informacje o skrajnych temperaturach w różnych miastach określonych krajów. Jednowymiarowa tablica cities zawiera nazwy tych miast, natomiast dwuwymiarowa tablica temperatures zawiera wartości temperatur minimalnej i maksymalnej dla każdego miasta. Tablica jednowymiarowa to sekwencyjna lista wartości. Jest ona identyfikowana w kodzie źródłowym przez jedną parę nawiasów klamrowych. Tablica dwuwymiarowa to tabela wartości — identyfikują ją dwie pary nawiasów kwadratowych w kodzie źródłowym. Nawiasy mogą się znajdować po dowolnej stronie nazwy pola, jednak często wpisuje się je zaraz po nazwie typu. Java obsługuje ponadto tablice o większej liczbie wymiarów. Pola owner i balance klasy CheckingAccount, a także pola country, cities i temperatures klasy WeatherData to przykłady pól instancji, które towarzyszą obiektom. Każda instancja obiektu klasy CheckingAccount posiada własną kopię pól owner i balance; kopie te są tworzone w tym samym momencie, gdy tworzony jest sam obiekt. Analogicznie każda instancja obiektu WeatherData posiada własną kopię pól country, cities i temperatures powstałą w momencie tworzenia obiektu. Zmiana wartości któregoś pola nie wpływa na wartość przechowywaną w innej kopii tego samego pola. W wielu sytuacjach pola instancji w całości spełniają potrzeby programisty. Czasami jednak dochodzi do sytuacji, w której potrzeba tylko jednej kopii pola bez względu na to, ile obiektów zostanie utworzonych. Załóżmy na przykład, że trzeba śledzić liczbę utworzonych obiektów CheckingAccount i w tym celu do definicji klasy dodaje się pole counter (którego wartością początkową jest 0). Ponadto w konstruktorze klasy (który zaprezentuję, gdy przejdę do opisu konstruktorów) w momencie tworzenia obiektu wartość pola counter będzie zwiększana o 1. Ponieważ jednak każdy obiekt posiada własną kopię pola counter, wartość pola nigdy nie przekroczy 1.

61

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

W celu rozwiązania tego problemu można zadeklarować counter jako pole klasy, czyli takie pole, które jest powiązane z klasą, a nie z obiektami klasy. Rozwiązanie przedstawiono na listingu 2.4, na którym deklarację pola counter poprzedza słowo zastrzeżone static. Listing 2.4. Deklaracja nowego pola counter w klasie CheckingAccount class CheckingAccount { String owner; int balance; static int counter; }

Obecne na listingu 2.4 słowo zastrzeżone static oznacza, że ma istnieć tylko jedna kopia pola counter, a nie wiele kopii — po jednej w każdym obiekcie. Teraz za każdym razem, gdy tworzony będzie nowy obiekt, wartość pola counter będzie zwiększana o 1. W ten sposób przez cały czas będzie dostępna informacja o rzeczywistej liczbie nowych obiektów Checking ´Account. Pola owner i balance są tworzone w momencie, gdy powstaje obiekt CheckingAccount, i niszczone, gdy sam obiekt zostaje zniszczony. W przeciwieństwie do nich pole counter powstaje w momencie ładowania klasy CheckingAccount do pamięci i jest niszczone dopiero wtedy, gdy klasa ta jest usuwana z pamięci (czyli w chwili zakończenia pracy aplikacji). Ta cecha to tak zwany czas życia. Dostęp do pola owner i balance jest możliwy wyłącznie w kontekście instancji obiektu (na przykład w konstruktorze). Natomiast pole counter jest dostępne zarówno w kontekście instancji, jak i klasy (na przykład w metodzie klasy, o której więcej powiemy w dalszej części rozdziału). Ta cecha to tak zwany zasięg.

Inicjalizowanie pól Na listingu 2.4 pola owner i balance są inicjalizowane domyślnymi wartościami — odpowiednio — null i 0. Inicjalizacja następuje w momencie utworzenia obiektu klasy Checking ´Account. Pole counter jest inicjalizowane wartością 0 w momencie załadowania klasy do pamięci. Powszechną praktyką jest inicjalizowanie pola instancji w konstruktorze klasy wartością potencjalnie unikatową. Polu można jednak także jawnie przypisać jakąś konkretną, nieunikatową wartość domyślną, a następnie zezwolić na pokrycie tej wartości domyślnej przez konstruktor. W odróżnieniu od pola instancji pola klasy często inicjalizuje się jawnie. Niezależnie od tego, w jaki sposób pole zostanie zainicjalizowane, wartością pola jest wynik wyrażenia. Samo wyrażenie to kombinacja zmiennych, wywołań metod, literałów (wartości określonych dosłownie) i operatorów. Typ wyrażenia, które jest przypisywane polu, musi być zgodny z typem pola. W przeciwnym razie kompilator zwróci błąd.

Wyrażenia proste Wyrażenie proste to wartość wyrażona w kodzie źródłowym przez nazwę zmiennej (wartość jest wówczas odczytywana ze zmiennej), wywołanie metody (wartość jest wówczas zwracana przez metodę) lub literał. 62

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

Java obsługuje kilka typów literałów: ciągi znaków, logiczne, znakowe, całkowitoliczbowe, zmiennopozycyjne oraz null (o tym ostatnim powiemy więcej w dalszej części tego rozdziału). Literał w postaci ciągu znaków składa się z sekwencji znaków Unicode otoczonej parą cudzysłowów ("). Na przykład: "Mężny bądź, chroń pułk twój i sześć flag.". Literał w postaci ciągu znaków może także zawierać sekwencje ucieczki. Sekwencja ucieczki to specjalna składnia (reguła łączenia symboli w elementy języka), służąca do reprezentowania określonych drukowalnych i niedrukowalnych znaków, które w inny sposób nie mogłaby się pojawić w literale. Na przykład w literale "Mężny bądź, chroń \"pułk\" twój i sześć flag." użyto sekwencji ucieczki \", aby otoczyć cudzysłowami wyraz pułk. W tabeli 2.2 przedstawiono wszystkie obsługiwane sekwencje ucieczki. Tabela 2.2. Sekwencje ucieczki Składnia ucieczki

Opis

\\

Lewy ukośnik

\"

Cudzysłów

\'

Apostrof

\b

Backspace

\f

Wysuw strony

\n

Nowy wiersz (czasem określany też jako wysuw wiersza)

\r

Powrót karetki

\t

Tabulator poziomy

Literał w postaci ciągu znaków może także zawierać sekwencje ucieczki Unicode. Są to specjalne składnie przeznaczone do reprezentowania znaków Unicode. Sekwencja ucieczki Unicode rozpoczyna się sekwencją znaków \u, po której następują cztery cyfry szesnastkowe (0-9, A-F, a-f) bez przedzielającego znaku spacji. Na przykład sekwencja \u0041 reprezentuje wielką literę A, natomiast \u20ac reprezentuje symbol waluty euro. Literał logiczny to zastrzeżone słowo true lub zastrzeżone słowo false. Literał znakowy zawiera pojedynczy znak Unicode otoczony apostrofami (na przykład 'A'). Literał znakowy można także zaprezentować za pomocą sekwencji ucieczki (na przykład '\'') lub sekwencji ucieczki Unicode (na przykład '\u0041'). Literał całkowitoliczbowy zawiera sekwencję cyfr. Jeżeli literał ma reprezentować długą liczbę całkowitą, trzeba go zakończyć wielką literą L lub małą literą l (L jest literą bardziej czytelną). Jeżeli takiego sufiksu zabraknie, literał będzie reprezentował 32-bitową liczbę całkowitą (typu int). Literały całkowitoliczbowe można definiować w domyślnym formacie dziesiątkowym, szesnastkowym, ósemkowym, a w Javie w wersji 7 również w formacie binarnym: • Format dziesiątkowy jest formatem domyślnym. Na przykład 255. • Format szesnastkowy wymaga poprzedzenia literału sekwencją znaków 0x lub 0X, po której mają następować cyfry szesnastkowe (0-9, A-F, a-f). Na przykład 0xFF.

63

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

• Format ósemkowy wymaga poprzedzenia literału znakiem 0, po którym powinny nastąpić cyfry ósemkowe (0-7). Na przykład 077. • Format binarny wymaga poprzedzenia literału sekwencją znaków 0b lub 0B, po której mają nastąpić zera i jedynki. Na przykład 0b11111111.

W wersji 7 języka Java dodano jeszcze jedno ciekawe rozwiązanie dotyczące literałów całkowitoliczbowych. Dzięki nowemu rozwiązaniu między cyframi można wstawiać znak podkreślenia, aby zwiększyć czytelność literału, na przykład 123_456_789. Między kolejnymi cyframi literału można umieścić więcej niż jeden znak podkreślenia (na przykład 0b1111__ ´0000), lecz podkreślenie nie może już wystąpić na początku literału (jak w _123), ponieważ kompilator potraktuje literał jakby był identyfikatorem. Znak podkreślenia nie może również występować na końcu literału (jak w 123_). Literał zmiennopozycyjny zawiera część całkowitoliczbową, znak części dziesiętnej (w postaci znaku kropki .), część ułamkową, wykładnik (który rozpoczyna się literą E lub e) oraz sufiks typu (litera D, d, F lub f). Większość z tych elementów ma charakter opcjonalny, jednak literał musi zawierać co najmniej tyle informacji, aby można było odróżnić literał zmiennopozycyjny od literału całkowitoliczbowego. Przykładami literału zmiennopozycyjnego są 0.1 (liczba zmiennopozycyjna o podwójnej precyzji), 10F (liczba zmiennopozycyjna), 10D (liczba zmiennopozycyjna o podwójnej precyzji) i 3.0E+23 (liczba zmiennopozycyjna o podwójnej precyzji). Załóżmy, że trzeba utworzyć klasę, której obiekty mają zapisywać komunikaty do odpowiedniego pliku. Każda instancja klasy będzie posiadać pole identyfikujące plik docelowy, które będzie inicjalizowane domyślną nazwą pliku. Na listingu 2.5 znajduje się kod źródłowy klasy, a także instrukcja, która inicjalizuje pole. Listing 2.5. Inicjalizacja pola filename klasy Logger class Logger { String filename = "log.txt"; }

Na listingu 2.5 zastosowano prostą inicjalizację do zainicjalizowania pola, które nie jest tablicą. Ten sam mechanizm można wykorzystać również do zainicjalizowania pól tablicowych. Na przykład na listingu 2.6 pokazano, jak zainicjalizować pola tablicowe cities i temperatures. Listing 2.6. Zainicjalizowanie pól klasy WeatherData class WeatherData { String country = "United States"; String[] cities = {"Chicago", "New York", "Los Angeles"}; double[][] temperatures = {{0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}}; }

Domyślnie klasa WeatherData z listingu 2.6 zawiera dane pogodowe dla trzech miast USA. Tablica cities jest inicjalizowana listą trzech literałów ciągów znaków oddzielonych od siebie przecinkiem i umieszczonych w nawiasach klamrowych. Tablica temperatures jest inicjalizowana tablicą o trzech wierszach i dwóch kolumnach, która zawiera zera wyrażone 64

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

w postaci liczb zmiennopozycyjnych i podwójnej precyzji. Zera te znajdują się w trzyelementowej tablicy jednowierszowej, której elementy są od siebie oddzielone znakiem przecinka i każdy element jest zdefiniowany jako dwuelementowa tablica jednokolumnowa.

Wyrażenia złożone Wyrażenie złożone to sekwencja prostych wyrażeń i operatorów, w których operator to sekwencja instrukcji (reprezentowanych symbolicznie w kodzie źródłowym) odpowiedzialnych za przekształcanie jednej lub większej liczby wartości, tak zwanych operandów, w inną wartość. Na przykład symbol + jest operatorem dodawania lub łączenia ciągów znaków, a jego konkretne znaczenie zależy od typu dwóch operandów. Jeżeli symbol pojawi się między operandami liczbowymi (na przykład 6+4), zostanie wykonana czynność dodawania. Z kolei jeżeli + znajdzie się między literałami, które są ciągami znaków (na przykład "A"+"B"), zostanie wykonane połączenie tych ciągów znaków. Java obsługuje dość szeroki wachlarz operatorów, które są klasyfikowane zależnie od liczby operandów. Operator unarny to operator przykładany do jednego operandu, operator binarny jest przykładany do dwóch operandów, zaś jedyny w Javie operator trójskładnikowy jest przykładany do trzech operandów. Operatory klasyfikuje się także na podstawie przedrostka, przyrostka i wzrostka. Operator przedrostkowy to operator unarny, który poprzedza operand. Operator przyrostkowy to operator unarny, który występuje za operandem, natomiast operator wzrostkowy to operator binarny lub trójskładnikowy, który występuje między dwoma (w przypadku operatora binarnego) lub trzema (w przypadku operatora trójskładnikowego) operandami. W tabeli 2.3 opisano wszystkie dostępne operatory. (Priorytety operatorów zostaną opisane w następnym akapicie). Tabela 2.3. Operatory Operator

Symbol

Opis

Priorytet

Dodawanie

+

W wyrażeniu operand1 + operand2, w którym operand musi być typu znakowego lub liczbowego, dodaje operand1 do operand2 i zwraca ich sumę.

10

Indeks tablicy

[]

W wyrażeniu zmienna[indeks], w którym indeks musi być typu całkowitoliczbowego, odczytuje wartość lub zapisuje ją do magazynu, jakim jest zmienna, w lokalizacji wskazywanej przez indeks.

13

Przypisanie

=

W wyrażeniu zmienna = operand, w którym musi występować zgodność przypisania (to znaczy typy zmiennej i operandu muszą być zgodne), przypisuje operand zmiennej.

0

Bitowa koniunkcja

&

W wyrażeniu operand1 & operand2, w którym operandy muszą być typu znakowego lub całkowitoliczbowego, dokonuje bitowej koniunkcji odpowiadających bitów i zwraca wynik. Bit wynikowy jest ustawiany na 1, jeżeli każdy odpowiadający bit operandu również jest ustawiony na 1. W przeciwnym razie bit wynikowy jest ustawiany na 0.

6

65

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Tabela 2.3. Operatory — ciąg dalszy Operator

Symbol

Opis

Priorytet

Bitowe dopełnienie

~

W wyrażeniu ~operand, w którym operand musi być typu znakowego lub całkowitoliczbowego, zamienia bity operandu (jedynki na zera i zera na jedynki) i zwraca wynik.

12

Bitowe LUB wyłączające

^

W wyrażeniu operand1 ^ operand2, w którym każdy operand musi być typu znakowego lub całkowitoliczbowego, przykłada operator bitowego LUB wyłączającego i zwraca wynik. Bit wynikowy jest ustawiany na 1, jeżeli odpowiadający bit jednego operandu jest ustawiony na 1 i odpowiadający bit drugiego operandu jest ustawiony na 0. W przeciwnym razie bit wynikowy jest ustawiany na 0.

5

Bitowe LUB włączające

|

W wyrażeniu operand1 | operand2, w którym każdy operand musi być typu znakowego lub całkowitoliczbowego, przykłada operator bitowego LUB włączającego i zwraca wynik. Bit wynikowy jest ustawiany na 1, jeżeli któryś z odpowiadających bitów (lub obydwa) operandów jest ustawiony na 1. W przeciwnym razie bit wynikowy jest ustawiany na 0.

4

Rzutowanie

(typ)

W wyrażeniu (typ) operand przekształca operand do odpowiadającej mu wartości reprezentowanej przez typ. Na przykład operatora można użyć do przekształcenia wartości zmiennopozycyjnej w 32-bitową liczbę całkowitą.

12

Przypisanie złożone

+=, -=, *=, /=, %=, &=, |=, ^=, =, >>>=

W wyrażeniu zmienna operator operand, w którym operator jest jednym z symboli wskazanych w kolumnie z lewej strony, a operand jest zgodny z sensie przypisania ze zmienną, wykonuje operację wskazaną przez operator. Wartość zmiennej jest wartością lewego operandu operatora i jest ona przypisywana zmiennej.

0

Warunek

?:

W wyrażeniu operand1 ? operand2 : operand3, w którym wszystkie operandy muszą być typu logicznego, zwracany jest operand2, jeśli operand1 jest równy true, lub operand3, jeżeli operand1 ma wartość false. Typy operandu1 i operandu2 muszą być zgodne.

1

Warunkowa koniunkcja

&&

W wyrażeniu operand1 && operand2, w którym każdy operand musi być typu logicznego, zwraca wartość true, jeżeli obydwa operandy mają wartość true. W przeciwnym razie zwracana jest wartość false. Jeżeli operand1 ma wartość false, wartość operandu2 nie jest sprawdzana. Jest to tak zwany skrót.

3

Warunkowa alternatywa

||

W wyrażeniu operand1 || operand2, w którym obydwa operandy muszą być typu logicznego, zwraca wartość true, jeżeli co najmniej jeden operand ma wartość true. W przeciwnym razie zwracana jest wartość false. Jeżeli operand1 ma wartość true, wartość operandu2 nie jest sprawdzana. Jest to tak zwany skrót.

2

66

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

Tabela 2.3. Operatory — ciąg dalszy Operator

Symbol

Opis

Priorytet

Dzielenie

/

W wyrażeniu operand1 / operand2, w którym obydwa operandy muszą być typu znakowego lub liczbowego, dzieli operand1 przez operand2 i zwraca iloraz.

11

Równość

==

W wyrażeniu operand1 == operand2, w którym obydwa operandy muszą być zgodne w sensie przypisania (nie można na przykład porównywać liczby całkowitej z literałem będącym ciągiem znaków), sprawdza równość obydwóch operandów i zwraca true, jeżeli operandy są równe. W przeciwnym razie zwracana jest wartość false.

7

Nierówność

!=

W wyrażeniu operand1 != operand2, w którym obydwa operandy muszą być zgodne w sensie przypisania (nie można na przykład porównywać liczby całkowitej z literałem będącym ciągiem znaków), sprawdza brak równości między obydwoma operandami i zwraca wartość true, jeżeli operandy nie są sobie równe. W przeciwnym razie zwracana jest wartość false.

7

Przesunięcie w lewo

operand2, w którym obydwa operandy muszą być typu znakowego lub liczbowego, zwraca wartość true, jeżeli operand1 jest większy niż operand2. W przeciwnym razie zwraca wartość false.

8

Relacyjne większe niż lub równe

>=

W wyrażeniu operand1 >= operand2, w którym obydwa operandy muszą być typu znakowego lub liczbowego, zwraca wartość true, jeżeli operand1 jest większy niż operand2 lub równy operandowi2. W przeciwnym razie zwraca wartość false.

8

68

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

Tabela 2.3. Operatory — ciąg dalszy Operator

Symbol

Opis

Priorytet

Relacyjne mniejsze niż


> operand2, w którym obydwa operandy muszą być typu znakowego lub całkowitoliczbowego, przesuwa binarną reprezentację operandu1 w prawo o liczbę bitów wskazywaną przez operand2. W każdym przesunięciu kopia bitu znaku (czyli bitu najbardziej znaczącego) jest przesuwana w prawo, zaś bit najmniej znaczący jest usuwany. W przypadku liczb całkowitych 32-bitowych przesuwanych jest tylko pięć najmniej znaczących bitów (aby zapobiec przesuwaniu większej liczby bitów, niż składa się na 32-bitową liczbę całkowitą). W przypadku liczb całkowitych 64-bitowych przesuwanych jest tylko sześć najmniej znaczących bitów (aby zapobiec przesuwaniu większej liczby bitów, niż składa się na 64-bitową liczbę całkowitą). W trakcie operacji przesuwania utrzymywane są wartości ujemne. Operacja przesuwania odpowiada dzieleniu przez wielokrotność liczby dwa, a jednocześnie jest wykonywana szybciej niż standardowe dzielenie.

9

Łączenie ciągów znaków

+

W wyrażeniu operand1 + operand2, w którym co najmniej jeden operand jest typu String, dokleja reprezentację operandu2 w postaci ciągu znaków do reprezentacji operandu1 w postaci ciągu znaków i zwraca ciąg znaków powstały w wyniku połączenia.

10

Odejmowanie

-

W wyrażeniu operand1 – operand2, w którym obydwa operandy muszą być typu znakowego lub liczbowego, odejmuje operand2 od operandu1 i zwraca różnicę.

10

69

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Tabela 2.3. Operatory — ciąg dalszy Operator

Symbol

Opis

Priorytet

Unarny minus

-

W wyrażeniu –operand, w którym operand musi być typu znakowego lub liczbowego, zwraca wartość przeciwną do wartości operandu.

12

Unarny plus

+

Podobnie jak operator poprzedni, lecz w tym przypadku zwracany jest operand. Operator rzadko używany.

12

Przesunięcie w prawo bez znaku

>>>

W wyrażeniu operand1 >>> operand2, w którym obydwa operandy muszą być typu znakowego lub całkowitoliczbowego, przesuwa binarną reprezentację operandu1 w prawo o liczbę bitów wskazywaną przez operand2. W każdym przesunięciu jako bit najbardziej znaczący wstawiane jest 0, natomiast bit najmniej znaczący jest usuwany. W przypadku liczb całkowitych 32-bitowych przesuwanych jest tylko pięć najmniej znaczących bitów (aby zapobiec przesuwaniu większej liczby bitów, niż składa się na 32-bitową liczbę całkowitą). W przypadku liczb całkowitych 64-bitowych przesuwanych jest tylko sześć najmniej znaczących bitów (aby zapobiec przesuwaniu większej liczby bitów, niż składa się na 64-bitową liczbę całkowitą). W trakcie operacji przesuwania wartości ujemne nie są utrzymywane. Operacja przesuwania odpowiada dzieleniu przez wielokrotność liczby dwa, a jednocześnie jest wykonywana szybciej niż standardowe dzielenie.

9

Zawarte w kolumnie Opis tabeli 2.3 sformułowanie „typ całkowitoliczbowy” dotyczy dowolnego spośród typów: bajtowej liczby całkowitej, krótkiej liczby całkowitej albo długiej liczby całkowitej, o ile tylko liczba całkowita zajmuje najwyżej 32 bity. Natomiast sformułowanie „typ liczbowy” dotyczy dowolnego spośród wymienionych typów całkowitoliczbowych, a także typu zmiennopozycyjnego oraz typu zmiennopozycyjnego z podwójną precyzją. W prawej kolumnie tabeli 2.3 znajdują się wartości, które wskazują priorytet (pierwszeństwo) danego operatora. Im wyższa jest to wartość, tym wyższy priorytet operatora. Na przykład priorytet dodawania to 10, a mnożenie ma priorytet 11, co oznacza, że w trakcie obliczania wartości wyrażenia 40+2*4 najpierw wykonane zostanie mnożenie, a dopiero potem dodawanie. Priorytety można obchodzić przez wstawianie w wyrażeniu nawiasów otwierającego i zamykającego (odpowiednio ( i )). Wówczas wyrażenie znajdujące się w najbardziej wewnętrznej parze nawiasów jest obliczane jako pierwsze. Na przykład w wyrażeniu (40+2)*4 najpierw zostanie wykonane dodawanie, a potem mnożenie, natomiast w wyrażeniu 40/(2-4) najpierw zostanie wykonane odejmowanie, a dopiero potem dzielenie. W trakcie obliczania wartości wyrażenia operatory z tym samym priorytetem (na przykład dodawania i odejmowania, które mają ten sam priorytet 10) są przetwarzane zgodnie z ich łącznością (ang. associativity). Jest to cecha, która wyznacza sposób grupowania operatorów o tym samym priorytecie, w sytuacji gdy nie zastosowano nawiasów. Na przykład wyrażenie 6*3/2 zostanie obliczone tak jak wyrażenie (6*3)/2, ponieważ zarówno *, jak i / charakteryzują się łącznością od lewej do prawej strony. Z kolei wyrażenie a=b=c=10 zostanie obliczone tak jak wyrażenie a=(b=(c=10)), to znaczy najpierw zmiennej c 70

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

zostanie przypisana wartość 10, nowa wartość zmiennej c (czyli 10) zostanie przypisana zmiennej b, a nowa wartość zmiennej b (10) zostanie przypisana zmiennej a. Wynika to z faktu, że = jest operatorem łączącym od prawej do lewej. Większość operatorów języka Java charakteryzuje się łącznością lewostronną (od strony lewej do prawej). Do operatorów łączności prawostronnej zaliczają się operatory przypisania, dopełnienia bitowego, rzutowania, przypisania złożonego, warunkowy, dopełnienia logicznego, tworzenia obiektu, predekrementacji, preinkrementacji, unarny minus i unarny plus. Za każdym razem, gdy operandy danego operatora mają różne typy, operator próbuje przekształcić jeden z operatorów (albo obydwa) do typu odpowiedniego dla wykonywanej operacji. Na przykład jeżeli 16-bitowa krótka liczba całkowita jest dodawana do 32-bitowej liczby całkowitej, operator dodawania najpierw przekształci krótką liczbę całkowitą w 32-bitową wartość całkowitoliczbową, a następnie doda do siebie dwie całkowitoliczbowe wartości 32-bitowe. Analogicznie, jeżeli podjęta zostanie próba dodania 32-bitowej liczby całkowitej do 32-bitowej liczby zmiennopozycyjnej, operator najpierw przekształci 32-bitową liczbę całkowitą w 32-bitową liczbę zmiennopozycyjną, a następnie doda do siebie obydwie wartości zmiennopozycyjne. Operator zawsze przekształca wartość o bardziej ograniczonej reprezentacji (na przykład 16-bitową liczbę całkowitą) w odpowiadającą jej wartość o mniej ograniczonej reprezentacji (na przykład w 32-bitową liczbę całkowitą). Czasami przekształcenie trzeba wykonać jawnie; w takich przypadkach stosuje się rzutowanie. Załóżmy na przykład, że w klasie znajdują się następujące deklaracje pól: char c = 'A'; byte b = c;

W momencie napotkania instrukcji b = c kompilator zwróci błąd z informacją o utracie precyzji. Przyczyną jest fakt, że c może reprezentować dowolną liczbę całkowitą bez znaku z przedziału od 0 do 65 535, natomiast b może reprezentować wyłącznie liczbę całkowitą ze znakiem z przedziału od –128 do 127. Wprawdzie wartości 'A' odpowiada wartość +65, która należy do zakresu dozwolonych wartości pola b, lecz równie dobrze polu c można by przypisać wartość początkową '\u123', która już wykracza poza ten zakres. Dlatego ze względu na możliwą utratę precyzji kompilator zwraca błąd. Rozwiązaniem problemu jest wykonanie rzutowania operatorem (byte), który jawnie wskaże kompilatorowi, że programista jest świadomy możliwości utraty precyzji, lecz mimo wszystko chce wykonać przekształcenie: byte b = (byte) c;

Jednak nie wszystkie przekształcenia da się wykonać. Na przykład operator odejmowania nie może od 32-bitowej liczby całkowitej odjąć literału będącego ciągiem znaków. Ponadto próba zrzutowania wartości zmiennopozycyjnej na klasę String, na przykład w instrukcji String s = (String) 20.0;, nie powiedzie się. W takich sytuacjach kompilator zawsze zwróci odpowiedni komunikat błędu. Operatory matematyczne (+, -, *, /, %) mogą zwracać wartości, które wykraczają poza graniczne wartości typu wartości wynikowej. Na przykład pomnożenie dwóch dużych, dodatnich, 32-bitowych liczb całkowitych może dać w wyniku wartość, której nie będzie już można zaprezentować jako 32-bitowej liczby całkowitej. Java nie rozpoznaje takich przypadków. 71

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Ciekawy wynik daje również próba podzielenia wartości liczbowej przez 0 za pomocą operatorów / lub %. W pierwszym przypadku próba podzielenia liczby całkowitej przez 0 spowoduje rzucenie obiektu wyjątku ArithmeticException (więcej na temat wyjątku Arithmetic ´Exception powiemy w rozdziale 4., gdy będą bliżej opisywane wyjątki). W drugim przypadku natomiast podzielenie wartości zmiennopozycyjnej przez 0 spowoduje, że operator zwróci plus nieskończoność lub minus nieskończoność, zależnie od tego, czy dzielna miała wartość dodatnią, czy ujemną. Z kolei dzielenie zmiennopozycyjnej wartości 0 przez 0 spowoduje, że operator zwróci wynik NaN (ang. Not a Number, czyli wartość nieliczbowa). Załóżmy, że zaimplementowano klasę ReportWriter, która przesyła raporty do drukarki. W czasie testowania implementacji chcemy zapewnić, by raporty były generowane prawidłowo, a jednocześnie nie chcemy zużywać na to papieru. Można więc przekierować wyniki do pliku. Kolejne wymaganie stanowi, że musi istnieć możliwość zmiany nazwy pliku lub nazwy drukarki w kodzie aplikacji. Jedno z możliwych rozwiązań przedstawiono na listingu 2.7. Listing 2.7. Inicjalizacja pola outputDevice klasy ReportWriter z wykorzystaniem operatora warunkowego class ReportWriter { static boolean test = true; static String outputDevice = (test) ? "file" : "printer"; }

Widoczna na listingu 2.7 klasa ReportWriter spełnia pierwsze wymaganie w ten sposób, że zawiera pole klasy o nazwie test. W momencie ładowania klasy polu test jest przypisywana wartość true, a więc nadpisywana jest domyślna wartość false pola logicznego. W wyrażeniu, które jest przypisywane polu klasy outputDevice, zastosowano operator warunkowy, aby dokonać sprawdzenia wartości pola test. Jeżeli pole test będzie mieć wartość true, operator zwróci literał ciągu znaków "file", który zostanie przypisany polu outputDevice. Jeżeli natomiast test będzie mieć wartość false, operator zwróci wartość "printer" i to ta wartość zostanie przypisana polu outputDevice. Aby spełnić drugie wymaganie, wystarczyło po prostu w kodzie aplikacji przypisać odpowiedni literał ciągu znaków, wskazujący przeznaczenie raportu, polu outputDevice.

Pola tylko do odczytu Java udostępnia zastrzeżone słowo final, które służy do deklarowania pól tylko do odczytu. Każdy obiekt posiada własną kopię pola instancji tylko do odczytu. Pole to musi zostać zainicjalizowane w deklaracji pola lub w konstruktorze klasy. Jeżeli pole jest inicjalizowane w konstruktorze, wówczas pole instancji tylko do odczytu jest nazywane pustym polem finalnym (ang. blank final), ponieważ nie posiada ono żadnej wartości do momentu jej przypisania w konstruktorze. Ze względu na to, że w konstruktorze pustemu polu finalnemu każdego obiektu można przypisać różne wartości, zmienne tylko do odczytu nie są prawdziwymi stałymi. Aby uzyskać prawdziwą stałą, czyli pojedynczą wartość tylko do odczytu dostępną dla wszystkich obiektów, należy utworzyć pole klasy tylko do odczytu. W tym celu w deklaracji pola trzeba, oprócz słowa zastrzeżonego final, umieścić także słowo zastrzeżone static. 72

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

Na listingu 2.8 znajduje się deklaracja pola klasy tylko do odczytu. Listing 2.8. Deklaracja prawdziwej stałej w klasie Employee class Employee { final static int RETIREMENT_AGE = 65; }

Obecna na listingu 2.8 deklaracja RETIREMENT_AGE jest przykładem stałej fazy kompilacji. Ponieważ istnieje tylko jedna kopia jej wartości (o czym decyduje słowo zastrzeżone static) i nigdy nie ulegnie ona zmianie (o czym decyduje słowo zastrzeżone final), kompilator może optymalizować skompilowany kod przez wstawianie wartości stałej we wszystkich obliczeniach, w których jest ona używana. Kod działa szybciej, ponieważ nie trzeba odczytywać wartości pola klasy tylko do odczytu.

Metody Po zadeklarowaniu klasy można w jej kodzie zadeklarować metody. Niektóre metody mogą być powiązane z obiektami — za ich pomocą opisuje się wówczas zachowanie encji. Inne metody można wiązać z klasami. W tym punkcie pokażemy, jak deklaruje się metody, jak się je implementuje oraz w jaki sposób się je przeciąża.

Deklarowanie metod Aby zadeklarować metodę w ciele klasy, trzeba co najmniej wskazać typ wartości zwracanej przez metodę, podać identyfikator stanowiący nazwę metody wraz z listą jej parametrów, po czym zdefiniować ciało metody. Na listingu 2.9 przedstawiono deklarację prostej metody. Listing 2.9. Deklaracja metody printBalance() klasy CheckingAccount class CheckingAccount { String owner; int balance; static int counter; void printBalance() { // kod, który zwraca wartość pola balance } }

Na listingu 2.9 w ciele klasy CheckingAccount została zadeklarowana metoda o nazwie print ´Balance. Zgodnie z konwencją nazwa metody zaczyna się małą literą, natomiast pierwsza litera każdego kolejnego słowa wchodzącego w skład nazwy metody zaczyna się wielką literą. Lista parametrów metody wskazuje liczbę, kolejność i typy wartości przekazywanych do metody w momencie jej wywołania. Lista parametrów znajduje się w nawiasach okrągłych, w których deklaracje zmiennych są oddzielane od siebie znakiem przecinka. Każda wartość przekazywana do metody to tak zwany argument, natomiast zmienna, której argument jest 73

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

przypisywany, to parametr. Ponieważ do metody printBalance nie przekazuje się żadnych argumentów, lista parametrów tej metody jest pusta. „ Uwaga • Nazwa metody oraz typy jej parametrów tworzą łącznie sygnaturę metody.

Ciało metody to kod, który ma zostać wykonany w momencie wywołania metody. W przedstawionym przykładzie ciało metody jest puste i zawiera jedynie jeden wiersz komentarza, który opisuje, co metoda tak naprawdę powinna robić. Ostatni element to typ wynikowy, który wskazuje, jakiego typu wartość będzie zwracać metoda. Ponieważ metoda printBalance() nie zwraca żadnej wartości, jako jej typ wynikowy wskazano void, do czego użyto słowa zastrzeżonego void. (Można przyjąć, że void to specjalny typ podstawowy, choć z drugiej strony zmiennej nie można zadeklarować jako typu void i dlatego też nie wspomniano o nim w tabeli 2.1). Ponieważ klasa CheckingAccount powinna także zawierać metody odpowiedzialne za dokonywanie wpłat i wypłat na konto, na listingu 2.10 rozszerzono kod klasy o odpowiednie metody deposit() i withdraw(). Listing 2.10. Deklaracja metod deposit() i withdraw() class CheckingAccount { String owner; int balance; static int counter; void printBalance() { // kod, który zwraca wartość pola balance } int deposit(int amount) { // kod, który dodaje określoną ilość pieniędzy do salda konta i zwraca nowe saldo } int withdraw(int amount) { /* kod, który wycofuje z konta określoną kwotę pieniędzy i zwraca nowe saldo */ } }

Obecne na listingu 2.10 metody deposit() i withdraw() zawierają niepuste listy parametrów i obydwie listy zawierają tylko jeden parametr o nazwie amount. Podobnie jak w przypadku każdego innego parametru czas życia parametru amount rozpoczyna się w punkcie wejścia do metody i kończy się w momencie wyjścia z metody i powrotu do kodu wywołującego (czyli kodu, który wywołał metodę). Zasięg parametru amount to cała metoda. Ponadto obydwie metody deposit() i withdraw() zwracają wartość całkowitoliczbową, o czym świadczy typ int jako typ wartości zwracanej. Ponieważ nie jesteśmy jeszcze gotowi, by zaimplementować ciała metod, zawarto w nich odpowiednie komentarze opisujące sposób działania metod. W odróżnieniu od metod printBalance() i deposit(), w metodzie withdraw() znajduje się komentarz wielowierszowy, 74

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

czyli jeden lub większa liczba wierszy dokumentacji. Komentarz wielowierszowy zaczyna się sekwencją znaków /* i kończy znakami */. Kompilator ignoruje wszystko, co znajduje się między tymi sekwencjami znaków. „ Ostrzeżenie • Komentarza wielowierszowego nie można zagnieżdżać w innym komentarzu wielowier-

szowym: /*/* Zagnieżdżanie komentarzy wielowierszowych jest zabronione! */*/.

Metody printBalance(), deposit() i withdraw() klasy CheckingAccount są przykładami metod instancji, ponieważ są one powiązane z obiektami. Każda z tych metod jest wywoływana z ukrytym argumentem, który odnosi się do bieżącego obiektu. Dzięki ukrytemu argumentowi metoda może korzystać z pól instancji obiektu oraz wywoływać inne metody instancji. Ponadto metody instancji mogą odczytywać pola klas i wywoływać metody klas. W większości przypadków w zupełności wystarczy zdefiniowanie metod instancji. Jednak gdy implementuje się aplikację, w przynajmniej jednej klasie aplikacji trzeba zdefiniować co najmniej jedną metodę klasy, czyli metodę, która będzie powiązana z klasą, a nie z obiektem klasy. W szczególności aplikacja musi zawierać metodę public static void main(String[] args), która stanowi punkt początkowy aplikacji. To właśnie słowo zastrzeżone static decyduje o tym, że metoda staje się metodą klasy. (Znaczenie słowa zastrzeżonego public zostanie opisane w dalszej części tego rozdziału). „ Uwaga • Metoda main() jest wywoływana z tablicą obiektów String, w której każdy obiekt zawiera

znakową reprezentację argumentu wiersza poleceń.

Ponieważ metody klas nie są wywoływane z ukrytym argumentem, który wskazuje na bieżący obiekt, w metodzie main() nie uzyska się dostępu do pól instancji obiektu ani nie wywoła się metod instancji. Metoda main() może jedynie odczytywać pola klas i wywoływać metody klas. Wiele metod wymaga, by w momencie ich wywoływania przekazać do nich określoną liczbę argumentów. Java pozwala jednak również na przekazywanie zmiennej liczby argumentów. Aby zadeklarować metodę, która będzie przyjmować zmienną liczbę argumentów, należy po nazwie typu skrajnego prawego parametru wpisać trzy kropki. Na listingu 2.11 trzy kropki zostały wstawione po nazwie typu double. Listing 2.11. Deklaracja metody sum(), która przyjmuje zmienną liczbę parametrów double sum(double... values) { // kod, który sumuje przekazane do metody argumenty typu double i zwraca tę sumę }

Sposób zaimplementowania zarówno tej metody, jak i metod zadeklarowanych wcześniej zostanie omówiony za chwilę. Następnie pokażemy, jak każdą z tych metod wywołać.

75

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Implementacja metod Pokazana na listingu 2.10 pusta metoda printBalance() nie wykonuje na razie żadnych czynności. Co więcej, puste metody deposit() i withdraw() powodują błędy kompilatora, ponieważ zadeklarowano dla nich niepusty typ zwracanych wartości, podczas gdy faktycznie metody te niczego nie zwracają. Aby sprawić, że metody te staną się przydatne i nie będą prowadzić do błędów kompilatora, należy umieścić w ich ciele jakieś instrukcje. Instrukcja to grupa działań, których celem jest wykonanie zadania. Może ona przybrać postać instrukcji prostej lub instrukcji złożonej. Instrukcja prosta to pojedyncze działanie zakończone znakiem średnika. Instrukcją prostą jest nie tylko instrukcja pusta, ale także deklaracja zmiennej, przypisanie, wywołanie metody, podjęcie decyzji, pętla (czyli działanie wykonywane w sposób powtarzalny), przerwa i kontynuacja, a także zwrócenie wartości przez metodę. (Dodatkowe rodzaje instrukcji prostych zostaną przedstawione w rozdziale 4.). Instrukcja złożona to (potencjalnie pusta) sekwencja instrukcji prostych oraz innych instrukcji złożonych zamkniętych w nawiasach klamrowych. Przykładem instrukcji złożonej jest ciało metody. Instrukcje złożone mogą występować wszędzie tam, gdzie mogą występować instrukcje proste. Instrukcje złożone nazywa się czasem blokiem.

Instrukcje puste, deklaracje zmiennych lokalnych, przypisania i wywołania metod Instrukcja pusta to instrukcja, która niczego nie wykonuje, zakończona średnikiem. Wprawdzie tego rodzaju instrukcja wydaje się bezużyteczna, jednak tak naprawdę może być przydatna, o czym więcej powiemy przy okazji omawiania pętli. Deklaracja zmiennej lokalnej to instrukcja, która wprowadza zmienną w ciele metody lub w innej instrukcji złożonej. Czas życia oraz zasięg zmiennej lokalnej rozciąga się od punktu, w którym zmienną zadeklarowano, do końca instrukcji złożonej, w której ta deklaracja się znajduje. Dostęp do zmiennej lokalnej jest możliwy jedynie w tych instrukcjach, które występują po deklaracji zmiennej. „ Uwaga • Zmienna lokalna przypomina parametr, ponieważ zarówno zmienna lokalna, jak i parametr

istnieją jedynie w trakcie wykonywania metody. Jednak parametr zawiera przekazany argument, podczas gdy zmienna lokalna zawiera dowolną wartość niezbędną do wykonania zadania przez metodę.

Instrukcja deklaracji zmiennej lokalnej jest podobna do deklaracji pola instancji, ponieważ musi ona zawierać co najmniej nazwę typu, identyfikator stanowiący nazwę zmiennej lokalnej i kończyć się znakiem średnika. Zgodnie z konwencją nazwa zmiennej lokalnej rozpoczyna się małą literą, natomiast pierwsza litera każdego kolejnego słowa wchodzącego w skład nazwy zmiennej lokalnej zaczyna się już wielką literą. „ Ostrzeżenie • Nie można zadeklarować więcej niż jednej zmiennej o tej samej nazwie w tym samym

zasięgu, ponieważ w takim przypadku kompilator zwróci komunikat błędu o treści „already defined”.

76

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

Instrukcja przypisania jest blisko związana z instrukcją deklaracji zmiennej lokalnej. Zamiast od typu instrukcja ta zaczyna się nazwą zadeklarowanej już wcześniej zmiennej, po której następuje operator przypisania (=) lub operator przypisania złożonego (na przykład +=). Instrukcja kończy się wyrażeniem lub znakiem średnika. Wywołanie metody powoduje wykonanie metody; w ramach tej instrukcji do metody mogą zostać przekazane jakieś argumenty, a wynikiem wykonania instrukcji może być uzyskanie jakiejś wartości wynikowej zwróconej przez metodę. Bez względu na to, czy metoda zwraca wartość, czy nie, wywołanie metody zawsze jest instrukcją. Jeżeli wartość wynikowa metody nie zostanie przypisania zmiennej, wartość ta zostanie utracona. Czasami jednak bardziej istotne jest to, co metoda rzeczywiście wykonuje, niż jej wartość wynikowa. Metoda printBalance() widoczna na listingu 2.12 prezentuje przykład wszystkich wspomnianych instrukcji oprócz instrukcji pustej. Listing 2.12. Deklaracja i wykorzystanie zmiennych lokalnych w metodzie printBalance() void printBalance() { int magnitude = (balance < 0) ? -balance : balance; String balanceRep = (balance < 0) ? "(" : ""; balanceRep += magnitude; balanceRep += (balance < 0) ? ")" : ""; System.out.println(balanceRep); }

Na listingu 2.12 w ciele metody printBalance() umieszczono kilka instrukcji, których celem jest stworzenie ciągu znaków stanowiącego reprezentację wartości pola balance, a następnie zwrócenie tego ciągu znaków. Jeżeli wartość jest dodatnia, jej reprezentacja nie będzie się różnić od oryginalnej wartości. Natomiast jeżeli wartość pola będzie ujemna, reprezentacja będzie zawierać liczbę dodatnią umieszczoną w nawiasach okrągłych. Pierwsze dwie instrukcje z listingu zawierają deklarację dwóch zmiennych lokalnych o nazwach magnitude i balanceRep. Wyrażenie inicjalizuje zmienną magnitude wartością bezwzględną pola balance, natomiast kolejne wyrażenie inicjalizuje pole balanceRep wartością "(" lub "" (pustym ciągiem znaków), zależnie od tego, czy pole balance ma wartość dodatnią, czy ujemną. W odróżnieniu od pól, które są inicjalizowane wartościami domyślnymi, oraz parametrów, które są inicjalizowane wartościami argumentów, zmienne lokalne nie są inicjalizowane niejawnie. Zanim będzie można odczytać wartość zmiennej lokalnej, trzeba najpierw tę wartość zmiennej przypisać. W przeciwnym razie kompilator zwróci błąd z informacją, że zmienna lokalna nie została zainicjalizowana. Po instrukcjach deklaracji zmiennych lokalnych następują dwie instrukcje przypisania. Instrukcje przypisania powodują dodanie do wartości zmiennej balanceRep ciągu znaków reprezentującego wartość bezwzględną pola balance wraz z ciągiem znaków ")" lub "". Rozwiązaniem alternatywnym mogłoby być wyrażenie pierwszej z tych instrukcji w postaci balanceRep = balanceRep + magnitude;. Kod widoczny na listingu 2.12 kończy się wywołaniem metody System.out.println(), której zadaniem jest zwrócenie znakowej reprezentacji wartości zmiennej balanceRep do standardowego wyjścia. (W rozdziale 1. krótko opisano standardowe wejście-wyjście oraz System.out.println() i jej podobne metody). 77

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Podejmowanie decyzji Na listingu 2.12 użyto operatora warunkowego, aby ustalić, czy zmiennej magnitude przypisać wartość –balance, czy balance. Wprawdzie operator warunkowy przydaje się przede wszystkim do inicjalizowania zmiennej jedną z dwóch wartości, jednak nie można go użyć, aby dokonać wyboru między dwiema instrukcjami do wykonania. Do tego celu Java udostępnia instrukcję if-else. Instrukcja if-else ma następującą składnię: if (wyrażenie logiczne) instrukcja1 else instrukcja2

Instrukcja składa się ze słowa zastrzeżonego if, po którym następuje wyrażenie logiczne zamknięte w okrągłych nawiasach, instrukcja przeznaczona do wykonania, w przypadku gdy wyrażenie logiczne będzie mieć wartość true, słowo zastrzeżone else oraz kolejna instrukcja do wykonania, w przypadku gdy wyrażenie logiczne będzie mieć wartość false. Na listingu 2.13 przedstawiono przykładową instrukcję if-else. Listing 2.13. Zmodyfikowana metoda printBalance() void printBalance() { if (balance < 0) System.out.println("(" + -balance + ")"); else System.out.println(balance); }

Instrukcja if-else widoczna na listingu 2.13 spowoduje, że jeżeli wartość zmiennej balance będzie mniejsza niż 0, to zostanie wykonana pierwsza metoda System.out.println(), natomiast druga metoda System.out.println() zostanie wywołana, w przypadku gdy wartość zmiennej balance będzie większa lub równa 0. Instrukcje instrukcja1 i instrukcja2 symbolizują kolejne instrukcje do wykonania. Jeżeli w przedstawionej składni część else jest niepotrzebna, można w niej pominąć fragment else i instrukcja2. Do wykonania pozostanie wówczas tylko instrukcja if. „ Uwaga • Jeżeli instrukcje if i if-else zostaną użyte łącznie i programista nie zastosuje odpowiednich

wcięć, ustalenie, która z instrukcji if jest powiązana z instrukcją else, może być bardzo trudne. Na przykład: if (car.door.isOpen()) if (car.key.isPresent()) car.start(); else car.door.open();

Czy programista chciał, by instrukcja else dotyczyła wewnętrznej instrukcji if, lecz ze względu na nieprawidłowo sformatowany kod można odnieść inne wrażenie? Na przykład: if (car.door.isOpen()) if (car.key.isPresent())

78

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA car.start(); else car.door.open();

Jeżeli metody car.door.isOpen() i car.key.isPresent() jednocześnie zwrócą wartość true, zostanie wykonana metoda car.start(). Jeżeli natomiast car.door.isOpen() zwróci true, a car.key. ´isPresent() zwróci false, zostanie wykonana metoda car.door.open() — wszak otwieranie otwartych drzwi nie ma sensu. Wygląda na to, że programista chciał powiązać instrukcję else z zewnętrzną instrukcją if, lecz zapomniał o tym, że else odnosi się zawsze do najbliższej instrukcji if. Problem ten można naprawić w ten sposób, że wewnętrzną instrukcję if umieści się w nawiasach w następujący sposób: if (car.door.isOpen()) { if (car.key.isPresent()) car.start(); } else car.door.open();

Gdy car.door.isOpen() zwróci true, zostanie wykonana instrukcja złożona. Jeżeli natomiast metoda ta zwróci false, zostanie wykonana metoda car.door.open(), co jest jak najbardziej uzasadnione. Gdy programista zapomni, że instrukcja else zawsze odnosi się do najbliższego if, i w dodatku źle sformatuje kod, który tę regułę ukryje, doprowadzi to do wystąpienia problemu wiszącego else.

Instrukcje if-else można umieszczać jedna za drugą; mamy wówczas do czynienia z następującą składnią: if (wyrażenie logiczne1) instrukcja1 else if (wyrażenie logiczne2) instrukcja2 else … else instrukcjaN

Jeżeli pierwsze wyrażenie logiczne ma wartość true, zostanie wykonana instrukcja1. W przeciwnym razie — jeśli drugie wyrażenie logiczne będzie mieć wartość true — zostanie wykonana instrukcja2. Taki schemat postępowania obowiązuje do momentu, aż jedno z wyrażeń będzie mieć wartość true i zostanie wykonana odpowiadająca mu instrukcja bądź też zostanie osiągnięta ostatnia instrukcja else i nastąpi wykonanie instrukcjiN (czyli instrukcji domyślnej). Na listingu 2.14 zaprezentowano następujące po sobie przykładowe instrukcje if-else. Listing 2.14. Zmodyfikowana metoda printBalance(), w której zawarto następujące po sobie instrukcje if-else void printBalance() { if (balance < 0) System.out.println("(" + -balance + ")");

79

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID else if (balance == 0) System.out.println("zero balance"); else System.out.println(balance); }

Warto przyjrzeć się bliżej kodowi z listingu 2.14. Widać na nim, że następujące po sobie instrukcje if-else to tak naprawdę jedna instrukcja if-else, w której w pierwszej części else znajduje się kolejna instrukcja if-else. Kod, w którym następują po sobie instrukcje if-else, jest rozwlekły i w niektórych przypadkach można go zapisać bardziej zwięźle za pomocą instrukcji switch. Instrukcja switch służy do tworzenia kodu, w którym wybiera się jedną lub kilka instrukcji do wykonania. Składnia tej instrukcji przedstawia się następująco: switch (wyrażenie selektora) { case wartość1: instrukcja1 [break;] case wartość2: instrukcja2 [break;] … case wartośćN: instrukcjaN [break;] [default: instrukcja] }

Instrukcja switch składa się ze słowa zastrzeżonego switch, po którym następuje wyrażenie selektora zamknięte w nawiasach okrągłych; po wyrażeniu selektora znajduje się natomiast lista przypadków. Wyrażenie selektora to zazwyczaj wyrażenie, którego wartością jest liczba całkowita. Wynikiem wyrażenia selektora może być na przykład 32-bitowa liczba całkowita albo 16-bitowy znak. Każdy przypadek rozpoczyna się słowem zastrzeżonym case, a po nim następuje literał i znak dwukropka (:). Następnie wpisuje się instrukcję, która ma zostać wykonana, i — opcjonalnie — instrukcję break (o której więcej powiemy nieco później). Po obliczeniu wartości wyrażenia selektora instrukcja switch porównuje ją z wartością wskazaną w każdym przypadku, aż obydwie wartości będą sobie równe. Jeżeli zostanie odnaleziony przypadek z wartością identyczną jak wartość wyrażenia selektora, ten właśnie przypadek zostanie wykonany. Na przykład jeżeli wyrażenie selektora będzie mieć wartość wartość1, zostanie wykonana instrukcja1. Opcjonalna instrukcja break (tak naprawdę cały kod znajdujący się w nawiasach kwadratowych jest opcjonalny), która składa się ze słowa zastrzeżonego break i znaku średnika, zapobiega przejściu potoku wykonania do instrukcji zdefiniowanej już dla następnego przypadku. Dzięki instrukcji break wykonanie zostanie wznowione na pierwszej instrukcji po instrukcji switch. „ Uwaga • Zwykle instrukcję break umieszcza się po instrukcji danego przypadku. Jeżeli instrukcji break

nie umieści się w kodzie, może to być przyczyną trudnych do zidentyfikowania błędów. Można jednak wyobrazić sobie sytuacje, w których warto pogrupować ze sobą kilka przypadków, by wykonywać je we wspólnym bloku kodu źródłowego. W takich sytuacjach w wybranych przypadkach instrukcję break się pomija.

80

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

Jeżeli żadna wartość przypadku nie będzie odpowiadać wartości wyrażenia selektora i zdefiniowano przypadek domyślny (oznaczony słowem zastrzeżonym default i znakiem dwukropka), wówczas zostanie wykonana instrukcja zdefiniowana dla przypadku domyślnego. Na listingu 2.15 przedstawiono przykładową instrukcję switch. Listing 2.15. Wykorzystanie instrukcji switch do wyświetlenia kierunku poruszania się class Compass { static final int POLNOC = 0; static final int POLUDNIE = 1; static final int ZACHOD = 2; static final int WSCHOD = 3; void printDirection(int dir) { switch (dir) { case POLNOC : System.out.println("Idziesz na północ."); break; case POLUDNIE: System.out.println("Idziesz na południe."); break; case WSCHOD : System.out.println("Idziesz na wschód."); break; case ZACHOD : System.out.println("Idziesz na zachód."); break; default : System.out.println("Kierunek nieznany"); } } }

Widoczna na listingu 2.15 klasa Compass to przykład typu wyliczeniowego, czyli nazwanej sekwencji powiązanych ze sobą stałych. Zbiór Compass zawiera stałe POLNOC, POLUDNIE, WSCHOD i ZACHOD. W wersji 5 języka Java udostępniono enum jako ulepszony typ wyliczeniowy, w którym wyeliminowano dotychczasowe problemy związane z definiowaniem typu wyliczeniowego w postaci listy. Wprowadzenie nowego typu spowodowało także zmiany w instrukcji switch, o których więcej powiemy w rozdziale 5., przy okazji rozważań na temat typów wyliczeniowych. „ Uwaga • W Javie w wersji 7 umożliwiono definiowanie instrukcji switch z wyrażeniami selektorów,

których wynikiem jest ciąg znaków. Wówczas wartością dla każdego przypadku jest literał w postaci ciągu znaków. Ten rodzaj instrukcji switch zostanie omówiony w następnym punkcie.

Pętle Czasami niektóre instrukcje obecne w kodzie trzeba wykonywać w sposób powtarzalny. Powtarzalne wykonywanie fragmentu kodu to tak zwana pętla. Java obsługuje trzy rodzaje pętli: for, while i do-while. Instrukcja for ma następującą składnię: for ([inicjalizacja]; [test]; [uaktualnienie]) instrukcja

Instrukcja składa się ze słowa zastrzeżonego for, nagłówka zamkniętego w nawiasach okrągłych oraz instrukcji przeznaczonej do wykonania. Nagłówek z kolei zawiera opcjonalną

81

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

sekcję inicjalizacyjną, opcjonalny test i opcjonalną sekcję uaktualnienia. Znak średnika jest obowiązkowy i musi on oddzielać pierwsze dwie sekcje od sekcji następnej. W sekcji inicjalizacji znajduje się lista deklaracji zmiennych lokalnych lub przypisań zmiennych oddzielonych od siebie znakiem przecinka. Za pomocą niektórych lub wszystkich tych zmiennych steruje się pętlą, dlatego są to tak zwane zmienne sterujące pętlą. Sekcja testu zawiera wyrażenie logiczne, na podstawie którego ustala się, jak długo pętla ma być wykonywana. Wykonanie pętli będzie trwać do momentu, gdy wyrażenie logiczne przybierze wartość true. Ostatnia sekcja, czyli sekcja uaktualnienia, zawiera listę wyrażeń oddzielonych od siebie przecinkami. Wyrażenia te zazwyczaj modyfikują zmienne sterujące pętlą. Instrukcja for doskonale nadaje się do iterowania (przechodzenia w pętli) przez tablicę. W każdej iteracji (pojedynczym wykonaniu pętli) można odczytać jeden z elementów tablicy, do czego służy wyrażenie tablica[indeks]. W wyrażeniu tym tablica to tablica, której element jest odczytywany, zaś indeks to lokalizacja odczytywanego elementu w tablicy, przy czym lokalizacja zaczyna się od zera. W kodzie na listingu 2.16 użyto instrukcji for do iterowania przez tablicę argumentów wiersza poleceń przekazaną do metody main(). Każdy argument jest wczytywany z tablicy, a w Javie w wersji 7 rozszerzono instrukcję switch o możliwość wykonywania poszczególnych przypadków zależnie od argumentu wiersza poleceń. Listing 2.16. Wykorzystanie pętli for i instrukcji switch z wyrażeniem selektora opartym na ciągu znaków do przetwarzania argumentów wiersza poleceń public static void main(String[] args) { for (int i = 0; i < args.length; i++) switch (args[i]) { case "-v": case "-V": System.out.println("wersja 1.0"); break; default : showUsage(); } }

W kodzie na listingu 2.16 instrukcja for zawiera sekcję inicjalizacyjną, w której zadeklarowano zmienną lokalną i, sekcję testu, w której bieżąca wartość zmiennej i jest porównywana z długością tablicy args (każda tablica zawiera pole length, które zwraca liczbę elementów w tej tablicy), aby upewnić się, że wartość zmiennej i jest mniejsza niż długość tablicy, oraz sekcję uaktualnienia, w której wartość zmiennej i jest zwiększana o 1. Pętla jest wykonywana tak długo, jak długo wartość zmiennej i jest mniejsza niż długość tablicy. W każdej iteracji (wykonaniu pętli) w wyrażeniu args[i] odczytywana jest jedna z wartości obecnych w tablicy. Wyrażenie to zwraca i-tą wartość tablicy (którą w naszym przykładzie jest obiekt klasy String). Pierwsza wartość w tablicy jest odczytywana wyrażeniem args[0]. Wyrażenie args[i] pełni rolę wyrażenia selektora dla instrukcji switch. Jeżeli zwrócony przez wyrażenie obiekt String będzie zawierać sekwencję znaków –V, zostanie wykonany drugi przypadek, w którym wywoływana jest metoda System.out.println() odpowiedzialna za wyświetlenie komunikatu o numerze wersji. Następująca po tym wywołaniu instrukcja break 82

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

zapobiega wykonaniu przypadku domyślnego, w którym wykonywana jest metoda showUsage(). Metoda showUsage() służy do wyświetlenia informacji o sposobie użycia programu, w przypadku gdy metoda main() zostanie wywołana z nieznanymi argumentami. Jeżeli obiekt String będzie zawierać sekwencję znaków –v, zostanie wykonana instrukcja dla drugiego przypadku, ponieważ w pierwszym przypadku odpowiadającym wystąpieniu sekwencji –v nie występuje instrukcja break. W konsekwencji zostanie więc wywołana metoda System.out.println(). Zaprezentowany przykład ilustruje zatem sytuację, w której zgrupowano przypadki po to, by wykonać wspólny dla nich blok kodu źródłowego. Instrukcja while ma następującą składnię: while (wyrażenie logiczne) instrukcja

Instrukcja while składa się ze słowa zastrzeżonego while, po którym następuje nagłówek z wyrażeniem logicznym zamkniętym w okrągłych nawiasach. Za nagłówkiem znajduje się instrukcja, która ma być wykonywana w sposób powtarzalny. Instrukcja while najpierw przetwarza wyrażenie logiczne. Jeżeli ma ono wartość true, wykonywana jest instrukcja, po czym ponownie obliczane jest wyrażenie logiczne. Jeżeli wciąż ma ono wartość true, ponownie wykonywana jest instrukcja. Cykl ten powtarzany jest raz za razem. Jednym z przypadków, w których pomocna okazuje się pętla while, jest sczytywanie znaku wpisywanego przez użytkownika. Załóżmy na przykład, że trzeba poprosić użytkownika o wpisanie konkretnej wielkiej litery lub odpowiadającej jej małej litery. Odpowiedni przykład znajduje się na listingu 2.17. Listing 2.17. Przykładowa instrukcja while, w której prosi się użytkownika o wpisanie określonego znaku int ch = 0; while (ch != 'C' && ch != 'c') { System.out.println("Naciśnij klawisz C lub c, aby kontynuować."); ch = System.in.read(); }

Najpierw w kodzie z listingu 2.17 inicjalizowana jest lokalna zmienna ch. Zmienną tę trzeba zainicjalizować, ponieważ w przeciwnym razie w momencie, gdy w wyrażeniu logicznym pętli while zostanie podjęta próba odczytania wartości zmiennej ch, kompilator zwróci błąd braku inicjalizacji zmiennej. W wyrażeniu użyto warunkowego operatora koniunkcji (&&), aby sprawdzić wartość zmiennej ch. Operator koniunkcji najpierw oblicza swój lewy operand, którym jest wyrażenie ch != 'C'. (Przed wykonaniem porównania operator != przekształci 'C' z 16-bitowej wartości typu char bez znaku w 32-bitowy typ int ze znakiem). Jeżeli zmienna ch nie zawiera litery C (a tak jest na tym etapie, ponieważ zmiennej ch przypisano wartość 0), wartością wyrażenia będzie true. Następnie operator && oblicza swój prawy operand, którym w powyższym przykładzie jest wyrażenie ch != 'c'. Ponieważ wyrażenie to także ma wartość true, operator warunkowej koniunkcji zwraca wartość true i w pętli while wykonywana jest instrukcja złożona.

83

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Instrukcja złożona najpierw wywołuje metodę System.out.println(), która wyświetla komunikat z informacją dla użytkownika, że należy nacisnąć klawisz C lub c. Następnie w instrukcji złożonej jest odczytywany (przy użyciu metody System.in.read(), o której była mowa w rozdziale 1.) znak wpisywany z klawiatury. Wartość całkowitoliczbowa wpisanego znaku jest przypisywana zmiennej ch. Po wykonaniu przypisania instrukcja złożona kończy swoje działanie, a pętla while po raz kolejny oblicza wartość wyrażenia logicznego. Załóżmy teraz, że zmienna ch zawiera wartość całkowitoliczbową odpowiadającą literze C. Operator warunkowej koniunkcji oblicza wyrażenie ch != 'C', którego wartością jest false. Ponieważ wyrażenie ma od razu wartość false, operator warunkowej koniunkcji dokonuje skrótu i nie oblicza już swego prawego operandu, lecz od razu zwraca wartość false. Wartość false zostaje następnie odczytana w pętli while, której wykonanie dobiega w ten sposób końca. Niech zmienna ch zawiera teraz wartość całkowitoliczbową odpowiadającą literze c. Operator warunkowej koniunkcji oblicza wyrażenie ch != 'C', które ma wartość true. Z tego względu operator oblicza wartość kolejnego wyrażenia ch != 'c', którego wartością jest false. Zatem również w tym przypadku wykonanie pętli while zostaje zakończone. „ Uwaga • Instrukcję for można zaimplementować w taki sposób, aby działała podobnie do instrukcji

while. Na przykład instrukcja: for (int i = 0; i < 10; i++) System.out.println(i);

odpowiada następującej instrukcji: int i = 0; while (i < 10) { System.out.println(i); i++; }

Instrukcja do-while ma następującą składnię: do instrukcja while(wyrażenie logiczne);

Powyższa instrukcja zawiera słowo zastrzeżone do, po którym następuje instrukcja przeznaczona do wykonywania w pętli. Następnie mamy słowo zastrzeżone while i nagłówek wyrażenia logicznego zamknięty w nawiasach okrągłych. Całą instrukcję kończy znak średnika. Instrukcja do-while najpierw wykonuje instrukcję przeznaczoną do wykonywania w pętli, a dopiero potem oblicza wartość wyrażenia logicznego. Jeżeli wyrażenie logiczne ma wartość true, instrukcja do-while ponownie wykonuje instrukcję przeznaczoną do wykonywania w pętli. Ponownie następuje obliczenie wartości wyrażenia logicznego. Jeżeli nadal ma ono wartość true, wykonywana jest instrukcja i ponawiany jest ten sam wzorzec wykonywania pętli. 84

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

Na listingu 2.18 przedstawiono sposób, w jaki można prosić użytkownika o wpisanie określonej małej lub wielkiej litery. Tym razem jednak w przykładzie wykorzystano pętlę do-while. Listing 2.18. Wykorzystanie pętli do-while, aby wymóc na użytkowniku wpisanie określonego znaku int ch; do { System.out.println("Naciśnij klawisz C lub c, aby kontynuować."); ch = System.in.read(); } while (ch != 'C' && ch != 'c');

Kod źródłowy widoczny na listingu 2.18 jest podobny do kodu z listingu 2.17. Tym razem jednak instrukcja złożona jest wykonywana jeszcze przed obliczeniem wartości wyrażenia logicznego. W efekcie nie trzeba już inicjalizować zmiennej ch, ponieważ przypisujemy jej wartość zwracaną przez metodę System.in.read() jeszcze przed obliczeniem wyrażenia logicznego. Czasami przydaje się możliwość wykonywania w pętli instrukcji pustej. Wówczas całe działanie pętli sprowadza się do czynności wykonywanych w nagłówku instrukcji. Odpowiedni przykład znajduje się na listingu 2.19. Listing 2.19. Wczytywanie i wyświetlanie wierszy tekstu for (String line; (line = readLine()) != null; System.out.println(line));

Na listingu 2.19 użyto pętli for, aby zaprezentować programistyczny idiom przeznaczony do kopiowania wierszy tekstu wczytywanych z jakiegoś wejścia (w przykładzie wejściem tym jest fikcyjna metoda readLine()) do jakiegoś obiektu docelowego, którym w przykładzie jest metoda System.out.println(). Kopiowanie wykonywane jest tak długo, aż metoda readLine() zwróci wartość null. Warto zwrócić uwagę na znak średnika na końcu wiersza, który oznacza instrukcję pustą. „ Ostrzeżenie • Instrukcji pustej należy używać z rozwagą, ponieważ może być ona źródłem trudnych do

zidentyfikowania błędów. Na przykład zadaniem poniższego fragmentu kodu jest wyświetlanie słowa „Witaj” w dziesięciu wierszach. Tak naprawdę jednak słowo zostanie wyświetlone tylko jeden raz, a dziesięć razy zostanie wykonana instrukcja pusta: for (int i = 0; i < 10; i++); // znak ; reprezentuje tutaj instrukcję pustą System.out.println("Witaj");

Przerywanie i kontynuowanie Co mają wspólnego instrukcje for(;;);, while(true); i do;while(true);? Otóż każda z przytoczonych instrukcji pętli stanowi skrajny przykład pętli nieskończonej (czyli pętli, której wykonanie nigdy się nie kończy). Pętli nieskończonych powinno się unikać, ponieważ fakt, że ich wykonanie nigdy się nie kończy, prowadzi do zawieszenia się aplikacji, a z perspektywy użytkownika taki objaw jest wysoce niepożądany. 85

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

„ Ostrzeżenie • Przyczyną wystąpienia pętli nieskończonej może być także wyrażenie logiczne w nagłówku

pętli, w którym wartość o zmiennej precyzji porównuje się z wartością niezerową przy użyciu operatora równości lub nierówności. Wynika to z faktu, że wiele wartości o zmiennej precyzji charakteryzuje się nieprecyzyjną reprezentacją wewnętrzną. Na przykład poniższy przykładowy fragment kodu źródłowego będzie wykonywany bez końca, ponieważ wartość 0.1 nie ma dokładnej wewnętrznej reprezentacji: for (double d = 0.0; d != 1.0; d += 0.1) System.out.println(d);

Czasami jednak wygodnie jest zaimplementować pętlę w taki sposób, by miała ona charakter pętli nieskończonej, z wykorzystaniem jednego ze wspomnianych wcześniej idiomów programistycznych. Można na przykład użyć pętli while(true), aby prosić użytkownika o wpisanie określonego znaku tak długo, aż znak ten rzeczywiście zostanie przez niego wpisany. Gdy użytkownik naciśnie wymagany klawisz, wykonanie pętli należy zakończyć. Do tego celu służy dostępna w języku Java instrukcja przerwania break. Instrukcja break przenosi wykonanie do pierwszej instrukcji następującej po instrukcji switch (o której była mowa wcześniej) lub po instrukcji pętli. W obydwu przypadkach instrukcja przerwania składa się ze słowa zastrzeżonego break i znaku średnika. Na listingu 2.20 instrukcja break została użyta wraz z instrukcją if po to, by zakończyć wykonywanie nieskończonej pętli while(true) w momencie wpisania przez użytkownika litery C lub c. Listing 2.20. Przerwanie wykonywania pętli nieskończonej int ch; while (true) { System.out.println("Naciśnij klawisz C lub c, aby kontynuować."); ch = System.in.read(); if (ch == 'C' || ch == 'c') break; }

Instrukcja przerwania przydaje się także w pętlach skończonych. Rozważmy na przykład scenariusz, w którym w tablicy szuka się konkretnej wartości, a w momencie jej znalezienia należy wyjść z pętli. Odpowiedni przykład ilustrujący ten przypadek przedstawiono na listingu 2.21. Listing 2.21. Przedwczesne przerwanie wykonywania pętli for int[] employeeIDs = { 123, 854, 567, 912, 224 }; int employeeSearchID = 912; boolean found = false; for (int i = 0; i < employeeIDs.length; i++) if (employeeSearchID == employeeIDs[i]) { found = true; break; } System.out.println((found) ? "pracownik o identyfikatorze " + employeeSearchID + " występuje" : "brak pracownika o identyfikatorze " + employeeSearchID);

86

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

W kodzie na listingu 2.21 użyto instrukcji for i if, aby wyszukać w tablicy identyfikatorów pracowników konkretny identyfikator. Jeżeli identyfikator ten jest obecny w tablicy, instrukcja złożona w instrukcji warunkowej if przypisze zmiennej found wartość true. Ponieważ nie ma wówczas sensu, aby kontynuować przeszukiwanie, w kolejnym kroku wykonywana jest instrukcja break, aby wyjść z pętli. Instrukcja kontynuacji powoduje, że pozostała część bieżącej iteracji pętli zostaje pominięta, ponownie obliczane jest wyrażenie logiczne z nagłówka i rozpoczynana jest kolejna iteracja (jeżeli wartością wyrażenia logicznego jest true) bądź też pętla zostaje zakończona (jeżeli wartością wyrażenia logicznego jest false). Instrukcja kontynuacji składa się ze słowa zastrzeżonego continue i znaku średnika. Przyjrzyjmy się pętli while, w której ze źródła danych wejściowych wczytywane są kolejne wiersze tekstu, a wiersze niepuste podlegają odpowiedniemu przetworzeniu. Wiersze puste nie powinny być w żaden sposób przetwarzane, dlatego gdy instrukcja while zidentyfikuje wiersz pusty, realizacja bieżącej iteracji zostanie zakończona. Ilustruje to przykład z listingu 2.22. Listing 2.22. Rezygnacja z wykonywania pozostałej części iteracji String line; while ((line = readLine()) != null) { if (isBlank(line)) continue; processLine(line); }

Na listingu 2.22 znajduje się wywołanie fikcyjnej metody isBlank(), która sprawdza, czy wczytany wiersz jest pusty. Jeżeli metoda isBlank() zwróci wartość true, w instrukcji warunkowej zostanie wykonana instrukcja continue. W ten sposób pozostała część bieżącej iteracji zostanie pominięta i nastąpi wczytanie kolejnego wiersza. Warto uważnie przeanalizować kod z listingu 2.22 — można wtedy zauważyć, że instrukcja kontynuacji tak naprawdę nie jest wcale potrzebna. Kod z listingu można zrefaktoryzować (przepisać na nowo w taki sposób, aby zwiększyć jego czytelność, spójność i możliwości ponownego wykorzystania) w sposób widoczny na listingu 2.23. Listing 2.23. Zrefaktoryzowany kod instrukcji warunkowej String line; while ((line = readLine()) != null) { if (!isBlank(line)) processLine(line); }

W wyniku refaktoryzacji dokonanej na listingu 2.23 w wyrażeniu logicznym instrukcji warunkowej użyto operatora logicznego dopełnienia (!). Dzięki temu za każdym razem, gdy metoda isBlank() zwróci wartość false, operator logicznego dopełnienia zmieni tę wartość na true i w instrukcji if zostanie wykonana metoda processLine().

87

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

W odróżnieniu od instrukcji break, instrukcja continue wydaje się nie być niezbędną częścią języka. Jak właśnie pokazano, instrukcję kontynuacji można zastąpić przez odwrócenie wartości wyrażenia logicznego. W przypadku zbyt częstego używania instrukcji kontynuacji należy poświęcić chwilę na zrefaktoryzowanie kodu źródłowego. Java obsługuje wersje instrukcji break i continue, w których występują one z etykietami. Uzyskuje się wtedy bardziej restrykcyjną formę instrukcji goto, która służy do przenoszenia potoku wykonania kodu do etykiety. (Wielu programistów wzbrania się przed używaniem instrukcji goto, ponieważ jej stosowanie często prowadzi do powstania nieczytelnego oraz trudnego w utrzymaniu kodu). „ Uwaga • W języku Java goto jest słowem zastrzeżonym, przez co nie może ono być nazwą jakiejkolwiek en-

cji kodu źródłowego. Jednak już słowo GOTO nie jest zastrzeżone, dlatego wydaje się, że zastrzeżenie słowa goto nie ma większego sensu.

Instrukcja przerwania z etykietą składa się ze słowa zastrzeżonego break, po którym następuje identyfikator odpowiadający istniejącej w innym miejscu kodu etykiecie (etykieta to identyfikator zakończony znakiem średnika). Co więcej, etykieta musi bezpośrednio poprzedzać instrukcję pętli. Instrukcja przerwania z etykietą przydaje się do przerywania pętli zagnieżdżonych (czyli pętli, które znajdują się wewnątrz pętli). Na przykład na listingu 2.24 użyto instrukcji break z etykietą, aby przenieść wykonanie do pierwszej instrukcji po zewnętrznej pętli. Listing 2.24. Przerywanie wykonywania zagnieżdżonych pętli outer: for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) if (i == 1 && j == 1) break outer; else System.out.println("i=" + i + ", j=" + j); System.out.println("Zakończono wykonywanie obydwóch pętli.");

Gdy zmienne i oraz j mają wartość 1, wykonywana jest instrukcja break outer;, aby zakończyć wykonywanie obydwóch pętli. Instrukcja przenosi wykonanie do pierwszej instrukcji, która występuje po pętli zewnętrznej. W przykładzie jest nią wywołanie metody System.out.println("Zakończono wykonywanie obydwóch pętli.");. Kod z listingu 2.23 zwraca następujący wynik: i=0, j=0 i=0, j=1 i=0, j=2 i=1, j=0 Zakończono wykonywanie obydwóch pętli.

Instrukcja kontynuacji z etykietą składa się ze słowa zastrzeżonego continue oraz identyfikatora, który musi odpowiadać etykiecie występującej w innym miejscu kodu źródłowego (etykieta to identyfikator zakończony znakiem średnika). Co więcej, etykieta musi bezpośrednio poprzedzać instrukcję pętli. 88

ROZDZIAŁ 2. „ PODSTAWY JĘZYKA JAVA

Instrukcja kontynuacji z etykietą przydaje się, gdy trzeba zakończyć bieżącą i przyszłe iteracje pętli zagnieżdżonych albo rozpocząć nową iterację pętli z etykietą. Na przykład na listingu 2.25 użyto instrukcji kontynuacji z etykietą, aby zakończyć wykonywanie iteracji pętli wewnętrznej. Listing 2.25. Kontynuacja wykonania pętli zewnętrznej outer: for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) if (i == 1 && j == 1) continue outer; else System.out.println("i=" + i + ", j=" + j); System.out.println("Zakończono wykonywanie obydwóch pętli.");

Gdy zmienne i oraz j mają wartość 1, wykonywana jest instrukcja continue outer;, która kończy wykonywanie wewnętrznej pętli for i kontynuuje wykonywanie zewnętrznej pętli for dla następnej wartości zmiennej i. Wykonywanie obydwóch pętli jest kontynuowane aż do ich naturalnego zakończenia. Kod z listingu 2.25 zwraca następujący wynik: i=0, j=0 i=0, j=1 i=0, j=2 i=1, j=0 i=2, j=0 i=2, j=1 i=2, j=2 Zakończono wykonywanie obydwóch pętli.

Wyjście z metody Metoda void jest wykonywana od pierwszej do ostatniej instrukcji. Jednak instrukcja return języka Java pozwala na wyjście z metody, zanim zostanie wykonana jej ostatnia instrukcja. Jak widać na listingu 2.26, instrukcja składa się ze słowa zastrzeżonego return i znaku średnika. Listing 2.26. Wyjście z metody class Employee { int salary; void setSalary(int empSalary) { if (empSalary < 0) { System.out.println("pensja nie może być ujemna"); return; } salary = empSalary; } }

89

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

W obecnej na listingu 2.26 metodzie setSalary() jest wykonywana instrukcja if, która ma identyfikować próby przypisania polu field wartości ujemnej. Gdy próba taka zostanie zidentyfikowana, kod zwraca komunikat błędu i przedwcześnie wychodzi z metody, aby uniknąć przypisania zmiennej wartości negatywnej. Taki sposób wyjścia z metody jest niedozwolony, jeżeli metoda zwraca jakąś wartość. W takich metodach używa się wersji instrukcji return, która pozwala zwrócić wartość (a jej typ musi odpowiadać typowi wartości zwracanej przez metodę). Odpowiedni przykład znajduje się na listingu 2.27. Listing 2.27. Zwracanie wartości przez metodę int deposit(int amount) { if (amount list; oznacza, że elementy listy nie są znane. Wię-

cej informacji na temat tego parametru typu znajduje się w punkcie „Potrzeba użycia symboli wieloznacznych”. Typ ogólny identyfikuje również typ surowy, czyli taki typ ogólny, który nie posiada parametrów typu. Na przykład typem surowym dla List jest List. Typy surowe nie są typami ogólnymi i mogą przechowywać dowolne obiekty klasy Object. „ Uwaga • Java pozwala na używanie łącznie typów surowych i typów ogólnych, aby zapewnić prawidłowe

działanie starszych aplikacji, napisanych przed udostępnieniem mechanizmów ogólnych. Jednak za każdym razem, gdy kompilator napotyka w kodzie źródłowym typ surowy, zwraca komunikat z ostrzeżeniem.

Deklarowanie i używanie własnych typów ogólnych Własne typy ogólne deklaruje się bez większego trudu. Oprócz specyfikowania listy parametrów typów formalnych, własny typ ogólny specyfikuje swoje parametry typów w drodze implementacji. Na przykład w kodzie na listingu 5.24 zadeklarowano typ ogólny Queue. Listing 5.24. Deklaracja i użycie typu ogólnego Queue public class Queue { private E[] elements; private int head, tail; @SuppressWarnings("unchecked") public Queue(int size) { elements = (E[]) new Object[size]; head = 0; tail = 0; } public void insert(E element) { if (isFull()) // insert() powinna rzucić wyjątek, gdy kolejka jest pełna. return; // Dla uproszczenia nie zaimplementowano metody insert(). elements[tail] = element; tail = (tail+1)%elements.length; } public E remove()

222

ROZDZIAŁ 5. „ ZAAWANSOWANE MECHANIZMY JĘZYKA — CZĘŚĆ II { if (isEmpty()) return null; E element = elements[head]; head = (head+1)%elements.length; return element; } public boolean isEmpty() { return head == tail; } public boolean isFull() { return (tail+1)%elements.length == head; } public static void main(String[] args) { Queue queue = new Queue(5); System.out.println(queue.isEmpty()); queue.insert("A"); queue.insert("B"); queue.insert("C"); queue.insert("D"); queue.insert("E"); System.out.println(queue.isFull()); System.out.println(queue.remove()); queue.insert("F"); while (!queue.isEmpty()) System.out.println(queue.remove()); System.out.println(queue.isEmpty()); System.out.println(queue.isFull()); } }

Klasa Queue implementuje kolejkę (ang. queue), czyli strukturę danych, która przechowuje elementy w kolejności „pierwsze przyszło – pierwsze wyszło”. Dany element jest wstawiany na ogonie kolejki, a usuwany u jej głowy. Kolejka jest pusta wówczas, gdy głowa jest tożsama z ogonem, i pełna, gdy ogon jest o jeden mniejszy niż głowa. Warto zwrócić uwagę, że w kodzie źródłowym pojawia się parametr typu E klasy Queue. Na przykład E występuje w deklaracji tablicy elements, gdzie wskazuje typ elementów tablicy. Ponadto E jest wskazywany jako typ parametru metody insert() oraz jako typ wartości zwracanej przez metodę remove(). E występuje także w instrukcji elements = (E[]) new Object[size];. (Nieco później wyjaśnimy, dlaczego zastosowano takie właśnie wyrażenie zamiast bardziej zwartej konstrukcji elements = new E[size];). Rzutowanie E[] skutkuje ostrzeżeniem ze strony kompilatora, ponieważ ma ono charakter niekontrolowany. Kompilator wskazuje w ten sposób, że rzutowanie w dół od Object[] do E[] może doprowadzić do pogwałcenia zasad bezpieczeństwa typologicznego, ponieważ w tablicy Object[]mogą być przechowywane dowolne obiekty. Jednak w naszym przykładzie ostrzeżenie kompilatora nie jest uzasadnione. Nie ma bowiem możliwości, by w tablicy E[] pojawił się obiekt inny niż E. Zatem ponieważ ostrzeżenie jest w tym kontekście bezzasadne, zostało ono wytłumione — w tym celu konstruktor poprzedzono adnotacją @SuppressWarnings("unchecked"). 223

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

„ Ostrzeżenie • Gdy rozważa się wytłumienie ostrzeżeń "unchecked", należy zachować szczególną

ostrożność. Najpierw trzeba zapewnić, że wyjątek ClassCastException nigdy nie wystąpi — dopiero potem można zdecydować się na wytłumienie ostrzeżenia.

Gdy przykładowa aplikacja zostanie uruchomiona, wynik jej działania będzie następujący: true true A B C D F true false

Ograniczenia parametrów typów Parametr typu E listy List i parametry typów K i V mapy Map to przykłady nieograniczonych parametrów typów. Do nieograniczonych parametrów typu można przekazać dowolny argument typu rzeczywistego. Czasami jednak konieczne jest ograniczenie argumentów typu rzeczywistego, które można przekazać do parametru typu. Na przykład można zadeklarować klasę, której instancje powinny przechowywać wyłącznie instancje klas potomnych po klasie Shape. Aby ograniczyć zakres argumentów typu rzeczywistego, należy wskazać górne ograniczenie, czyli typ, który stanowi górne ograniczenie typów dozwolonych jako argumenty typu rzeczywistego. Górne ograniczenie określa się przy użyciu słowa zastrzeżonego extends, po którym występuje nazwa typu. Na przykład deklaracja ShapesList wskazuje Shape jako górne ograniczenie. Można więc wyspecyfikować ShapesList, ShapesList, a nawet ShapesList, lecz ShapesList nie jest już prawidłową deklaracją, ponieważ String nie jest klasą potomną po Shape. Parametrowi typu można przypisać więcej niż jedno górne ograniczenie — pierwszym górnym ograniczeniem musi być wówczas klasa lub interfejs, a każdym kolejnym górnym ograniczeniem musi być interfejs. Nazwy ograniczeń należy oddzielać od siebie znakiem ampersandu (&). Odpowiedni przykład znajduje się na listingu 5.25. Listing 5.25. Przypisanie parametrowi typu więcej niż jednego górnego ograniczenia abstract class Shape { } class Circle extends Shape implements Comparable { private double x, y, radius; Circle(double x, double y, double radius) { this.x = x; this.y = y; this.radius = radius;

224

ROZDZIAŁ 5. „ ZAAWANSOWANE MECHANIZMY JĘZYKA — CZĘŚĆ II } @Override public int compareTo(Circle circle) { if (radius < circle.radius) return -1; else if (radius > circle.radius) return 1; else return 0; } @Override public String toString() { return "(" + x + ", " + y + ", " + radius + ")"; } } class SortedShapesList { @SuppressWarnings("unchecked") private S[] shapes = (S[]) new Shape[2]; private int index = 0; void add(S shape) { shapes[index++] = shape; if (index < 2) return; System.out.println("Przed sortowaniem: " + this); sort(); System.out.println("Po sortowaniu: " + this); } private void sort() { if (index == 1) return; if (shapes[0].compareTo(shapes[1]) > 0) { S shape = (S) shapes[0]; shapes[0] = shapes[1]; shapes[1] = shape; } } @Override public String toString() { return shapes[0].toString() + " " + shapes[1].toString(); } } public class SortedShapesListDemo { public static void main(String[] args) { SortedShapesList ssl = new SortedShapesList(); ssl.add(new Circle(100, 200, 300)); ssl.add(new Circle(10, 20, 30)); } }

225

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Klasa Circle z listingu 5.25 rozszerza klasę Shape i implementuje interfejs java.lang. ´Comparable, za pomocą którego definiowany jest sposób naturalnego porządkowania obiektów klasy Circle. Metoda compareTo() implementuje to porządkowanie w ten sposób, że zwraca wartość wskazującą określony porządek: • Wartość ujemna jest zwracana wówczas, gdy obiekt bieżący powinien poprzedzać obiekt przekazany do metody compareTo(). • Zero jest zwracane wówczas, gdy obiekt bieżący i przekazany argument są identyczne. • Wartość dodatnia jest zwracana wówczas, gdy obiekt bieżący powinien się znajdować

za obiektem przekazanym jako argument. Metoda pokrywająca compareTo() klasy Circle porównuje dwa obiekty klasy Circle na podstawie ich promieni. Metoda ta ustawia instancję klasy Circle z mniejszym promieniem przed instancją klasy Circle z większym promieniem. W klasie SortedShapesList jako listę parametrów wskazano . Argumentem typu rzeczywistego przekazanym do parametru S musi być klasa potomna po Shape, która jednocześnie musi implementować interfejs Comparable. Klasa Circle spełnia obydwa te kryteria: jest klasą potomną po Shape i implementuje interfejs Comparable. W efekcie, gdy w metodzie main() kompilator napotka instrukcję Sorted ´ShapesList ssl = new SortedShapesList();, nie zgłosi żadnego błędu. Wyspecyfikowanie górnego ograniczenia zapewnia także dodatkową statyczną kontrolę typu, która stanowi gwarancję, że typ parametryzowany będzie spełniał to ograniczenie. Dzięki tej gwarancji metody typu wskazanego jako górne ograniczenie można wywoływać bez żadnego zagrożenia. Na przykład metoda sort() może wywoływać metodę compareTo() interfejsu Comparable. Po uruchomieniu aplikacji zwróci ona dane wynikowe widoczne poniżej. Na podstawie tych danych można stwierdzić, że obiekty Circle zostały posortowane rosnąco względem długości ich promieni: Przed sortowaniem: (100.0, 200.0, 300.0) (10.0, 20.0, 30.0) Po sortowaniu: (10.0, 20.0, 30.0) (100.0, 200.0, 300.0)

Argumenty typu rzeczywistego można także ograniczać przez wskazanie ograniczenia dolnego, czyli wskazanie typu, który będzie wyznaczał dolne ograniczenie typów dozwolonych do przekazywania jako argumenty typu rzeczywistego. Ograniczenie dolne wskazuje się za pomocą symbolu wieloznacznego, zastrzeżonego słowa super oraz nazwy typu. Ponieważ ograniczenia dolne są używane wyłącznie razem z argumentem typu w postaci symbolu wieloznacznego, więcej na temat ograniczeń dolnych będziemy mogli powiedzieć w punkcie poświęconym potrzebie używania symboli wieloznacznych.

Zasięg parametrów typu Zasięgiem (widocznością) parametru typu jest typ ogólny, który zawiera listę parametrów typów formalnych, a parametr typu jest składową tej listy. Na przykład zasięgiem S z Sorted ´ShapesList są wszystkie typy z listy parametrów typów formalnych oraz SortedShapesList. 226

ROZDZIAŁ 5. „ ZAAWANSOWANE MECHANIZMY JĘZYKA — CZĘŚĆ II

„ Uwaga • Ograniczenie parametru typu, które zawiera parametr typu, to tak zwane rekurencyjne ogranicze-

nie typu (ang. recursive type bound). Przykładem rekurencyjnego ograniczenia typu jest Comparable w . Rekurencyjne ograniczenia typu spotyka się rzadko i zwykle w połączeniu z interfejsem Comparable, przy użyciu którego definiuje się sposób naturalnego porządkowania dla typu.

Parametr typu można zamaskować; w tym celu na liście parametrów typów formalnych typu zagnieżdżonego wystarczy umieścić parametr typu o takiej samej nazwie. Na listingu 5.26 zamaskowano parametr typu T klasy zawierającej. Listing 5.26. Maskowanie zmiennej typu class EnclosingClass { static class EnclosedClass { } }

Parametr typu T klasy zawierającej EnclosingClass został zamaskowany przez parametr typu T klasy EnclosedClass. Parametr ten wyznacza górne ograniczenie, według którego do klasy EnclosedClass można przekazywać wyłącznie te typy, które implementują interfejs Comparable. Jeżeli maskowanie jest niepożądane, najlepiej jest nadać parametrowi inną nazwę. Można na przykład użyć deklaracji EnclosedClass. Wprawdzie w deklaracji tej U nie jest tak opisową nazwą jak T, jednak wybór tej nazwy jest uzasadniony okolicznościami.

Potrzeba użycia symboli wieloznacznych W rozdziale 3. była mowa o tym, że typ podrzędny jest rodzajem typu nadrzędnego. Na przykład Circle, czyli koło, jest rodzajem figury Shape, zaś ciąg znaków String jest rodzajem ogólnego obiektu Object. Taką wielopostaciową naturą charakteryzują się też typy parametryzowane z takimi samymi parametrami typów (na przykład List jest rodzajem Collection). Jednak taka wielopostaciowa natura nie dotyczy tych typów parametryzowanych, które różnią się między sobą jedynie jednym parametrem typu będącego typem podrzędnym innego parametru typu. Na przykład nie można powiedzieć, że List jest rodzajem List. Na listingu 5.27 pokazano, dlaczego typy parametryzowane różniące się jedynie parametrami typów nie posiadają cech wielopostaciowości. Listing 5.27. Dowód na to, że typy parametryzowane różniące się tylko parametrami typów nie cechują się wielopostaciowością public static void main(String[] args) { List ls = new ArrayList(); List lo = ls;

227

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID lo.add(new Employee()); String s = ls.get(0); }

Gdy kod z listingu 5.27 zostanie skompilowany, wówczas w fazie wykonania zostanie rzucony wyjątek ClassCastException. Po utworzeniu instancji listy List obiektów String i zrzutowaniu w górę tej instancji do typu List obiektów Object metoda main() dodaje do listy obiektów Object nowy obiekt klasy Employee. Następnie obiekt klasy Employee jest zwracany przez metodę get() i przy użyciu zmiennej będącej odwołaniem do listy List obiektów klasy String. Wyjątek ClassCastException zostaje rzucony, dlatego że wykonywane jest niejawne rzutowanie do klasy String, a przecież obiekt klasy Employee nie jest obiektem klasy String. „ Uwaga • Wprawdzie List nie można zrzutować do góry do List, lecz tę samą listę

List można zrzutować do góry do typu surowego List, aby zapewnić współdziałanie ze starszym kodem źródłowym.

Opisany przykład można uogólnić do następującej reguły: dla danego typu podrzędnego x typu y i danego G jako deklaracji typu surowego G nie jest typem podrzędnym dla G.

Na listingu 5.28 widać, jak łatwo jest tę regułę naruszyć. Listing 5.28. Próba wyświetlenia listy ciągów znaków public static void main(String[] args) { List ls = new ArrayList(); ls.add("pierwszy"); ls.add("drugi"); ls.add("trzeci"); outputList(ls); } static void outputList(List list) { for (int i = 0; i < list.size(); i++) System.out.println(list.get(i)); }

Kod przedstawiony na listingu 5.28 nie zostanie skompilowany, ponieważ założono w nim, że lista List ciągów znaków String jest jednocześnie listą obiektów klasy Object, co — jak wspomniano przed chwilą — nie jest prawdą, ponieważ narusza to regułę bezpieczeństwa typologicznego. Metoda outputList() może wyświetlić wyłącznie listę obiektów, co czyni ją bezużyteczną. Obejściem wspomnianego problemu, które będzie bezpieczne pod względem typologicznym, jest użycie argumentu typu w postaci symbolu wieloznacznego (?), bowiem dzięki temu akceptowany będzie dowolny argument typu rzeczywistego. Na listingu 5.28 fragment List wystarczy zamienić na List, aby kod został skompilowany prawidłowo. Jednak do listy List nie można dodawać elementów, ponieważ w ten sposób zostanie naruszone bezpieczeństwo typologiczne. Jako przykład na listingu 5.29 zaprezentowano metodę copyList(), która próbuje skopiować zawartość listy List do innej listy List.

228

ROZDZIAŁ 5. „ ZAAWANSOWANE MECHANIZMY JĘZYKA — CZĘŚĆ II

Listing 5.29. Próba skopiowania listy List do innej listy List public static void main(String[] args) { List ls1 = new ArrayList(); ls1.add("pierwszy"); ls1.add("drugi"); ls1.add("trzeci"); List ls2 = new ArrayList(); copyList(ls1, ls2); } static void copyList(List list1, List list2) { for (int i = 0; i < list1.size(); i++) list2.add(list1.get(i)); }

Kod z listingu 5.29 nie zostanie skompilowany, ponieważ w momencie napotkania instrukcji list2.add(list1.get(i)); kompilator zgłosi następujący komunikat błędu: x.java:13: cannot find symbol symbol : method add(java.lang.Object) location: interface java.util.List list2.add(list1.get(i)); ^ 1 error

Na podstawie komunikatu błędu można stwierdzić, że choć elementy listy list1 (które mogą być dowolnego typu) można przypisywać obiektom klasy Object, to typ elementów listy list2 jest nieznany. Jeżeli jest to jakikolwiek typ inny niż Object (na przykład typem tym jest String), oznacza to naruszenie bezpieczeństwa typologicznego. Jednym z możliwych rozwiązań tego problemu jest użycie deklaracji static void copyList(List list1, List list2). Drugie rozwiązanie opisanego problemu wymaga, by pierwszy parametr typu metody copyList() został zadeklarowany jako ? extends String, zaś drugi parametr typu tej metody jako ? super String. Na listingu 5.30 przedstawiono tę samą metodę copyList() z już zdefiniowanymi ograniczeniami górnym i dolnym. Listing 5.30. Skopiowanie jednej listy do drugiej jest możliwe po użyciu symboli wieloznacznych jako ograniczeń public static void main(String[] args) { List ls1 = new ArrayList(); ls1.add("pierwszy"); ls1.add("drugi"); ls1.add("trzeci"); List ls2 = new ArrayList(); copyList(ls1, ls2); } static void copyList(List l) { for (int i = 0; i < l.size(); i++) System.out.println (l.get(i)); System.out.println(); } }

Parametry typów metody ogólnej wynikają z kontekstu, w którym metodę wywołano. Na przykład kompilator sam wywnioskuje, że wywołanie w postaci copyList(ls, lsCopy); ma na celu skopiowanie listy List obiektów String do innej listy List obiektów String. Analogicznie — wywołanie copyList(lc, lcCopy); oznacza dla kompilatora, że listę List obiektów Circle należy skopiować do innej listy List obiektów klasy Circle. Gdy uruchomi się aplikację, zwróci ona następujące wyniki: A B C A B C (10.0, 20.0, 30.0) (5.0, 4.0, 16.0) (10.0, 20.0, 30.0) (5.0, 4.0, 16.0)

Typy wyliczeniowe Typ wyliczeniowy (ang. enumerated type) to typ, którego dozwolone wartości są wskazywane w postaci nazwanej sekwencji powiązanych ze sobą stałych. Przykładami typów wyliczeniowych mogą być miesiące kalendarza, monety w określonej walucie i dni tygodnia. Programiści tworzący programy w języku Java tradycyjnie reprezentowali typy wyliczeniowe za pomocą zbiorów nazwanych stałych całkowitoliczbowych. Jednak ponieważ z biegiem czasu taki sposób reprezentacji typów wyliczeniowych okazywał się coraz bardziej problematyczny, w wersji 5 języka udostępniono typy wyliczeniowe w postaci znanej współcześnie. W tym punkcie przedstawimy typy wyliczeniowe. Najpierw powiemy, na czym polegają problemy związane z tradycyjnymi typami wyliczeniowymi, po czym opiszemy właściwe typy wyliczeniowe. W dalszej kolejności przedstawimy klasę Enum, która stanowi punkt wyjścia dla typów wyliczeniowych.

233

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Problem z tradycyjnymi typami wyliczeniowymi Na listingu 5.34 zadeklarowano typ wyliczeniowy Coin, którego zbiór stałych identyfikuje różne rodzaje monet polskiej waluty. Listing 5.34. Typ wyliczeniowy, który opisuje monety public class Coin { public final static public final static public final static public final static }

int int int int

JEDNOGROSZOWKA = 0; PIECIOGROSZOWKA = 1; DWUDZIESTOGROSZOWKA = 2; PIECDZIESIECIOGROSZOWKA = 3;

Na listingu 5.35 zadeklarowano typ wyliczeniowy, którego zbiór stałych opisuje dni tygodnia. Listing 5.35. Typ wyliczeniowy, który opisuje dni tygodnia public class Weekday { public final static public final static public final static public final static public final static public final static public final static }

int int int int int int int

PONIEDZIALEK = 0; WTOREK = 1; SRODA = 2; CZWARTEK = 3; PIATEK = 4; SOBOTA = 5; NIEDZIELA = 6;

Podejście do reprezentacji typów wyliczeniowych zaprezentowane na listingach 5.34 i 5.35 jest problematyczne, a największym problemem jest brak bezpieczeństwa typologicznego w fazie kompilacji. Z tego powodu można na przykład przekazać monetę do metody, która wymaga wskazania dnia tygodnia — kompilator w ogóle nie zgłosi żadnego ostrzeżenia ani błędu. Ponadto w przypadku zastosowania takiej reprezentacji monety można porównywać z dniami tygodnia, na przykład Coin.PIECIOGROSZOWKA == Weekday.PONIEDZIALEK, a nawet zdefiniować wyrażenie zupełnie pozbawione sensu, na przykład Coin.DWUDZIESTOGROSZOWKA+ ´Weekday.PIATEK-1/Coin.PIECDZIESIECIOGROSZOWKA. Również w tym przypadku kompilator nie zgłosi żadnego zastrzeżenia, ponieważ wszędzie będzie widział wartości int. Aplikacje, które bazują na typach wyliczeniowych zdefiniowanych w tradycyjny sposób, są podatne na różnego rodzaju błędy. Ponieważ stałe typu są kompilowane do plików klas aplikacji, zmiana wartości int danej stałej powoduje, że konieczne jest ponowne skompilowanie aplikacji. Inaczej bowiem ryzykuje się, że aplikacja będzie działać błędnie. Kolejny problem z tradycyjnymi typami wyliczeniowymi polega na tym, że stałych int nie można przekształcić w znaczące opisowe ciągi znaków. Na przykład gdy debuguje się aplikację, która działa nieprawidłowo, skąd można wiedzieć, co oznacza 4? Zdecydowanie łatwiej zrozumieć działanie aplikacji, gdy widzi się stałą PIATEK, a nie 4.

234

ROZDZIAŁ 5. „ ZAAWANSOWANE MECHANIZMY JĘZYKA — CZĘŚĆ II

„ Uwaga • Opisane problemy można by obejść, stosując stałe typu String. Na przykład można użyć de-

klaracji public final static String CZWARTEK = "CZWARTEK";. Wprawdzie tak zdefiniowane wartości stałych na pewno niosą o wiele więcej znaczenia, jednak z powodu stałych w postaci ciągu znaków cierpi wydajność aplikacji. Nie można bowiem za pomocą operatora == wydajnie porównywać dowolnych ciągów znaków (o czym przekonamy się w rozdziale 7.). Kolejnym problemem wynikającym z używania stałych w postaci ciągów znaków String jest konieczność jawnego zdefiniowania ich w kodzie źródłowym ("CZWARTEK") zamiast definiowania nazwy stałej (CZWARTEK) — tak zdefiniowaną wartość stałej zdecydowanie trudniej jest zmienić w późniejszym czasie. Ponadto popełnienie błędu literowego w stałej ("CZFARTEK") spowoduje błędy w fazie wykonania, choć nie wpłynie na prawidłowość kompilacji.

Enum — alternatywa dla tradycyjnego typu wyliczeniowego W wersji 5 języka Java wprowadzono lepszą alternatywę dla tradycyjnych typów wyliczeniowych. Jest to enum — typ wyliczeniowy definiowany za pomocą słowa zastrzeżonego enum. Na listingu 5.36 zaprezentowano typy wyliczeniowe z listingów 5.34 i 5.35, tym razem zadeklarowane jako enum. Listing 5.36. Ulepszone deklaracje typów wyliczeniowych dla monet i dni tygodnia public enum Coin { JEDNOGROSZOWKA, PIECIOGROSZOWKA, DWUDZIESTOGROSZOWKA, PIECDZIESIECIOGROSZOWKA } public enum Weekday { PONIEDZIALEK, WTOREK, SRODA, CZWARTEK, PIATEK, SOBOTA, NIEDZIELA }

Pomimo widocznego podobieństwa do typów wyliczeniowych bazujących na int znanych z C++ i innych języków, typy wyliczeniowe enum widoczne na listingu 5.36 są klasami. Każda stała jest polem public static final, które reprezentuje instancję klasy enum. Ponieważ są to stałe finalne oraz dlatego, że nie można wywołać konstruktora stałej enum w celu utworzenia kolejnych stałych, można użyć operatora ==, aby bezpiecznie i wydajnie porównywać stałe (czego nie można powiedzieć o porównywaniu w ten sposób stałych w postaci ciągów znaków). Można na przykład wykonać instrukcję c == Coin.PIECIOGROSZOWKA. Typy wyliczeniowe enum zapewniają bezpieczeństwo typologiczne w fazie kompilacji, ponieważ zapobiegają porównywaniu stałych z dwóch różnych typów wyliczeniowych. Na przykład gdy kompilator napotka instrukcję Coin.JEDNOGROSZOWKA == Weekday.PONIEDZIALEK, zwróci błąd. Kompilator zgłosi także zastrzeżenie wobec próby przekazania do metody stałej należącej do nieodpowiedniego typu wyliczeniowego. Na przykład przekazanie stałej Weekday.PIATEK do metody, której typem parametru jest Coin, jest niedozwolone. Aplikacje, w których używa się typów wyliczeniowych enum, nie są tak podatne na błędy, ponieważ stałe tych typów wyliczeniowych nie są kompilowane do plików klas aplikacji. Ponadto typy enum udostępniają metodę toString(), która zwraca bardziej użyteczny opis wartości stałej. Ze względu na tak szeroką użyteczność typów wyliczeniowych enum, w wersji 5 języka Java działanie instrukcji switch rozszerzono o obsługę tych typów. Na listingu 5.37 zaprezentowano instrukcję switch, która przetwarza stałe typu wyliczeniowego Coin z listingu 5.36. 235

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Listing 5.37. Wykorzystanie instrukcji switch do przetwarzania wartości typu wyliczeniowego enum public class EnhancedSwitch { private enum Coin { JEDNOGROSZOWKA, PIECIOGROSZOWKA, DWUDZIESTOGROSZOWKA, PIECDZIESIECIOGROSZOWKA } public static void main(String[] args) { Coin coin = Coin. PIECIOGROSZOWKA; switch (coin) { case JEDNOGROSZOWKA : System.out.println("1 grosz"); break; case PIECIOGROSZOWKA : System.out.println("5 groszy"); break; case DWUDZIESTOGROSZOWKA : System.out.println("20 groszy"); break; case PIECDZIESIECIOGROSZOWKA: System.out.println("50 groszy"); default : assert false; } } }

Na listingu 5.37 pokazano działanie instrukcji switch na stałych typu wyliczeniowego enum. Tak rozszerzona instrukcja switch pozwala na wskazywanie etykiet poszczególnych przypadków tylko w postaci nazw stałych. Jeżeli nazwa stałej zostanie poprzedzona nazwą typu wyliczeniowego enum, na przykład jako Coin.DWUDZIESTOGROSZOWKA, kompilator zwróci błąd.

Rozszerzenia typów wyliczeniowych Enum Do typu wyliczeniowego enum można dodawać pola, konstruktory i metody; typy enum mogą nawet implementować interfejsy. Na przykład w kodzie na listingu 5.38 do typu Coin dodano pole, konstruktor i dwie metody, aby za ich pomocą każdej stałej typu Coin przypisać odpowiedni nominał monety, którą ta stała reprezentuje (na przykład 1 dla stałej JEDNOGROSZOWKA, 5 dla PIECIOGROSZOWKA i tak dalej), oraz przekształcać stałe na odpowiadające im nominały. Listing 5.38. Rozszerzenie typu wyliczeniowego enum Coin public enum Coin { JEDNOGROSZOWKA(1), PIECIOGROSZOWKA(5), DWUDZIESTOGROSZOWKA(20), PIECDZIESIECIOGROSZOWKA(50);

}

private final int denomValue; Coin(int denomValue) { this.denomValue = denomValue; } public int denomValue() { return denomValue; } public int toDenomination(int numPennies) { return numPennies/denomValue; }

236

ROZDZIAŁ 5. „ ZAAWANSOWANE MECHANIZMY JĘZYKA — CZĘŚĆ II

Konstruktor z listingu 5.38 przyjmuje wartość nominału, którą następnie przypisuje pustemu, finalnemu polu prywatnemu o nazwie denomValue — wszystkie pola powinny zostać zadeklarowane jako final, ponieważ stałe są niezmienne. Warto zauważyć, że wartość nominalna jest przekazywana do określonej zmiennej w momencie jej tworzenia (na przykład JEDNOGROSZOWKA(1)). „ Ostrzeżenie • Jeżeli po liście stałych oddzielonych od siebie przecinkami następuje cokolwiek innego

niż nawias klamrowy zamykający definicję typu wyliczeniowego enum, listę tę należy zakończyć znakiem średnika. W przeciwnym razie kompilator zwróci błąd.

Metoda denomValue() z listingu 5.38 zwraca wartość nominalną denomValue, zaś metoda toDenomination() zwraca liczbę monet określonego nominału, których wartość łączna jest równa liczbie groszy przekazanej do metody jako jej argument. Na przykład na 16 groszy składają się trzy pięciogroszówki. Na listingu 5.39 pokazano, w jaki sposób można wykorzystać rozszerzony typ wyliczeniowy enum o nazwie Coin. Listing 5.39. Wykorzystanie rozszerzonego typu wyliczeniowego Coin public static void main(String[] args) { if (args.length == 1) { int numPennies = Integer.parseInt(args[0]); System.out.println("Na " + numPennies + " groszy składa się:"); int numQuarters = Coin.PIECDZIESIECIOGROSZOWKA.toDenomination(numPennies); System.out.println(numQuarters + " raz(y) " + Coin.PIECDZIESIECIOGROSZOWKA.toString() ´+ ","); numPennies -= numQuarters*Coin.PIECDZIESIECIOGROSZOWKA.denomValue(); int numDimes = Coin.DWUDZIESTOGROSZOWKA.toDenomination(numPennies); System.out.println(numDimes + " raz(y) " + Coin.DWUDZIESTOGROSZOWKA.toString() + ","); numPennies -= numDimes*Coin.DWUDZIESTOGROSZOWKA.denomValue(); int numNickels = Coin.PIECIOGROSZOWKA.toDenomination(numPennies); System.out.println(numNickels + " raz(y) " + Coin.PIECIOGROSZOWKA.toString() + (numNickels != 1 ? ", " : " oraz")); numPennies -= numNickels*Coin.PIECIOGROSZOWKA.denomValue(); System.out.println(numPennies + " raz(y) " + Coin.JEDNOGROSZOWKA.toString()); } System.out.println(); System.out.println("Wartości nominalne:"); for (int i = 0; i < Coin.values().length; i++) System.out.println(Coin.values()[i].denomValue()); }

Na listingu 5.39 znajduje się kod aplikacji, która przekształca przekazany do niej argument wskazujący liczbę groszy w odpowiednie liczby pięćdziesięciogroszówek, dwudziestogroszówek, pięciogroszówek i jednogroszówek. Oprócz wywoływania na stałych typu Coin metod denomValue() i toDenomValue() aplikacja wywołuje także metodę toString(), aby wyświetlić ciąg znaków reprezentujący poszczególne monety. Kolejną wywoływaną metodą typu wyliczeniowego jest values(). Metoda ta zwraca tablicę wszystkich stałych typu Coin, które zadeklarowano w typie wyliczeniowym Coin (typem 237

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

wartości zwracanej przez metodę values() jest w przykładzie typ Coin[]). Tablica zwracana przez metodę przydaje się wówczas, gdy trzeba wykonać iterację przez stałe. Na przykład na listingu 5.39 metoda jest wywoływana po to, aby wyświetlić nominały każdej monety. Gdy przykładową aplikację wykona się z argumentem wiersza poleceń o wartości 119, zwróci ona następujący wynik: Na 119 groszy składa się: 2 raz(y) PIECDZIESIECIOGROSZOWKA, 0 raz(y) DWUDZIESTOGROSZOWKA, 3 raz(y) PIECIOGROSZOWKA, 4 raz(y) JEDNOGROSZOWKA Wartości nominalne: 1 5 20 50

Postać danych wynikowych wskazuje, że metoda toString() zwraca nazwę stałej. Czasami warto jest pokryć tę metodę i zaimplementować ją tak, by zwracała bardziej znaczące wartości. Na przykład metoda, która wyodrębnia z ciągu znaków tokeny (czy nazwane sekwencje znaków), może wykorzystywać typ wyliczeniowy Token do wyświetlania listy nazw tokenów oraz, przy użyciu metody toString(), ich wartości — jak na listingu 5.40. Listing 5.40. Pokrycie metody toString(), tak aby zwracała wartości stałych typu Token public enum Token { IDENTYFIKATOR("ID"), LICZBACALKOWITA("INT"), LEWYNAWIAS("("), PRAWYNAWIAS(")"), PRZECINEK(","); private final String tokValue; Token(String tokValue) { this.tokValue = tokValue; } @Override public String toString() { return tokValue; } public static void main(String[] args) { System.out.println("Wartości tokenów:"); for (int i = 0; i < Token.values().length; i++) System.out.println(Token.values()[i].name() + " = " + Token.values()[i]); } }

Aplikacja z listingu 5.40 wywołuje metodę values(), która ma za zadanie zwrócić tablicę stałych Token. Na każdej stałej jest wywoływana jej metoda name(), która zwraca nazwę tej 238

ROZDZIAŁ 5. „ ZAAWANSOWANE MECHANIZMY JĘZYKA — CZĘŚĆ II

stałej, a niejawnie wykonywana jest też metoda toString(), która zwraca wartość stałej. Gdyby aplikacja została uruchomiona, zwróciłaby ona następujący wynik: Wartości tokenów: IDENTYFIKATOR = ID LICZBACALKOWITA = INT LEWYNAWIAS = ( PRAWYNAWIAS = ) PRZECINEK = ,

Kolejny sposób rozszerzenia typu wyliczeniowego enum polega na przypisaniu różnych zachowań poszczególnym zmiennym. W celu wykonania tego zadania należy wprowadzić do typu wyliczeniowego metodę abstrakcyjną, a następnie pokryć ją w anonimowej klasie potomnej danej stałej. Opisaną technikę zaprezentowano na listingu 5.41 na przykładzie typu wyliczeniowego enum o nazwie TempConversion. Listing 5.41. Wykorzystanie anonimowych klas potomnych do różnicowania zachowania stałych typu wyliczeniowego public enum TempConversion { C2F("Stopnie Celsjusza na stopnie Fahrenheita") { @Override public double convert(double value) { return value*9.0/5.0+32.0; } }, F2C("Stopnie Fahrenheita na stopnie Celsjusza") { @Override public double convert(double value) { return (value-32.0)*5.0/9.0; } }; TempConversion(String desc) { this.desc = desc; } private String desc; @Override public String toString() { return desc; } public abstract double convert(double value); public static void main(String[] args) { System.out.println(C2F + " dla 100.0 stopni = " + C2F.convert(100.0)); System.out.println(F2C + " dla 98.6 stopni = " + F2C.convert(98.6)); } }

239

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Gdy uruchomi się przykładową aplikację, zwróci ona następujący wynik: Stopnie Celsjusza na stopnie Fahrenheita dla 100.0 stopni = 212.0 Stopnie Fahrenheita na stopnie Celsjusza dla 98.6 stopni = 37.0

Klasa Enum Dla kompilatora typ wyliczeniowy enum to „syntaktyczny cukierek”. Gdy kompilator napotyka deklarację typu wyliczeniowego enum (na przykład enum Coin{}), generuje klasę o takiej samej nazwie, jaka widnieje w deklaracji (Coin); dodatkowo klasa ta jest klasą potomną po abstrakcyjnej klasie Enum (z pakietu java.lang). Klasa Enum jest klasą bazową dla wszystkich typów wyliczeniowych języka Java. Analiza dokumentacji klasy Enum języka Java wykaże, że w klasie tej pokrywane są metody clone(), equals(), finalize(), hashCode() i toString() klasy Object: • clone() jest pokryta po to, aby zapobiec klonowaniu stałych, dzięki czemu nigdy nie

istnieje więcej niż jedna kopia stałej. W przeciwnym razie stałych nie można by porównywać przy użyciu operatora ==. • equals() jest pokryta po to, aby porównywać stałe na podstawie ich odwołań. Stałe tożsame (==) muszą mieć tę samą zawartość (equals()), a stałe, które nie są tożsame,

mają inne zawartości. • finalize() jest pokryta po to, aby stałych nie można było sfinalizować. • hashCode() jest pokryta dlatego, że pokryta jest equals(). • toString() jest pokryta po to, aby zwracała nazwę stałej.

Wszystkie wspomniane metody oprócz toString() są zadeklarowane jako final, dlatego nie można ich pokrywać w klasie potomnej. Klasa Enum udostępnia także własne metody. Są to final compareTo(), (Enum implementuje interfejs Comparable), getDeclaringClass(), name() oraz ordinal(): • Metoda compareTo() porównuje stałą bieżącą ze stałą przekazaną do metody jako jej

argument, aby ustalić, która z tych stałych występuje jako pierwsza w typie wyliczeniowym enum, oraz zwraca wartość wskazującą kolejność tych stałych. Dzięki metodzie możliwe jest posortowanie nieuporządkowanej tablicy stałych. • Metoda getDeclaringClass() zwraca obiekt klasy Class, który odpowiada typowi wyliczeniowemu enum bieżącej zmiennej. Na przykład jeśli dla typu wyliczeniowego Coin z listingu 5.36 zostanie wywołana metoda Coin.JEDNOGROSZOWKA.getDeclaring ´Class(),zostanie zwrócony obiekt Class dla typu Coin. Natomiast gdy dla typu wyliczeniowego enum o nazwie TempConversion z listingu 5.41 zostanie wywołana metoda TempConversion.C2F.getDeclaringClass(),zostanie zwrócony obiekt TempConversion. Metoda compareTo() wykorzystuje metodę getClass() klasy Class i metodę getDeclaring ´Class() klasy Enum, aby zapewnić porównywanie tylko tych stałych, które należą do tego samego typu wyliczeniowego enum. W przeciwnym razie zostanie rzucony wyjątek ClassCastException. (Więcej o klasie Class powiemy w rozdziale 7.).

240

ROZDZIAŁ 5. „ ZAAWANSOWANE MECHANIZMY JĘZYKA — CZĘŚĆ II

• Metoda name() zwraca nazwę stałej. Metoda toString() także zwraca nazwę stałej,

o ile nie zostanie pokryta metodą, która będzie zwracała inną wartość. • Metoda ordinal() zwraca wartość porządkową, czyli liczbę całkowitą, która wskazuje pozycję stałej w typie wyliczeniowym enum (dla pierwszej stałej wartością porządkową jest zero). Metoda compareTo() porównuje wartości porządkowe.

Klasa Enum udostępnia ponadto statyczną metodę valueOf(ClasstypEnum, String nazwa), która zwraca stałą wskazanego typu enum o podanej nazwie: • typEnum wskazuje obiekt klasy Class typu wyliczeniowego enum, którego stałą należy

zwrócić. • nazwa wskazuje nazwę stałej, którą należy zwrócić.

Na przykład instrukcja Coin penny = Enum.valueOf(Coin.class, "JEDNOGROSZOWKA"); przypisze zmiennej penny stałą typu Coin o nazwie JEDNOGROSZOWKA. W dokumentacji klasy Enum języka Java metoda value() nie jest opisana, ponieważ kompilator syntetyzuje (tworzy) ją w trakcie generowania klasy.

Rozszerzanie klasy Enum Typem ogólnym dla klasy Enum jest Enum. Wprawdzie lista parametrów typów formalnych wygląda mało przyjaźnie, jednak nietrudno ją rozszyfrować. Najpierw przedstawimy kod na listingu 5.42. Listing 5.42. Klasa Coin widoczna z perspektywy jej pliku klasy public final class Coin extends Enum { public static final Coin JEDNOGROSZOWKA = new Coin("JEDNOGROSZOWKA", 0); public static final Coin PIECIOGROSZOWKA = new Coin("PIECIOGROSZOWKA", 1); public static final Coin DWUDZIESTOGROSZOWKA = new Coin("DWUDZIESTOGROSZOWKA", 2); public static final Coin PIECDZIESIECIOGROSZOWKA = new Coin("PIECDZIESIECIOGROSZOWKA", 3); private static final Coin[] $VALUES = { JEDNOGROSZOWKA, PIECIOGROSZOWKA, ´DWUDZIESTOGROSZOWKA, PIECDZIESIECIOGROSZOWKA }; public static Coin[] values() { return Coin.$VALUES.clone(); } public static Coin valueOf(String name) { return Enum.valueOf(Coin.class, "Coin"); } private Coin(String name, int ordinal) { super(name, ordinal); } }

Niejawnie kompilator przekształci deklarację typu wyliczeniowego enum o nazwie Coin z listingu 5.36 do postaci deklaracji klasy podobnej do przedstawionej na listingu 5.42.

241

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Poniżej przedstawiono reguły, na podstawie których zapis Enum jest interpretowany w kontekście Coin extends Enum: • Każda klasa potomna po Enum musi dostarczać do Enum argument typu rzeczywistego. Na przykład w nagłówku Coin znajduje się deklaracja Enum. • Argumentem typu rzeczywistego musi być klasa potomna po Enum. Na przykład Coin jest klasą potomną po Enum. • W klasie potomnej po Enum (takiej jak Coin) należy używać idiomu w postaci nazwy tej klasy (Coin) przekazanej jako argument typu rzeczywistego.

Trzecia reguła pozwala na deklarowanie dla klasy Enum metod, takich jak compareTo(), getDeclaringClass() i valueOf(), których parametry i (lub) typy zwracanych wartości są wskazywane w kontekście klasy potomnej (Coin), a nie w kontekście Enum. Dzięki zastosowaniu takiego rozwiązania nie trzeba wykonywać rzutowań. Na przykład w instrukcji Coin penny = Enum.valueOf(Coin.class, "JEDNOGROSZOWKA"); nie trzeba rzutować wartości zwracanej przez metodę valueOf() do typu Coin. „ Uwaga • Kodu z listingu 5.42 nie można skompilować, ponieważ kompilator nigdy nie skompiluje żadnej

klasy, która rozszerza klasę Enum. Tak samo kompilator zgłosi zastrzeżenia względem super(name, ordinal);.

Ćwiczenia Celem poniższych ćwiczeń jest sprawdzenie wiedzy wyniesionej z tego rozdziału, dotyczącej asercji, adnotacji, mechanizmów ogólnych i typów wyliczeniowych. 1. Co to jest asercja? 2. Kiedy używa się asercji? 3. Zastosowanie opcji wiersza poleceń –ea bez argumentów włącza wszystkie asercje, w tym asercje systemowe. Prawda czy fałsz? 4. Co to jest adnotacja? 5. Dla jakich elementów aplikacji można definiować adnotacje? 6. Należy wskazać trzy typy adnotacji przeznaczonych dla kompilatora. 7. Jak deklaruje się typ adnotacji? 8. Co to jest adnotacja znacznikowa? 9. Co to jest element? 10. W jaki sposób elementowi przypisuje się wartość domyślną? 11. Co to jest metaadnotacja? 12. Należy wskazać cztery typy metaadnotacji w języku Java. 13. Należy podać definicję mechanizmu ogólnego. 14. Po co używa się mechanizmów ogólnych? 15. Jaka jest różnica między typem ogólnym a parametryzowanym?

242

ROZDZIAŁ 5. „ ZAAWANSOWANE MECHANIZMY JĘZYKA — CZĘŚĆ II

16. Który z wymienionych bytów nie może być mechanizmem ogólnym: niestatyczna klasa składowa, klasa lokalna, klasa wewnętrzna klasy anonimowej? 17. Należy wskazać pięć rodzajów argumentów typu rzeczywistego. 18. Nazwy typu podstawowego (na przykład double lub int) nie można wskazać jako argumentu typu rzeczywistego. Prawda czy fałsz? 19. Co to jest typ surowy? 20. Kiedy kompilator zgłasza komunikat ostrzeżenia „unchecked” i dlaczego? 21. W jaki sposób wytłumia się komunikaty ostrzeżeń „unchecked”? 22. Parametr typu E listy List nie jest ograniczony. Prawda czy fałsz? 23. W jaki sposób definiuje się pojedyncze górne ograniczenie? 24. MyList oznacza, że dla parametru typu E istnieje dolne ograniczenie Circle. Prawda czy fałsz? 25. Co to jest rekurencyjne ograniczenie typu? 26. Dlaczego niezbędne są wieloznaczne argumenty typu? 27. Co to jest uściślanie? 28. Parametry typu są uściślane. Prawda czy fałsz? 29. Co to jest zjawisko usuwania? 30. Co to jest metoda ogólna? 31. Która przeciążona metoda someOverloadedMethod() z listingu 5.43 wywołuje ogólną metodę methodCaller()? Listing 5.43. Która metoda someOverloadedMethod() jest wywoływana? import java.util.Date; public class CallOverloadedNGMethodFromGMethod { public static void someOverloadedMethod(Object o) { System.out.println("wywołanie someOverloadedMethod(Object o)"); } public static void someOverloadedMethod(Date d) { System.out.println("wywołanie someOverloadedMethod(Date d)"); } public static void methodCaller(T t) { someOverloadedMethod(t); } public static void main(String[] args) { methodCaller(new Date()); } }

32. Co to jest typ wyliczeniowy? 33. Należy opisać trzy problemy, które mogą wystąpić, gdy używa się typów wyliczeniowych ze stałymi typu int. 34. Co to jest typ wyliczeniowy enum?

243

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

35. W jaki sposób używa się instrukcji switch z enum? 36. Na jakie sposoby można rozszerzyć typ wyliczeniowy enum? 37. Do czego służy abstrakcyjna klasa Enum? 38. Jaka jest różnica między metodami name() i toString() klasy Enum? 39. Typem ogólnym klasy Enum jest Enum. Prawda czy fałsz? 40. Należy zadeklarować typ adnotacji znacznikowej ToDo, który będzie adnotować elementy typów oraz wykorzystywać domyślną politykę czasu użycia. 41. Należy przepisać aplikację StubFinder w taki sposób, aby korzystała ona z typu adnotacji Stub z listingu 5.15 (z odpowiednimi adnotacjami @Target i @Retention) oraz z klasy Deck z listingu 5.16. 42. Należy zaimplementować stos o konstrukcji analogicznej do klasy Queue z listingu 5.24. Stack musi być typem ogólnym, musi implementować metody push(), pop() i isEmpty() (można także zadeklarować metodę isFull(), jednak nie jest to konieczne w ramach tego ćwiczenia). Metoda push() musi rzucać instancję wyjątku StackFullException, gdy stos jest pełny, a pop() musi rzucać instancję wyjątku StackEmptyException, gdy stos jest pusty. (Konieczne jest zatem utworzenie własnych klas pomocniczych StackFullException i StackEmptyException, prywatnych w ramach pakietu, ponieważ nie występują one w bibliotece klas języka Java). Należy zadeklarować analogiczną metodę main() i umieścić w niej dwie asercje, które będą weryfikować przyjęte założenia o tym, że stos jest pusty zaraz po utworzeniu oraz po zdjęciu z niego ostatniego elementu. „ Uwaga • Stos to struktura danych, w której elementy są przechowywane w trybie ostatni wszedł –

pierwszy wyszedł. Elementy są dodawane do stosu za pomocą operacji umieszczania na stosie (ang. push) i zdejmowane poprzez operację zdejmowania (ang. pop). Ostatni element umieszczony na stosie jest pierwszym elementem, który zostanie z tego stosu zdjęty. 43. Należy zadeklarować typ wyliczeniowy enum o nazwie Compass ze składowymi POLNOC, POLUDNIE, WSCHOD, ZACHOD. Trzeba zadeklarować klasę UseCompass, której metoda main() będzie losowo wybierać jedną z tych stałych, a następnie na jej podstawie wykonywać instrukcję switch. Każdy z przypadków przetwarzanych w instrukcji switch powinien wyświetlać komunikat w stylu jazda na północ.

Podsumowanie Asercja to instrukcja, za pomocą której wyraża się założenie co do poprawności działania aplikacji, mające postać wyrażenia logicznego. Jeżeli wartością tego wyrażenia jest true, wykonanie aplikacji jest kontynuowane na następnej instrukcji. W przeciwnym razie następuje rzucenie błędu, który wskazuje przyczynę niepowodzenia. Istnieje wiele okoliczności, w których powinno się używać asercji. Okoliczności te kategoryzuje się na niezmiennik wewnętrzny, niezmiennik sterowania przebiegiem wykonania oraz projektowanie zgodnie z kontraktem. Niezmiennik to coś, co nie ulega zmianie. Oprócz okoliczności, w których powinno się używać asercji, występują też sytuacje, w których asercji nie powinno się stosować. Na przykład asercji nie powinno się używać do sprawdzania argumentów przekazanych do metod publicznych. 244

ROZDZIAŁ 5. „ ZAAWANSOWANE MECHANIZMY JĘZYKA — CZĘŚĆ II

Kompilator zapisuje asercje w pliku klasy. Jednak w fazie wykonania asercje są wyłączone, ponieważ mogą negatywnie wpływać na wydajność. Aby rozpocząć testowanie założeń odnośnie do sposobu zachowania klas, należy najpierw włączyć asercje w plikach klas. Adnotacje to instancje typów adnotacji, które kojarzą metadane z elementami aplikacji. Adnotacje wyraża się w kodzie źródłowym w ten sposób, że nazwy typów adnotacji poprzedza się symbolami @. Na przykład @Readonly jest adnotacją, której typem jest Readonly. Java udostępnia dość spory zbiór typów adnotacji, w tym typy przeznaczone dla kompilatora: Override, Deprecated i SuppressWarnings. Można jednak także zadeklarować własne typy adnotacji — służy do tego składnia @interface. Typy adnotacji można adnotować metaadnotacjami. Metaadnotacje wskazują elementy aplikacji, których może dotyczyć adnotacja (na przykład konstruktory, metody lub pola), reguły czasu życia i inne charakterystyki. Adnotacje, których typom przypisuje się reguły czasu życia (do czego służą adnotacje @Retention), mogą być przetwarzane w fazie wykonania za pomocą własnych aplikacji lub narzędzia apt języka Java, które w wersji 6 języka zostało zintegrowane z kompilatorem. W wersji 5 języka Java wprowadzono obsługę mechanizmów ogólnych, czyli mechanizmów języka służących do deklarowania i używania klas oraz interfejsów, których typ nie jest z góry określony. Gdy korzysta się z platformy kolekcji języka Java, dzięki mechanizmom ogólnym można uniknąć wyjątków ClassCastException. Typ ogólny to klasa lub interfejs, który wprowadza rodzinę typów sparametryzowanych przez zadeklarowanie listy parametrów typów formalnych. Metoda ogólna to metoda statyczna lub niestatyczna, której implementacja jest uogólniona pod względem używanych typów. Typ wyliczeniowy to taki typ, który jako zbiór dozwolonych wartości definiuje nazwaną sekwencję powiązanych ze sobą stałych. Programiści Javy tradycyjnie reprezentowali typy wyliczeniowe jako zbiory nazwanych stałych całkowitoliczbowych. Ponieważ z upływem czasu zbiory nazwanych stałych całkowitoliczbowych okazały się problematyczne, w wersji 5 języka Java wprowadzono alternatywną postać typów wyliczeniowych — typy wyliczeniowe enum. Typ wyliczeniowy enum to taki typ wyliczeniowy, który definiuje się przy użyciu słowa zastrzeżonego enum. Do typu wyliczeniowego enum można dodawać pola, konstruktory i metody. Typ enum może nawet implementować interfejsy. Można w nim także pokrywać metodę toString(), aby za jej pomocą zwracać bardziej zrozumiały opis wartości stałej, a także tworzyć klasy potomne względem stałych, aby przypisywać im różne zachowania. Dla kompilatora enum jest „cukierkiem syntaktycznym” oznaczającym klasę potomną po klasie Enum. Abstrakcyjna klasa enum pokrywa różne metody klasy Object, aby w ten sposób zdefiniować zachowania domyślne (zwykle ze względów bezpieczeństwa), a także implementuje dodatkowe metody o różnym przeznaczeniu. Tym rozdziałem zamykamy etap poznawania języka Java. Język ten posiada jednak kilka bardziej zaawansowanych mechanizmów, którym warto się przyjrzeć. Kilka z tych mechanizmów opiszemy w rozdziale 6., który jest pierwszym rozdziałem części poświęconej opisowi różnorodnych typów dostępnych w bibliotece klas standardowych języka Java SE i systemu operacyjnego Android.

245

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

246

ROZDZIAŁ 6

Podstawowe interfejsy API — część I Poważni programiści aplikacji dla systemu Android muszą gruntownie znać najważniejsze interfejsy API języka Java. Z kilkoma API mieliśmy już do czynienia, dość wspomnieć klasy Object i String oraz hierarchię klas Throwable. W tym rozdziale przedstawimy kolejne podstawowe interfejsy API przeznaczone do wykonywania obliczeń matematycznych, operowania na pakietach i typach podstawowych, a także mechanizm odśmiecania. „ Uwaga • W rozdziale 6. zostaną opisane podstawowe klasy i interfejsy API zlokalizowane w pakietach

java.lang, java.lang.ref i java.math.

Interfejsy API do wykonywania obliczeń matematycznych W rozdziale 2. zaprezentowano operatory +, -, *, / i % języka Java przeznaczone do wykonywania najważniejszych operacji matematycznych na wartościach typów podstawowych. Java udostępnia także klasy przeznaczone do wykonywania operacji trygonometrycznych i innych zaawansowanych działań, precyzyjnej prezentacji wartości pieniężnych oraz obsługi bardzo długich liczb całkowitych wykorzystywanych do szyfrowania algorytmem RSA (http://pl. wikipedia.org/wiki/RSA_(kryptografia)) i w innych kontekstach.

Klasy Math i StrictMath Klasa java.lang.Math deklaruje stałe typu double o nazwach E i PI, które reprezentują odpowiednio podstawę logarytmu naturalnego (2,71828…) oraz stosunek obwodu okręgu do jego średnicy (3,141519…). Stała E jest inicjalizowana wartością 2,718281828459045, natomiast stała PI ma wartość 3,141592653589793. W klasie Math zadeklarowane są także wybrane metody klasy, przeznaczone do wykonywania różnego rodzaju działań matematycznych. W tabeli 6.1 opisano większość dostępnych metod.

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Tabela 6.1. Metody klasy Math Metoda

Opis

double abs(double d)

Zwraca wartość bezwzględną liczby d. Istnieją cztery przypadki szczególne: abs(-0.0) = +0.0, abs(+nieskończoność) = +nieskończoność, abs(-nieskończoność) = +nieskończoność oraz abs(NaN) = NaN.

float abs(float f)

Zwraca wartość bezwzględną liczby f. Istnieją cztery przypadki szczególne: abs(-0.0) = +0.0, abs(+nieskończoność) = +nieskończoność, abs(-nieskończoność) = +nieskończoność oraz abs(NaN) = NaN.

int abs(int i)

Zwraca wartość bezwzględną liczby i. Istnieje jeden przypadek szczególny: wartością bezwzględną Integer.MIN_VALUE jest Integer.MIN_VALUE.

long abs(long l)

Zwraca wartość bezwzględną liczby l. Istnieje jeden przypadek szczególny: wartością bezwzględną Long.MIN_VALUE jest Long.MIN_VALUE.

double acos(double d)

Zwraca arcus cosinus kąta d z przedziału od 0 do PI. Istnieją trzy przypadki szczególne: acos(wartość > 1) = NaN, acos(wartość < -1) = NaN oraz acos(NaN) = NaN.

double asin(double d)

Zwraca arcus sinus kąta d z przedziału od –PI/2 do PI/2. Istnieją trzy przypadki szczególne: asin(wartość > 1) = NaN, asin(wartość < -1) = NaN oraz asin(NaN) = NaN.

double atan(double d)

Zwraca arcus tangens kąta d z przedziału –PI/2 do PI/2. Istnieje pięć przypadków szczególnych: atan(+0.0) = +0.0, atan(-0.0) = -0.0, atan(+nieskończoność) = +PI/2, atan(-nieskończoność) = -PI/2 oraz atan(NaN) = NaN.

double ceil(double d)

Zwraca najmniejszą wartość (najbliższą minus nieskończoności), która nie jest mniejsza od d i jest liczbą całkowitą. Istnieje sześć przypadków szczególnych: ceil(+0.0) = +0.0, ceil(-0.0) = -0.0, ceil(wartość > -1.0 oraz < 0.0) = -0.0, ceil(+nieskończoność) = +nieskończoność, ceil(-nieskończoność) = -nieskończoność oraz ceil(NaN) = NaN.

double cos(double d)

Zwraca cosinus kąta d (wyrażonego w radianach). Istnieją trzy przypadki szczególne: cos(+nieskończoność) = NaN, cos(-nieskończoność) = NaN oraz cos(NaN) = NaN.

double exp(double d)

Zwraca liczbę Eulera e podniesioną do potęgi d. Istnieją trzy przypadki szczególne: exp(+nieskończoność) = + nieskończoność, exp(nieskończoność) = +0.0 oraz exp(NaN) = NaN.

double floor(double d)

Zwraca największą wartość (najbliższą plus nieskończoności), która nie jest większa od d i jest liczbą całkowitą. Istnieje pięć przypadków szczególnych: floor(+0.0) = +0.0, floor(-0.0) = -0.0, floor(+nieskończoność) = +nieskończoność, floor(-nieskończoność) = -nieskończoność oraz floor(NaN) = NaN.

double log(double d)

Zwraca logarytm naturalny (przy podstawie e) z liczby d. Istnieje sześć przypadków szczególnych: log(+0.0) = - nieskończoność, log(-0.0) = - nieskończoność, log(wartość < 0) = NaN, log(+nieskończoność) = +nieskończoność, log(-nieskończoność) = NaN oraz log(NaN) = NaN.

248

ROZDZIAŁ 6. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ I

Tabela 6.1. Metody klasy Math — ciąg dalszy Metoda

Opis

double log10(double d)

Zwraca logarytm przy podstawie 10 z liczby d. Istnieje sześć przypadków szczególnych: log10(+0.0) = -nieskończoność, log10(-0.0) = -nieskończoność, log10(wartość < 0) = NaN, log10(+nieskończoność) = +nieskończoność, log10(-nieskończoność) = NaN oraz log10(NaN) = NaN.

double max(double d1, double d2)

Zwraca największą (najbliższą plus nieskończoności) spośród liczb d1 i d2. Istnieją cztery przypadki szczególne: max(NaN, wartość) = NaN, max(wartość, NaN) = NaN, max(+0.0, -0.0) = +0.0 oraz max(-0.0, +0.0) = +0.0.

float max(double f1, double f2)

Zwraca największą (najbliższą plus nieskończoności) spośród liczb f1 i f2. Istnieją cztery przypadki szczególne: max(NaN, wartość) = NaN, max(wartość, NaN) = NaN, max(+0.0, -0.0) = +0.0 oraz max(-0.0, +0.0) = +0.0.

int max(int i1, int i2)

Zwraca największą (najbliższą plus nieskończoności) spośród liczb i1 i i2.

long max(long l1, long l2)

Zwraca największą (najbliższą plus nieskończoności) spośród liczb l1 i l2.

double min(double d1, double d2)

Zwraca najmniejszą (najbliższą minus nieskończoności) spośród liczb d1 i d2. Istnieją cztery przypadki szczególne: min(NaN, wartość) = NaN, min(wartość, NaN) = NaN, min(+0.0, -0.0) = -0.0 oraz min(-0.0, +0.0) = -0.0.

float min(float f1, float f2)

Zwraca najmniejszą (najbliższą minus nieskończoności) spośród liczb f1 i f2. Istnieją cztery przypadki szczególne: min(NaN, wartość) = NaN, min(wartość, NaN) = NaN, min(+0.0, -0.0) = -0.0 oraz min(-0.0, +0.0) = -0.0.

int min(int i1, int i2)

Zwraca najmniejszą (najbliższą minus nieskończoności) spośród liczb i1 i i2.

long min(long l1, long l2)

Zwraca najmniejszą (najbliższą minus nieskończoności) spośród liczb l1 i l2.

double random()

Zwraca liczbę pseudolosową z przedziału prawostronnie otwartego od 0,0 (włącznie) do 1,0.

long round(double d)

Zwraca wynik zaokrąglenia liczby d do długiej liczby całkowitej. Wynik jest równoważny wynikowi wyrażenia (long) Math.floor(d+0.5). Istnieje siedem przypadków szczególnych: round(+0.0) = +0.0, round(-0.0) = +0.0, round(wartość > Long.MAX_VALUE) = Long.MAX_VALUE, round(wartość < Long.MIN_VALUE) = Long.MIN_VALUE, round(+nieskończoność) = Long.MAX_ ´VALUE, round(-nieskończoność) = Long.MIN_VALUE oraz round(NaN) = +0.0.

int round(float f)

Zwraca wynik zaokrąglenia liczby f do liczby całkowitej. Wynik jest równoważny wynikowi wyrażenia (long) Math.floor(f+0.5). Istnieje siedem przypadków szczególnych: round(+0.0) = +0.0, round(-0.0) = +0.0, round(wartość > Integer.MAX_VALUE) = Integer.MAX_VALUE, round(wartość < Integer.MIN_VALUE) = Integer.MIN_VALUE, round(+nieskończoność) = Integer.MAX_VALUE, round(-nieskończoność) = Integer.MIN_VALUE oraz round(NaN) = +0.0.

double signum(double d)

Zwraca znak liczby d jako liczbę –1,0 (jeżeli d jest mniejsze od 0,0), liczbę 0,0 (jeżeli d jest równe 0,0) lub 1,0 (jeżeli d jest większe niż 0,0). Istnieje pięć przypadków szczególnych: signum(+0.0) = +0.0, signum(-0.0) = -0.0, signum(+nieskończoność) = +1.0, signum(-nieskończoność) = -1.0 oraz signum(NaN) = NaN. 249

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Tabela 6.1. Metody klasy Math — ciąg dalszy Metoda

Opis

float signum(float f)

Zwraca znak liczby f jako liczbę –1,0 (jeżeli f jest mniejsze od 0,0), liczbę 0,0 (jeżeli f jest równe 0,0) lub 1,0 (jeżeli f jest większe niż 0,0). Istnieje pięć przypadków szczególnych: signum(+0.0) = +0.0, signum(0.0) = -0.0, signum(+nieskończoność) = +1.0, signum(-nieskończoność) = -1.0 oraz signum(NaN) = NaN.

double sin(double d)

Zwraca sinus kąta d (wyrażonego w radianach). Istnieje pięć przypadków szczególnych: sin(+0.0) = +0.0, sin(-0.0) = -0.0, sin(+nieskończoność) = NaN, sin(-nieskończoność) = NaN oraz sin(NaN) = NaN.

double sqrt(double d)

Zwraca pierwiastek kwadratowy liczby d. Istnieje pięć przypadków szczególnych: sqrt(+0.0) = +0.0, sqrt(-0.0) = -0.0, sqrt(wartość < 0) = NaN, sqrt(+nieskończoność) = +nieskończoność oraz sqrt(NaN) = NaN.

double tan(double d)

Zwraca tangens kąta d (wyrażonego w radianach). Istnieje pięć przypadków szczególnych: tan(+0.0) = +0.0, tan(-0.0) = -0.0, tan(+nieskończoność) = NaN, tan(-nieskończoność) = NaN oraz tan(NaN) = NaN.

double toDegrees (double angrad)

Przekształca miarę kąta angrad z radianów na stopnie za pomocą wyrażenia angrad*180/PI. Istnieje pięć przypadków szczególnych: toDegrees(+0.0) = +0.0, toDegrees(-0.0) = -0.0, toDegrees(+nieskończoność) = +nieskończoność, toDegrees(-nieskończoność) = -nieskończoność oraz toDegrees(NaN) = NaN.

double toRadians (angdeg)

Przekształca miarę kąta angdeg ze stopni na radiany za pomocą wyrażenia angdeg/180*PI. Istnieje pięć przypadków szczególnych: toRadians(+0.0) = +0.0, toRadians(-0.0) = -0.0, toRadians(+nieskończoność) = +nieskończoność, toRadians(-nieskończoność) = -nieskończoność oraz toRadians(NaN) = NaN.

W tabeli 6.1 przedstawiono obszerny zbiór metod przydatnych do wykonywania działań matematycznych. Na przykład każda metoda abs zwraca wartość bezwzględną (czyli liczbę bez względu na znak) przekazanego do niej argumentu. Metody abs(double) oraz abs(float) przydają się do bezpiecznego porównywania liczb zmiennopozycyjnych o podwójnej precyzji oraz liczb zmiennopozycyjnych. Na przykład wyrażenie 0.3 == 0.1+0.1+0.1 ma wartość false, ponieważ liczba 0,1 nie ma dokładnej reprezentacji. Wyrażenia te można jednak ze sobą porównać przy użyciu metody abs() i wartości tolerancji, która wskazuje akceptowalny poziom błędu. Na przykład wyrażenie Math.abs ´(0.3-(0.1+0.1+0.1)) < 0.1 będzie już mieć wartość true, ponieważ bezwzględna różnica między 0.3 i 0.1+0.1+0.1 jest mniejsza niż wartość tolerancji 0,1. We wcześniejszych rozdziałach przedstawiono inne metody klasy Math. Na przykład w rozdziale 2. zostały wykorzystane metody sin(), toRadians(), cos(), round(double) i random() tej klasy. Jak widać na przykładzie aplikacji Lotto649 z rozdziału 5., funkcja random() (która zwraca liczbę wyglądającą na losową, choć w rzeczywistości jest wyznaczana przez określoną funkcję matematyczną i dlatego jest tak naprawdę liczbą pseudolosową) przydaje się do symulacji, gier i w innych zastosowaniach, w których potrzeba losowości. Najpierw jednak 250

ROZDZIAŁ 6. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ I

liczbę zwracaną przez random(), która należy do przedziału od 0,0 do prawie 1,0, trzeba jakoś przekształcić do wartości bardziej przydatnej, na przykład należącej do przedziału od 0 do 49 albo od –100 do 100. Na listingu 6.1 znajduje się metoda rnd(), przydatna do wykonywania tego typu przekształceń. Listing 6.1. Przekształcanie wartości zwracanej przez random() do bardziej przydatnej wartości public static int rnd(int limit) { return (int) (Math.random()*limit); }

Metoda rnd() przekształca zwracaną przez random() liczbę zmiennopozycyjną o podwójnej precyzji z przedziału od 0,0 do 1,0 do liczby całkowitej z przedziału od 0 do limit – 1. Na przykład rnd(50) zwróci liczbę całkowitą z przedziału od 0 do 49. Z kolei instrukcja -100+rnd(201) przekształci przedział od 0,0 do prawie 1,0 do przedziału od –100 do 100 przez dodanie odpowiedniej wartości przesunięcia i wykorzystanie odpowiedniej wartości limit. „ Ostrzeżenie • Nie należy wykonywać instrukcji (int) Math.random()*limit, ponieważ wyrażenie

to zawsze będzie mieć wartość 0. W wyrażeniu ułamkowa liczba zmiennopozycyjna o podwójnej precyzji z przedziału od 0,0 do 0,99999… najpierw jest rzutowana na liczbę całkowitą 0 przez ucięcie części ułamkowej, a następnie 0 jest mnożone przez limit, co w efekcie daje również 0.

W tabeli 6.1 opisano także przypadki szczególne, dotyczące najczęściej wartości +nieskończoność, –nieskończoność, +0.0, –0.0 i NaN (ang. Not a Number — wartość, która nie jest liczbą). Wynikiem obliczeń zmiennopozycyjnych wykonywanych w języku Java mogą być wartości +nieskończoność, –nieskończoność, +0.0, –0.0 i NaN, ponieważ Java w dużej mierze jest zgodna ze standardem IEEE 754 (http://pl.wikipedia.org/wiki/IEEE_754), który opisuje sposób wykonywania obliczeń zmiennopozycyjnych. Poniżej przedstawiono okoliczności, w których pojawiają się wspomniane wartości specjalne: • +nieskończoność jest zwracana jako wynik dzielenia liczby dodatniej przez 0,0. Na przykład instrukcja System.out.println(1.0/0.0); zwróci wartość Infinity. • –nieskończoność jest zwracana jako wynik dzielenia liczby ujemnej przez 0,0. Na przykład instrukcja System.out.println(-1.0/0.0); zwróci wartość –Infinity. • NaN jest zwracana jako wynik dzielenia 0,0 przez 0,0, wartość pierwiastka kwadrato-

wego liczby ujemnej oraz wynik innych dziwnych operacji. Na przykład instrukcje System.out.println(0.0/0.0); oraz System.out.println(Math.sqrt(-1.0)); zwracają wynik NaN. • +0.0 jest zwracana jako wynik dzielenia liczby całkowitej przez +nieskończoność. Na przykład instrukcja System.out.println(1.0/(1.0/0.0)); zwróci wartość +0.0. • -0.0 jest zwracana jako wynik dzielenia liczby ujemnej przez +nieskończoność. Na przykład instrukcja System.out.println(-1.0/(1.0/0.0)); zwróci wartość -0.0.

251

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Jeżeli wynikiem jakiegoś działania jest wartość specjalna +nieskończoność, –nieskończoność lub NaN, wówczas całe wyrażenie ma zwykle taki sam wynik równy wartości specjalnej. Na przykład wynikiem instrukcji System.out.println(1.0/0.0*20.0); jest Infinity. Jednak wyrażenie, w którym pojawia się wartość +nieskończoność lub –nieskończoność, może też zwrócić wartość NaN. Na przykład w wyrażeniu 1.0/0.0*0.0 najpierw pojawia się +nieskończoność (jako wynik wyrażenia 1.0/0.0), a następnie NaN (jako wynik wyrażenia +nieskończoność*0, 0). Kolejna ciekawostka wiąże się z wartościami Integer.MAX_VALUE, Integer.MIN_VALUE, Long.MAX_VALUE oraz Long.MIN_VALUE. Każda z tych wartości jest klasą opakowującą typu podstawowego, która identyfikuje wartość maksymalną lub minimalną, jaka może być reprezentowana przez typ podstawowy skojarzony z klasą. Można się także zastanawiać, dlaczego nie istnieją przeciążone wersje metod abs(), max() i min(), które obsługują argumenty typu byte i short, czyli byte abs(byte b) oraz short abs(short s). Wersje takie nie są jednak potrzebne, ponieważ ograniczone przedziały liczb całkowitych typu byte i short sprawiają, że są one mało przydatne do wykonywania obliczeń. Jeżeli jednak metody takie są potrzebne, można skorzystać z implementacji przedstawionej na listingu 6.2. Listing 6.2. Przeciążone metody byte abs(byte b) i short abs(short s) public static byte abs(byte b) { return (b < 0) ? (byte) -b : b; } public static short abs(short s) { return (s < 0) ? (short) -s : s; } public static void main(String[] args) { byte b = -2; System.out.println(abs(b)); // Wynik: 2 short s = -3; System.out.println(abs(s)); // Wynik: 3 }

Rzutowania (byte) i (short) trzeba wykonać, ponieważ wyrażenie –b przekształca wartość zmiennej b z typu byte na typ int, a wyrażenie –s przekształca wartość zmiennej s z typu short na typ int. Z kolei rzutowania nie są potrzebne w wyrażeniach (b < 0) i (s < 0), ponieważ w tych przypadkach wartości b i s są automatycznie rzutowane do typu int przed porównaniem ich z wartością 0 typu int. „ Wskazówka • Brak wspomnianych wersji metod w klasie Math mógłby sugerować, że typy byte

i short nie są zbyt przydatne w deklaracjach metod. Jednak typy te przydają się wówczas, gdy deklaruje się tablice, których elementy przechowują małe wartości (na przykład wartości bajtowe w pliku binarnym). Gdyby tablica do przechowywania takich wartości została zadeklarowana jako typu int lub long, zmarnowałoby się w ten sposób znaczną ilość miejsca na stercie (a w skrajnym przypadku mogłoby to doprowadzić do wyczerpania się pamięci).

252

ROZDZIAŁ 6. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ I

Gdy w dokumentacji języka Java analizuje się informacje na temat pakietu java.lang, można natknąć się na klasę o nazwie StrictMath. Oprócz dłuższej nazwy, klasa wydaje się identyczna z klasą Math. Różnice między dwiema klasami można podsumować następująco: • Metody klasy StrictMath zwracają identyczne wyniki na wszystkich platformach. Natomiast metody klasy Math mogą zwracać nieco odmienne wartości, zależnie od platformy. • Ponieważ StrictMath nie może używać elementów charakterystycznych dla konkret-

nych platform, takich jak choćby koprocesor obliczeń matematycznych o zwiększonej precyzji, implementacja klasy StrictMath może być mniej wydajna od implementacji klasy Math. W większości przypadków metody klasy Math mogą wywoływać swoje odpowiedniczki z klasy StrictMath. Dwoma wyjątkami od tej reguły są metody toDegrees() i toRadians(). Wprawdzie w obydwóch klasach obie metody mają taką samą implementację, lecz w nagłówkach tych metod w klasie StrictMath występuje zastrzeżone słowo strictfp: public static strictfp double toDegrees(double angrad) public static strictfp double toRadians(double angdeg)

Według Wikipedii (http://en.wikipedia.org/wiki/Strictfp) słowo zastrzeżone strictfp ogranicza obliczenia zmiennopozycyjne w taki sposób, aby zapewnić przenośność. Słowo to umożliwia przenośność dzięki zapewnieniu jednolitej pośredniej reprezentacji liczb zmiennopozycyjnych oraz w zakresie nadmiarów i niedomiarów (czyli generowania wartości zbyt dużej lub zbyt małej w stosunku do ograniczeń reprezentacji). „ Uwaga • Zgodnie ze wspomnianym przed chwilą artykułem z Wikipedii na temat słowa zastrzeżonego

strictfp klasa Math zawiera metodę public static strictfp double abs(double); oraz inne metody strictfp. Jednak gdy w Javie 6 update 16 przeanalizuje się kod źródłowy tej klasy, okaże się, że nie ma w nim żadnego wystąpienia słowa strictfp. Jednak wiele metod klasy Math (na przykład metoda sin()) wywołuje swoje odpowiedniczki z klasy StrictMath, które są już zaimplementowane w bibliotece dla odpowiedniej platformy, a implementacje metod w tej bibliotece bazują już na strictfp.

Jeżeli słowo zastrzeżone strictfp nie jest obecne, wówczas obliczenia pośrednie nie są ograniczone do 32-bitowych i 64-bitowych reprezentacji zmiennopozycyjnych obsługiwanych przez Javę. W zamian w obliczeniach można korzystać z szerszych reprezentacji (w szczególności 128-bitowych) na tych platformach, które takie reprezentacje obsługują. W przypadku reprezentacji wartości na 32 lub 64 bitach w trakcie obliczeń pośrednich może dojść do nadmiaru lub niedomiaru. Natomiast jeśli reprezentacja bazuje na większej liczbie bitów, prawdopodobieństwo wystąpienia nadmiaru lub niedomiaru się zmniejsza. Ze względu na te różnice zapewnienie pełnej przenośności nie jest możliwe. Dlatego słowo zastrzeżone strictfp do pewnego stopnia wyrównuje te niespójności przez nałożenie wymagania, by na wszystkich platformach obliczenia pośrednie były wykonywane na reprezentacji 32-bitowej lub 64-bitowej. Gdy w deklaracji metody zostanie zawarte słowo zastrzeżone strictfp, będzie ono gwarantować, że wszystkie wykonywane w metodzie obliczenia zmiennopozycyjne zostaną wykonane zgodnie z regułami zapewnienia precyzji. Słowo zastrzeżone strictfp można umieścić 253

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

również w deklaracji klasy (na przykład public strictfp class FourierTransform), aby zapewnić, że wszystkie obliczenia zmiennopozycyjne wykonywane w ramach tej klasy będą przeprowadzane z zapewnieniem właściwej precyzji. „ Uwaga • Klasy Math i StrictMath są zadeklarowane jako final i dlatego nie można ich rozszerzać.

Ponadto w klasach tych znajdują się deklaracje prywatnych, pustych i bezargumentowych konstruktorów, co powoduje, że nie można tworzyć ich instancji. Klasy Math i StrictMath to przykłady tak zwanych klas narzędziowych (ang. utility classes), ponieważ wyznaczają one obszar zarezerwowany dla stałych narzędziowych oraz metod narzędziowych (static).

Klasa BigDecimal W rozdziale 2. zdefiniowano klasę CheckingAccount z polem o nazwie balance. Zgodnie z deklaracją pole balance jest typu int, a dodatkowo w kodzie źródłowym został umieszczony komentarz, według którego pole balance reprezentuje liczbę złotych, jaką można wypłacić z konta. Alternatywnie można było wskazać, że pole balance zawiera liczbę groszy dostępnych do wypłaty. Można zadać pytanie, dlaczego pole balance nie zostało zadeklarowane jako pole typu double lub float. Dzięki temu w polu balance można by przechowywać na przykład wartość 18,26 (18 złotych w części całkowitoliczbowej i 26 groszy w części ułamkowej). Pole balance nie zostało zadeklarowane jako typu float lub double z następujących względów: • Nie wszystkie wartości zmiennopozycyjne, które mogą reprezentować kwoty pienięż-

ne (w złotych i groszach), mogą być przechowywane w pamięci z odpowiednią precyzją. Na przykład liczba 0.1 (która może oznaczać 10 groszy) nie posiada precyzyjnej reprezentacji w pamięci. Gdyby wykonano wyrażenie double total = 0.1; for (int i = 0; i < 50; i++) total += 0.1; System.out.println(total);, jego wynikiem byłaby wartość 5.099999999999998, a nie prawidłowa wartość 5.1. • Wynik każdego obliczenia zmiennopozycyjnego musi zostać zaokrąglony do jednego

grosza. Jeżeli tak się nie stanie, w obliczeniach pojawią się drobne niedokładności, które w efekcie mogą spowodować, że ostateczny wynik będzie się znacznie różnił od prawidłowego. Wprawdzie klasa Math udostępnia dwie metody round(), za pomocą których można próbować zaokrąglać wyniki obliczeń do jednego grosza, lecz metody te zaokrąglają wartości do najbliższych liczb całkowitych (czyli złotych). Aplikacja InvoiceCalc z listingu 6.3 ilustruje obydwa wspomniane problemy. Jednak pierwszy z tych problemów nie jest aż tak poważny, ponieważ nie wpływa znacząco na dokładność ostatecznego wyniku. Zdecydowanie bardziej istotny jest drugi problem, przez który wyniki obliczeń nie są zaokrąglane do jednego grosza. Listing 6.3. Obliczanie zmiennopozycyjnej wartości faktury, które prowadzi do powstania błędów import java.text.NumberFormat; class InvoiceCalc {

254

ROZDZIAŁ 6. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ I final static double DISCOUNT_PERCENT = 0.1; // 10% final static double TAX_PERCENT = 0.05; // 5% public static void main(String[] args) { double invoiceSubtotal = 285.36; double discount = invoiceSubtotal*DISCOUNT_PERCENT; double subtotalBeforeTax = invoiceSubtotal-discount; double salesTax = subtotalBeforeTax*TAX_PERCENT; double invoiceTotal = subtotalBeforeTax+salesTax; NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(); System.out.println("Suma: " + currencyFormat.format(invoiceSubtotal)); System.out.println("Rabat: " + currencyFormat.format(discount)); System.out.println("Suma po uwzględnieniu rabatu: " + currencyFormat.format(subtotalBeforeTax)); System.out.println("Podatek: " + currencyFormat.format(salesTax)); System.out.println("Łącznie: " + currencyFormat.format(invoiceTotal)); } }

Na listingu 6.3 wykorzystano klasę NumberFormat (z pakietu java.text) i jej metodę format(), aby sformatować wartość zmiennopozycyjną o podwójnej precyzji na postać wartości walutowej. Więcej na temat klasy NumberFormat powiemy w rozdziale 9. Gdy uruchomi się aplikację InvoiceCalc, zwróci ona następujący wynik: Suma: 285,36 zł Rabat: 28,54 zł Suma po uwzględnieniu rabatu: 256,82 zł Podatek: 12,84 zł Łącznie: 269,67 zł

W wynikach działania aplikacji prawidłowo obliczone zostały kwoty sumy, rabatu, sumy po uwzględnieniu rabatu i podatku. Nieprawidłowo natomiast wskazana jest kwota łączna, której wartość wynosi 269,67 zamiast 269,66. Klient nie będzie skłonny zapłacić dodatkowego grosza, mimo że zgodnie z regułami obliczeń zmiennopozycyjnych to właśnie kwota 269,67 jest wartością prawidłową: Suma: 285,36 Rabat: 28,536 Suma po uwzględnieniu rabatu: 256,824 Podatek: 12,8412 Łącznie: 269,6652

Źródłem problemu jest to, że po wykonaniu każdej operacji, a przed wykonaniem operacji następnej uzyskany wynik nie jest zaokrąglany do najbliższego grosza. W efekcie 0,024 w liczbie 256,824 oraz 0,012 w liczbie 12,84 są uwzględniane także w kwocie końcowej, przez co metoda format() klasy NumberFormat zaokrągla kwotę końcową do wartości 269,67. Java udostępnia rozwiązanie obydwóch problemów w postaci klasy java.math.BigDecimal. Jest to klasa niezmienna (to znaczy instancja klasy BigDecimal nie może być zmieniona), która reprezentuje liczbę dziesiętną o określonym znaku (na przykład 23,653) ze wskazana precyzją (liczbą cyfr) i odpowiednią skalą (czyli z uwzględnieniem liczby całkowitej wyznaczającej liczbę cyfr po przecinku).

255

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

W klasie BigDecimal zadeklarowano trzy wygodne stałe: ONE, TEN i ZERO. Każda z tych stałych jest odpowiednikiem wartości 1, 10 i 0 ze skalą zerową. „ Ostrzeżenie • W klasie BigDecimal zadeklarowano kilka stałych, których nazwy zaczynają się od sło-

wa ROUND_. Stałe te są w większości przestarzałe i powinno się ich unikać. To samo dotyczy metod public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode) oraz public BigDecimal setScale(int newScale, int roundingMode), które są nadal obecne w klasie, aby zagwarantować prawidłową kompilację starszego kodu.

Klasa BigDecimal deklaruje ponadto kilka przydatnych konstruktorów i metod. Wybrane konstruktory i metody zostały opisane w tabeli 6.2. Tabela 6.2. Konstruktory i metody klasy BigDecimal Metoda

Opis

BigDecimal(int val)

Inicjalizuje instancję klasy BigDecimal liczbą cyfr wskazywaną przez val i skalą 0.

BigDecimal(String val)

Inicjalizuje instancję klasy BigDecimal dziesiętnym odpowiednikiem val. Jako skalę ustawia liczbę cyfr po przecinku lub 0, jeżeli przecinek nie występuje. Jeśli val będzie null, konstruktor rzuci wyjątek java.lang.NullPointerException. Jeżeli reprezentacja val w postaci ciągu znaków będzie nieprawidłowa (na przykład będzie zawierała litery), konstruktor rzuci wyjątek java.lang.NumberFormatException.

BigDecimal abs()

Zwraca nową instancję klasy BigDecimal, która zawiera bezwzględną wartość wartości instancji bieżącej. Skala nowej instancji jest taka sama jak skala instancji bieżącej.

BigDecimal add(BigDecimal augend)

Zwraca nową instancję klasy BigDecimal, która zawiera sumę wartości bieżącej oraz argumentu przekazanego do konstruktora. Skala w nowej instancji jest wyznaczana przez wartość większą spośród skali instancji bieżącej i skali instancji przekazanej jako argument. Jeżeli augend będzie null, metoda rzuci wyjątek NullPointerException.

BigDecimal divide(BigDecimal divisor)

Zwraca nową instancję klasy BigDecimal, która zawiera iloraz wartości bieżącej podzielonej i wartości argumentu. Skala nowej instancji to różnica między skalą instancji bieżącej i skalą instancji przekazanej jako argument. Skala ta może zostać odpowiednio dostosowana, jeżeli do przedstawienia wyniku dzielenia potrzebna będzie większa liczba cyfr. Jeżeli divisor będzie null, metoda rzuci wyjątek NullPointerException, a jeżeli divisor będzie reprezentować wartość 0 lub wyniku dzielenia nie będzie można zaprezentować precyzyjnie, metoda rzuci wyjątek java.lang.ArithmeticException.

BigDecimal max(BigDecimal val)

Zwraca this lub val, zależnie od tego, która z tych instancji posiada większą wartość. Jeżeli val będzie null, metoda rzuci wyjątek NullPointerException.

BigDecimal min(BigDecimal val)

Zwraca this lub val, zależnie od tego, która z tych instancji posiada mniejszą wartość. Jeżeli val będzie null, metoda rzuci wyjątek NullPointerException.

256

ROZDZIAŁ 6. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ I

Tabela 6.2. Konstruktory i metody klasy BigDecimal — ciąg dalszy Metoda

Opis

BigDecimal multiply(BigDecimal multiplicand)

Zwraca nową instancję klasy BigDecimal, która zawiera iloczyn wartości bieżącej i wartości instancji podanej jako argument. Skala nowej instancji jest sumą skali instancji bieżącej i instancji przekazanej jako argument. Jeżeli multiplicand jest null, metoda rzuci wyjątek NullPointerException.

BigDecimal negate()

Zwraca nową instancję klasy BigDecimal, która zawiera wartość przeciwną do wartości bieżącej. Skala nowej instancji jest taka sama jak skala instancji bieżącej.

int precision()

Zwraca precyzję bieżącej instancji klasy BigDecimal.

BigDecimal remainder(BigDecimal divisor)

Zwraca nową instancję klasy BigDecimal, która zawiera resztę z dzielenia wartości instancji bieżącej przez wartość instancji przekazanej jako argument. Skala nowej instancji to różnica między skalą bieżącą i skalą argumentu. Skala ta może zostać odpowiednio dostosowana, jeżeli do wyświetlenia wyniku potrzeba będzie większej liczby cyfr. Jeżeli divisor będzie null, metoda rzuci wyjątek NullPointerException, a jeśli divisor będzie mieć wartość 0, metoda rzuci wyjątek ArithmeticException.

int scale()

Zwraca skalę bieżącej instancji klasy BigDecimal.

BigDecimal setScale(int newScale, RoundingMode roundingMode)

Zwraca nową instancję klasy BigDecimal o wskazanej skali i trybie zaokrąglania. Jeżeli nowa skala jest większa niż skala dotychczasowa, wówczas do wartości, które były niewyskalowane, dodawane są zera. W takiej sytuacji nie potrzeba wykonywać zaokrąglania. Jeżeli nowa skala jest mniejsza niż skala dotychczasowa, wówczas usuwane są ostatnie cyfry. Jeżeli usuwane cyfry są inne niż zero, uzyskana w ten sposób liczba niewyskalowana musi zostać zaokrąglona. Zaokrąglenie wykonuje się w trybie zaokrąglania wskazanym jako argument. Jeżeli roundingMode jest null, metoda rzuci wyjątek NullPointerException. Jeżeli natomiast roundingMode będzie mieć wartość RoundingMode.ROUND_UNNECESSARY, a ze względu na bieżącą skalę zaokrąglenie będzie potrzebne, metoda rzuci wyjątek ArithmeticException.

BigDecimal subtract(BigDecimal subtrahend)

Zwraca nową instancję klasy BigDecimal, która zawiera wartość bieżącą pomniejszoną o wartość argumentu. Skala nowej instancji jest większą spośród skali bieżącej i skali argumentu. Jeżeli subtrahend będzie null, metoda rzuci wyjątek NullPointerException.

String toString()

Zwraca ciąg znaków, który stanowi reprezentację bieżącej instancji BigDecimal. Jeżeli to konieczne, zostanie użyta notacja naukowa.

W tabeli 6.2 wspomniano o typie RoundingMode wyznaczającym tryb zaokrąglania. Rounding ´Mode to typ wyliczeniowy enum, który zawiera stałe odpowiadające różnorodnym trybom zaokrąglania. Stałe te opisano w tabeli 6.3. Najlepszym sposobem oswojenia się z klasą BigDecimal jest zastosowanie jej w praktyce. Na listingu 6.4 wykorzystano tę klasę do wykonania prawidłowych obliczeń wartości na fakturze, którą przedstawiono wcześniej na listingu 6.3.

257

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Tabela 6.3. Stałe typu RoundingMode Stała

Opis

CEILING

Zaokrągla w kierunku plus nieskończoności.

DOWN

Zaokrągla w kierunku zera.

FLOOR

Zaokrągla w kierunku minus nieskończoności.

HALF_DOWN

Zaokrągla w kierunku „bliższej liczby sąsiedniej”, chyba że odległość do liczb sąsiednich jest taka sama — wówczas zaokrągla w dół.

HALF_EVEN

Zaokrągla w kierunku „bliższej liczby sąsiedniej”, chyba że odległość do liczb sąsiednich jest taka sama — wówczas zaokrągla w stronę tej liczby sąsiedniej, która jest parzysta.

HALF_UP

Zaokrągla w kierunku „bliższej liczby sąsiedniej”, chyba że odległość do liczb sąsiednich jest taka sama — wówczas zaokrągla w górę (o tym trybie zaokrąglania najczęściej uczy się w szkole).

UNNECESSARY

Zaokrąglanie nie jest potrzebne, ponieważ wynik działania jest zawsze dokładny.

UP

Liczby dodatnie są zaokrąglane w kierunku plus nieskończoności, a liczby ujemne są zaokrąglane w kierunku minus nieskończoności.

Listing 6.4. Obliczenia wartości na fakturze przy użyciu klasy BigDecimal, dzięki której unika się błędów zaokrągleń class InvoiceCalc { public static void main(String[] args) { BigDecimal invoiceSubtotal = new BigDecimal("285.36"); BigDecimal discountPercent = new BigDecimal("0.10"); BigDecimal discount = invoiceSubtotal.multiply(discountPercent); discount = discount.setScale(2, RoundingMode.HALF_UP); BigDecimal subtotalBeforeTax = invoiceSubtotal.subtract(discount); subtotalBeforeTax = subtotalBeforeTax.setScale(2, RoundingMode.HALF_UP); BigDecimal salesTaxPercent = new BigDecimal("0.05"); BigDecimal salesTax = subtotalBeforeTax.multiply(salesTaxPercent); salesTax = salesTax.setScale(2, RoundingMode.HALF_UP); BigDecimal invoiceTotal = subtotalBeforeTax.add(salesTax); invoiceTotal = invoiceTotal.setScale(2, RoundingMode.HALF_UP); System.out.println("Suma: " + invoiceSubtotal); System.out.println("Rabat: " + discount); System.out.println("Suma po uwzględnieniu rabatu: " + subtotalBeforeTax); System.out.println("Podatek: " + salesTax); System.out.println("Łącznie: " + invoiceTotal); } }

W metodzie main() z listingu 6.4 najpierw tworzone są obiekty BigDecimal o nazwach invoiceSubtotal i discountPercent, inicjalizowane wartościami odpowiednio 285.36 i 0.10. Wartość invoiceSubtotal jest mnożona przez discountPercent, a wynikowa instancja klasy BigDecimal tego mnożenia jest przypisywana zmiennej discount. Na tym etapie zmienna discount ma wartość 28.5360. Jeśli nie liczyć ostatniego zera, wartość ta jest taka sama jak wartość wynikowa wyrażenia invoiceSubtotal*DISCOUNT_PERCENT 258

ROZDZIAŁ 6. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ I

z listingu 6.3. Wartością, która powinna zostać przypisana zmiennej discount, jest 28.54. Aby rozwiązać ten problem jeszcze przed wykonaniem kolejnego obliczenia, w metodzie main() jest wywoływana metoda setScale() z następującymi argumentami: • 2 — dwie cyfry po przecinku. • RoundingMode.HALF_UP — standardowy sposób zaokrąglania.

Gdy określona jest już odpowiednia skala i tryb zaokrąglania, metoda main() odejmuje discount od invoiceSubtotal, a instancję BigDecimal stanowiącą reprezentację uzyskanej różnicy przypisuje zmiennej subtotalBeforeTax. Następnie na zmiennej subtotalBeforeTax jest wywoływana metoda setScale(), aby odpowiednio zaokrąglić jej wartość przed wykonaniem kolejnego obliczenia. W kolejnym kroku main() tworzy obiekt klasy BigDecimal o nazwie salesTaxPercent, który zostaje zainicjalizowany wartością 0.05. Dalej subtotalBeforeTax zostaje przemnożona przez salesTaxPercent, a iloczyn jest przypisywany zmiennej salesTax. Na tej zmiennej, której typem jest BigDecimal, jest wywoływana metoda setScale(), aby odpowiednio zaokrąglić wartość reprezentowaną przez ten obiekt. Po wykonaniu tej czynności main() dodaje salesTax do subtotalBeforeTax. Suma zostaje przypisana zmiennej invoiceTotal, na której metoda setScale() dokonuje odpowiedniego zaokrąglenia. Na koniec wartości wszystkich obiektów klasy BigDecimal zostają przekazane do standardowego wyjścia za pomocą metody System.out.println(), która wywołuje na każdym obiekcie ich metody toString(), aby uzyskać ciąg znaków reprezentujący wartości poszczególnych instancji BigDecimal. Gdy tak zmodyfikowana wersja aplikacji InvoiceCalc zostanie uruchomiona, uzyskamy następujący wynik: Suma: 285,36 Rabat: 28,54 Suma po uwzględnieniu rabatu: 256,82 Podatek: 12,84 Łącznie: 269,66

„ Ostrzeżenie • Klasa BigDecimal deklaruje konstruktor BigDecimal(double val), którego w miarę

możliwości powinno się unikać. Konstruktor ten inicjalizuje instancję klasy BigDecimal wartością przekazaną jako val, przez co w sytuacji gdy wartość typu double nie może zostać przedstawiona jako wartość dokładna, tak utworzona instancja może odzwierciedlać nieprawidłową reprezentację wartości. Na przykład wynikiem wywołania BigDecimal(0.1) jest instancja z wartością 0.10000000000000000555111512 ´31257827021181583404541015625. Z kolei wywołanie BigDecimal("0.1") daje w wyniku instancję z prawidłową wartością 0.1.

Klasa BigInteger Klasa BigDecimal przechowuje liczbę dziesiętną ze znakiem w postaci niewyskalowanej wartości ze skalą określoną przez 32-bitową liczbę całkowitą. Wartość niewyskalowana jest przechowywana w instancji klasy java.math.BigInteger.

259

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Klasa BigInteger jest klasą niezmienną, która reprezentuje liczbę całkowitą ze znakiem o określonej precyzji. Wartość jest w tej klasie przechowywana w kodzie uzupełnień do dwóch (inaczej mówiąc, wszystkie bity są zamienione — jedynki na zera, a zera na jedynki — a do wyniku dodawana jest wartość 1, aby wynik był zgodny z kodem uzupełnień do dwóch używanym w typach języka Java: całkowitoliczbowym bajtowym, krótkim całkowitoliczbowym, całkowitoliczbowym i długim całkowitoliczbowym). „ Uwaga • Więcej informacji na temat zapisu w kodzie uzupełnień do dwóch można znaleźć w Wikipedii,

na stronie pod adresem http://pl.wikipedia.org/wiki/Kod_uzupe%C5%82nie%C5%84_do_dw%C3%B3ch.

W klasie BigInteger są zadeklarowane trzy przydatne stałe o nazwach ONE, TEN i ZERO. Każda z tych stałych odpowiada instancji klasy BigInteger reprezentującej wartości odpowiednio 1, 10 i 0. W klasie BigInteger jest zadeklarowana całkiem pokaźna liczba przydatnych konstruktorów i metod. Kilka wybranych konstruktorów i metod przedstawiono w tabeli 6.4. Najlepszym sposobem, aby oswoić się z klasą BigInteger, jest wypróbowanie jej w praktyce. Na listingu 6.5 wykorzystano klasę BigInteger do zaimplementowania metod factorial, aby porównać sposób ich działania. Listing 6.5. Porównanie metod factorial() class FactComp { public static void main(String[] args) { System.out.println(factorial(12)); System.out.println(); System.out.println(factorial(20L)); System.out.println(); System.out.println(factorial(170.0)); System.out.println(); System.out.println(factorial(new BigInteger("170"))); System.out.println(); System.out.println(factorial(25.0)); System.out.println(); System.out.println(factorial(new BigInteger("25"))); } public static int factorial(int n) { if (n == 0) return 1; else return n*factorial(n-1); } public static long factorial(long n) { if (n == 0) return 1; else return n*factorial(n-1); } public static double factorial(double n)

260

ROZDZIAŁ 6. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ I

Tabela 6.4. Konstruktory i metody klasy BigInteger Metoda

Opis

BigInteger(byte[] val)

Inicjalizuje instancję klasy BigInteger liczbą całkowitą przechowywaną w tablicy val. Element val[0] tej tablicy zawiera osiem najbardziej znaczących (lewych) bitów. Jeżeli val będzie null, konstruktor rzuci wyjątek NullPointerException, natomiast jeśli val.length będzie mieć wartość 0, konstruktor rzuci wyjątek NumberFormatException.

BigInteger(String val)

Inicjalizuje instancję klasy BigInteger liczbą całkowitą odpowiadającą val. Jeżeli val będzie null, konstruktor rzuci wyjątek NullPointerException, a jeśli ciąg znaków reprezentujący val będzie nieprawidłowy (na przykład będzie zawierać litery), wówczas konstruktor rzuci wyjątek NumberFormatException.

BigInteger abs()

Zwraca nową instancję klasy BigInteger, która zawiera wartość bezwzględną wartości instancji bieżącej.

BigInteger add(BigInteger augend)

Zwraca nową instancję klasy BigInteger, która zawiera sumę wartości bieżącej oraz wartości przekazanej jako argument. Jeżeli augend będzie null, metoda rzuci wyjątek NullPointerException.

BigInteger divide(BigInteger divisor)

Zwraca nową instancję klasy BigInteger, która zawiera iloraz wartości bieżącej podzielonej przez wartość przekazaną jako argument. Jeżeli divisor jest null, metoda rzuci NullPointerException, natomiast jeśli divisor reprezentuje wartość 0 lub wynik dzielenia nie jest wartością dokładną, metoda rzuci wyjątek ArithmeticException.

BigInteger max(BigInteger val)

Zwraca this lub val zależnie od tego, która instancja klasy BigInteger zawiera większą wartość. Jeżeli val jest null, metoda rzuci wyjątek NullPointerException.

BigInteger min(BigInteger val)

Zwraca this lub val zależnie od tego, która instancja klasy BigInteger zawiera mniejszą wartość. Jeżeli val jest null, metoda rzuci wyjątek NullPointerException.

BigInteger multiply(BigInteger multiplicand)

Zwraca nową instancję klasy BigInteger, która zawiera iloczyn wartości bieżącej oraz wartości argumentu. Jeżeli multiplicand będzie null, metoda rzuci NullPointerException.

BigInteger negate()

Zwraca nową instancję klasy BigInteger, która zawiera wartość przeciwną do wartości bieżącej.

BigInteger remainder(BigInteger divisor)

Zwraca nową instancję klasy BigInteger, która zawiera resztę z podzielenia wartości bieżącej przez wartość argumentu. Jeżeli divisor jest null, metoda rzuci wyjątek NullPointerException, natomiast jeżeli divisor reprezentuje wartość 0, metoda rzuci wyjątek ArithmeticException.

BigInteger subtract(BigInteger subtrahend)

Zwraca nową instancję klasy BigInteger, która zawiera wartość bieżącą pomniejszoną o wartość argumentu. Jeżeli subtrahend będzie null, metoda rzuci wyjątek NullPointerException.

String toString()

Zwraca ciąg znaków stanowiący reprezentację instancji BigInteger.

261

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID { if (n == 1.0) return 1.0; else return n*factorial(n-1); } public static BigInteger factorial(BigInteger n) { if (n.equals(BigInteger.ZERO)) return BigInteger.ONE; else return n.multiply(factorial(n.subtract(BigInteger.ONE))); } }

Na listingu 6.5 porównano cztery wersje rekurencyjnej metody factorial(), której zadaniem jest wyliczanie silni. Porównanie to pozwala zidentyfikować największą wartość argumentu, jaką można przekazać do pierwszych trzech metod, zanim metody te zaczną zwracać nieprawidłowe wartości spowodowane przekroczeniem przedziału wartości poprawnie reprezentowanych przez dany typ liczbowy. W pierwszej wersji metody wykorzystywany jest argument typu int, a przedział wartości przekazywanego argumentu wynosi od 0 do 12. Jeżeli do metody zostanie przekazany dowolny argument większy od 12, wartości silni tego argumentu nie będzie już można zaprezentować prawidłowo jako liczby typu int. Przedział, do jakiego może należeć argument metody factorial(), można poszerzyć przez zmianę typu parametru na long. Przedział ten nie zwiększy się jednak znacząco. Po wprowadzeniu zmiany typu argumentu do metody będzie można przekazać wartość nie większą niż 20. Aby jeszcze bardziej zwiększyć zakres obsługiwanych wartości, można zaimplementować metodę factorial(), której parametr i zwracana wartość będą typu double. Jest to możliwe, ponieważ za pomocą typu double można precyzyjnie reprezentować liczby całkowite. Jednak w takim przypadku największym argumentem, jaki można przekazać do metody, jest 170.0. Każda wartość większa niż 170.0 spowoduje, że metoda factorial() zwróci plus nieskończoność. Może się zdarzyć sytuacja, że konieczne będzie wyliczenie silni wartości większej niż 170 — na przykład gdy będą wykonywane pewne obliczenia statystyczne uwzględniające kombinacje lub permutacje. Jedyny sposób, który pozwoli na obliczanie takiej silni, polega na użyciu wersji metody factorial() wykorzystującej typ BigInteger. Gdy aplikacja z listingu 6.5 zostanie uruchomiona, zwróci następujące wyniki: 479001600 2432902008176640000 7.257415615307994E306 72574156153079989673967282111292631147169916812964513765435777989005618434017061578523507492 42617459511490991237838520776666022565442753025328900773207510902400430280058295603966612599 65825710439855829425756896631343961226257109494680671120556888045719334021266145280000000000 0000000000000000000000000000000 1.5511210043330986E25 15511210043330985984000000

262

ROZDZIAŁ 6. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ I

Pierwsze trzy wartości to najwyższe wartości silni zwracane przez metody factorial(), w których wykorzystano odpowiednio typy int, long i double. Czwarta wartość reprezentuje wartość typu BigInteger, która odpowiada najwyższej wartości silni typu double. Warto zauważyć, że metoda z typem double nie jest w stanie dokładnie zaprezentować wartości 170! (! to matematyczny symbol silni). Precyzja tej metody jest po prostu zbyt mała. Wprawdzie metoda próbuje zaokrąglić najmniejszą cyfrę, lecz takie zaokrąglenie nie zawsze się udaje — liczba kończy się cyframi 7994 zamiast 7998. Zaokrąglenie sprawdza się jedynie w przypadkach argumentów nie większych niż 25.0, o czym świadczą dwa ostatnie wiersze danych wynikowych. „ Uwaga • Algorytm szyfrowania RSA, klasa BigDecimal i obliczanie silni to praktyczne przykłady zasto-

sowania klasy BigInteger. Klasy tej można używać jednak także w zastosowaniach niestandardowych. Na przykład w artykule opublikowanym w magazynie „JavaWorld” z lutego 2006 pod tytułem Travel Through Time with Java (http://www.javaworld.com/javaworld/jw-02-2006/jw-0213-funandgames.html), który stanowił kolejną część serii Java Fun and Games, została wykorzystana klasa BigInteger do przechowywania obrazka w postaci bardzo dużej liczby całkowitej. Pomysł polegał na tym, by poeksperymentować z klasą BigInteger i sprawdzić, jak za jej pomocą można wyszukiwać zdjęcia ludzi i miejsc, które istniały w przeszłości, będą istnieć w przyszłości lub nigdy nie istniały. Artykuł można polecić zwłaszcza programistom, którzy lubią trochę poszaleć.

Informacje na temat pakietów Klasa java.lang.Package udostępnia informacje na temat pakietu (wstępne informacje na temat pakietów zostały przedstawione w rozdziale 4.). Informacje te dotyczą wersji implementacji i specyfikacji pakietu języka Java, nazwy pakietu, a także wskazują, czy pakiet został upakowany (ang. sealed), czyli czy wszystkie klasy wchodzące w skład pakietu zostały zarchiwizowane w tym samym pliku JAR. „ Uwaga • Pliki JAR zostały opisane w rozdziale 1.

W tabeli 6.5 opisano wybrane metody klasy Package. Na listingu 6.6 znajduje się kod źródłowy aplikacji PackageInfo, którą zaimplementowano, aby zaprezentować działanie większości metod klasy Package z tabeli 6.5. Listing 6.6. Uzyskiwanie informacji na temat pakietu public class PackageInfo { public static void main(String[] args) { if (args.length == 0) { System.err.println("użycie: java PackageInfo nazwaPakietu [wersja]"); return; } Package pkg = Package.getPackage(args[0]); if (pkg == null)

263

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Tabela 6.5. Metody klasy Package Metoda

Opis

String getImplementationTitle()

Zwraca tytuł implementacji pakietu, który może być null. Format tytułu jest nieokreślony.

String getImplementationVendor()

Zwraca nazwę sprzedawcy lub organizacji, która dostarczyła implementację pakietu. Nazwa ta może być null. Format nazwy jest nieokreślony.

String getImplementationVersion()

Zwraca numer wersji implementacji pakietu, który może być null. Ciąg reprezentujący numer wersji musi być sekwencją dodatnich dziesiętnych liczb całkowitych, oddzielonych od siebie znakami kropki. Liczby te mogą mieć poprzedzające zera.

String getName()

Zwraca nazwę pakietu w standardowym zapisie z kropkami, na przykład java.lang.

static Package getPackage(String packageName)

Zwraca obiekt klasy Package, który jest skojarzony z pakietem wskazywanym przez packageName. Jeżeli pakietu wskazywanego przez packageName nie można znaleźć, zwraca null. Jeżeli packageName jest null, metoda zwraca NullPointerException.

static Package[] getPackages()

Zwraca tablicę wszystkich obiektów klasy Package, które są dostępne dla kodu wywołującego.

String getSpecificationTitle()

Zwraca tytuł specyfikacji pakietu, który może być null. Format tytułu jest nieokreślony.

String getSpecificationVendor()

Zwraca nazwę sprzedawcy lub organizacji, która dostarczyła specyfikację zaimplementowaną w ramach pakietu. Nazwa ta może być null. Format nazwy jest nieokreślony.

String getSpecificationVersion()

Zwraca numer wersji specyfikacji implementacji pakietu, który może być null. Ciąg wskazujący numer wersji musi być sekwencją dodatnich dziesiętnych liczb całkowitych oddzielonych od siebie znakami kropek. Liczby te mogą mieć poprzedzające zera.

boolean isCompatibleWith(String desired)

Sprawdza, czy pakiet jest zgodny z podanym ciągiem wersji. W tym celu wersja specyfikacji pakietu jest porównywana z wersją desired. Jeżeli numer wersji specyfikacji pakietu jest wyższy od pożądanego numeru wersji desired lub mu równy, metoda zwraca wartość true (co oznacza, że pakiet jest zgodny); w przeciwnym razie metoda zwraca wartość false. Jeżeli desired jest null, metoda rzuca wyjątek NullPointerException, a jeżeli numer wersji pakietu lub numer wersji desired nie ma formatu z kropkami, metoda rzuca wyjątek NumberFormatException.

boolean isSealed()

Zwraca wartość true, jeżeli pakiet jest upakowany. W przeciwnym razie zwracana jest wartość false.

264

ROZDZIAŁ 6. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ I {

}

}

System.err.println("Nie znaleziono pakietu " + args[0]); return;

} System.out.println("Nazwa: " + pkg.getName()); System.out.println("Tytuł implementacji: " + pkg.getImplementationTitle()); System.out.println("Dostawca implementacji: " + pkg.getImplementationVendor()); System.out.println("Wersja implementacji: " + pkg.getImplementationVersion()); System.out.println("Tytuł specyfikacji: " + pkg.getSpecificationTitle()); System.out.println("Dostawca specyfikacji: " + pkg.getSpecificationVendor()); System.out.println("Wersja specyfikacji: " + pkg.getSpecificationVersion()); System.out.println("Upakowany: " + pkg.isSealed()); if (args.length > 1) System.out.println("Zgodny z wersją " + args[1] + ": " + pkg.isCompatibleWith(args[1]));

Aby skorzystać z aplikacji, w wierszu poleceń należy podać przynajmniej nazwę pakietu. Na przykład polecenie java PackageInfo java.lang zwraca w wersji 6 języka Java następujące dane wynikowe: Nazwa: java.lang Tytuł implementacji: Java Runtime Environment Dostawca implementacji: Sun Microsystems, Inc. Wersja implementacji: 1.6.0_16 Tytuł specyfikacji: Java Platform API Specification Dostawca specyfikacji: Sun Microsystems, Inc. Wersja specyfikacji: 1.6 Upakowany: false

Aplikacja PackageInfo pozwala ustalić, czy specyfikacja pakietu jest zgodna z określonym numerem wersji. Pakiet jest zgodny ze swoimi poprzednikami. Na przykład jeżeli aplikację wywoła się poleceniem java PackageInfo java.lang 1.6, zwróci ona wynik Zgodny z wersją 1.6: true, podczas gdy aplikacja wywołana poleceniem java PackageInfo java.lang 1.8 zwróci wynik Zgodny z wersją 1.6: false. Za pomocą aplikacji PackageInfo można także analizować pakiety utworzone samodzielnie. O tym, jak tworzy się własne pakiety, była już mowa w rozdziale 4. W przykładzie 4. zaprezentowano przykładowy pakiet logging. Plik PackageInfo.class należy skopiować do katalogu, w którym znajduje się katalog pakietu logging (w którym z kolei znajdują się skompilowane pliki klas). Następnie należy wykonać polecenie java PackageInfo logging. Aplikacja PackageInfo zwróci wówczas następujący wynik: Nie znaleziono pakietu logging

Komunikat błędu jest zwracany dlatego, że metoda getPackage() wymaga załadowania z pakietu co najmniej jednego pliku klasy, zanim będzie mogła zwrócić obiekt klasy Package z opisem pakietu. 265

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Jedynym sposobem, w jaki można wyeliminować powyższy komunikat błędu, jest załadowanie klasy z pakietu. W tym celu kod z listingu 6.7 należy uwzględnić w kodzie z listingu 6.6. Listing 6.7. Dynamiczne ładowanie klasy z pliku klasy if (args.length == 3) try { Class.forName(args[2]); } catch (ClassNotFoundException cnfe) { System.err.println("nie można załadować " + args[2]); return; }

Powyższy fragment kodu należy wstawić przed instrukcją Package pkg = Package.get ´Package(args[0]);. Kod ten ładuje plik klasy o nazwie wskazanej przez trzeci argument wiersza poleceń aplikacji PackageInfo. Gdy nową wersję aplikacji PackageInfo uruchomi się poleceniem java PackageInfo logging 1.5 logging.File, zwróci ona dane wynikowe przedstawione poniżej. W poleceniu wywołującym jako klasę, którą aplikacja ma załadować, wskazano klasę File pakietu logging. Nazwa: logging Tytuł implementacji: null Dostawca implementacji: null Wersja implementacji: null Tytuł specyfikacji: null Dostawca specyfikacji: null Wersja specyfikacji: null Upakowany: false Exception in thread "main" java.lang.NumberFormatException: Empty version string at java.lang.Package.isCompatibleWith(Unknown Source) at PackageInfo.main(PackageInfo.java:43)

Tak wiele wartości null w danych wynikowych nie powinno dziwić, ponieważ do pakietu logging nie dodano żadnych informacji. Dodatkowo metoda isCompatibleWith() rzuca wyjątek NumberFormatException, ponieważ pakiet logging nie zawiera numeru wersji specyfikacji w zapisie z kropką (numer ten jest null). Zapewne najprostszym sposobem umieszczenia informacji na temat pakietu w pakiecie logging jest utworzenie pliku logging.jar w sposób podobny do opisanego w rozdziale 4. Najpierw jednak trzeba utworzyć niewielki plik tekstowy, który będzie zawierać te informacje o pakiecie. Plik można nazwać dowolnie. Na listingu 6.8 została przedstawiona wersja pliku o nazwie manifest.mf. Listing 6.8. Zawartość pliku manifest.mf z informacjami o pakiecie Implementation-Title: Implementacja mechanizmu rejestracji Implementation-Vendor: Jeff Friesen Implementation-Version: 1.0a Specification-Title: Specyfikacja mechanizmu rejestracji Specification-Vendor: Jeff "JavaJeff" Friesen Specification-Version: 1.0 Sealed: true

266

ROZDZIAŁ 6. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ I

„ Uwaga • Należy pamiętać o tym, by ostatni wiersz pliku (Sealed: true) zakończyć naciśnięciem klawisza

Return lub Enter. W przeciwnym razie aplikacja zwróci prawdopodobnie wartość Upakowany: false, ponieważ narzędzie jar pakietu JDK może nie zapisać w pakiecie logging odpowiadającego temu parametrowi wiersza z pliku manifestu. Potwierdza to powszechną opinię, że narzędzie jar jest dość kapryśne.

Aby utworzyć plik JAR, który będzie zawierał pakiet logging i składające się na niego pliki oraz którego manifest, czyli specjalny plik MANIFEST.MF, będzie zawierał informacje na temat zawartości pliku JAR przedstawione na listingu 6.8, należy wykonać następujące polecenie: jar cfm logging.jar manifest.mf logging

Powyższe polecenie utworzy plik JAR o nazwie logging.jar (o czym decyduje opcja c odpowiedzialna za utworzenie i opcja f, która mówi o pliku). Polecenie włączy także zawartość pliku manifest.mf (dzięki opcji m jak manifest) do pliku MANIFEST.MF, który znajduje się w katalogu META-INF pakietu. „ Uwaga • Aby dowiedzieć się więcej na temat manifestu pliku JAR, warto zapoznać się z punktem „Jar

Manifest” na stronie „JAR File Specification” dokumentacji JDK (http://java.sun.com/javase/6/docs/ technotes/guides/jar/jar.html#JAR%20Manifest).

Jeżeli narzędzie jar nie zwróci żadnych komunikatów o błędach, należy wykonać przedstawione poniżej polecenie wiersza poleceń systemu Windows (lub wiersza systemu operacyjnego, który akurat jest używany). Polecenie to wykona aplikację PackageInfo i odczyta informacje na temat pakietu logging: java -cp logging.jar;. PackageInfo logging 1.0 logging.File

Tym razem wynik działania aplikacji będzie następujący: Nazwa: logging Tytuł implementacji: Implementacja mechanizmu rejestracji Dostawca implementacji: Jeff Friesen Wersja implementacji: 1.0a Tytuł specyfikacji: Specyfikacja mechanizmu rejestracji Dostawca specyfikacji: Jeff "JavaJeff" Friesen Wersja specyfikacji: 1.0 Upakowany: false Zgodny z wersją 1.0: true

Podstawowe klasy opakowujące Pakiet java.lang zawiera klasy Boolean, Byte, Character, Double, Float, Integer, Long i Short. Są to tak zwane podstawowe klasy opakowujące (ang. primitive wrapper classes), ponieważ opakowują one wartości typów podstawowych. „ Uwaga • Podstawowe klasy opakowujące są również znane jako klasy wartości (ang. value classes).

267

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Java udostępnia osiem wspominanych podstawowych klas opakowujących z dwóch powodów: • Platforma kolekcji (o której więcej powiemy w rozdziale 8.) udostępnia listy, zbiory

i mapy, które mogą przechowywać jedynie obiekty, natomiast nie można w nich umieszczać wartości typów podstawowych. Wartość typu podstawowego umieszcza się w instancji podstawowej klasy opakowującej, a następnie tę instancję można umieścić w kolekcji. • Podstawowe klasy opakowujące są kontenerami, w których można wygodnie skojarzyć stałe (na przykład MAX_VALUE i MIN_VALUE) i metody klas (takie jak metody parseInt() klasy Integer i metody isDigit(), isLetter() i toUpperCase() klasy Character) z typami

podstawowymi. W tym punkcie opiszemy wszystkie podstawowe klasy opakowujące, a także klasę o nazwie Number.

Klasa Boolean Klasa Boolean to najmniejsza spośród podstawowych klas opakowujących. Klasa ta deklaruje trzy stałe, z których dwie to stałe TRUE i FALSE, oznaczające predefiniowane obiekty klasy Boolean. Klasa Boolean deklaruje także dwa konstruktory, które inicjalizują obiekty klasy: • Boolean(boolean value) inicjalizuje obiekt klasy Boolean wartością value. • Boolean(String s) przekształca tekst przypisany parametrowi s na wartość true lub false, a następnie umieszcza tę wartość w obiekcie klasy Boolean.

Drugi konstruktor porównuje wartość parametru s z wartością true. Ponieważ w porównaniu tym nie bierze się pod uwagę wielkości liter, dowolna kombinacja czterech liter składających się na wartość true (czyli true, TRUE, tRue i tak dalej) zostanie umieszczona w obiekcie. W przeciwnym razie w obiekcie zostanie umieszczona wartość false. Uzupełnieniem dla konstruktorów klasy Boolean jest metoda boolean booleanValue(), która zwraca opakowaną wartość klasy Boolean. Klasa Boolean deklaruje także lub pokrywa następujące metody: • int compareTo(Boolean b), która porównuje bieżący obiekt klasy Boolean z parametrem b, aby ustalić ich kolejność. Metoda zwraca wartość 0, jeżeli bieżący obiekt zawiera tę samą wartość logiczną co parametr b, wartość dodatnią, jeżeli bieżący obiekt zawiera wartość true, a parametr b ma wartość false, oraz wartość ujemną, jeżeli bieżący obiekt zawiera wartość false, a parametr b wartość true. • boolean equals(Object o), która porównuje bieżący obiekt klasy Boolean z parametrem o i zwraca true, jeżeli o nie jest null, o jest typu Boolean i obydwa obiekty zawie-

rają tę samą wartość logiczną. • static boolean getBoolean(String name), która zwraca true, jeżeli właściwość systemowa (o których powiemy w rozdziale 7.) identyfikowana przez name istnieje i jest równa true. 268

ROZDZIAŁ 6. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ I

• int hashCode(),zwracająca odpowiedni kod skrótu, dzięki któremu możliwe jest wykorzystanie obiektów klasy Boolean w kolekcjach opartych na skrótach (powiemy

o nich w rozdziale 8.). • static boolean parseBoolean(String s), która parsuje wartość parametru s i zwraca true, jeżeli s równa się "true", "TRUE", "True" lub dowolnej innej kombinacji tych liter. W przeciwnym razie metoda zwraca false. (Parsowanie to czynność, w trakcie

której sekwencja znaków jest rozkładana na komponenty o określonym znaczeniu, tak zwane tokeny). • String toString(), która zwraca "true", jeżeli bieżąca instancja klasy Boolean zawiera true. W przeciwnym razie metoda zwraca "false". • static String toString(boolean b), która zwraca "true", jeżeli b zawiera true. W przeciwnym razie metoda zwraca wartość "false". • static Boolean valueOf(boolean b), która zwraca TRUE, jeżeli b zawiera true lub FALSE, jeżeli b zawiera false. • static Boolean valueOf(String s), która zwraca TRUE, jeżeli s równa się "true", "TRUE", "True" lub dowolnej innej kombinacji tych liter. W przeciwnym razie metoda zwraca FALSE. „ Ostrzeżenie • Programiści, którzy dopiero poznają klasę Boolean, często spodziewają się, że metoda

getBoolean() zwróci wartość true lub false obiektu klasy Boolean. Jednak metoda getBoolean() zwraca wartość właściwości systemowej opartej na klasie Boolean (więcej na temat właściwości systemowych powiemy w rozdziale 7.). Aby uzyskać wartość true lub false, należy skorzystać z metody booleanValue().

W większości przypadków lepiej jest użyć stałych TRUE lub FALSE, zamiast tworzyć obiekty klasy Boolean. Załóżmy na przykład, że trzeba zaimplementować metodę, która zwróci obiekt klasy Boolean z wartością true, jeżeli argument typu double tej metody będzie miał wartość ujemną, lub false, jeżeli argument ten będzie mieć wartość dodatnią lub będzie równy zero. Potrzebną metodę można zadeklarować w sposób analogiczny jak w przypadku metody isNegative() z listingu 6.9. Listing 6.9. Metoda isNegative(), w której tworzy się obiekt klasy Boolean, choć nie jest to konieczne public Boolean isNegative(double d) { return new Boolean(d < 0); }

Kod metody jest zwarty, lecz niepotrzebnie tworzy się w niej obiekt klasy Boolean. Jeśli metoda będzie często wywoływana, zostanie utworzonych wiele obiektów Boolean, które zajmą przestrzeń na stercie. Gdy na stercie zaczyna brakować miejsca, włączany jest proces odśmiecania, który spowalnia działanie aplikacji i obniża w ten sposób jej wydajność. Na listingu 6.10 przedstawiono ulepszoną wersję metody isNegative().

269

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Listing 6.10. Zrefaktoryzowana metoda isNegative(), w której nie tworzy się obiektów klasy Boolean public Boolean isNegative(double d) { return (d < 0) ? Boolean.TRUE : Boolean.FALSE; }

W nowej wersji metody nie trzeba już tworzyć obiektów klasy Boolean, ponieważ metoda zwraca predefiniowany obiekt TRUE lub FALSE. „ Wskazówka • Należy dążyć do tego, by liczba tworzonych obiektów klasy Boolean była jak najmniej-

sza. Dzięki temu mniejsza będzie ilość pamięci zużywanej przez aplikację, a wydajność samej aplikacji będzie wyższa, ponieważ proces odśmiecania nie będzie musiał być uruchamiany zbyt często.

Klasa Character Klasa Character jest największą spośród podstawowych klas opakowujących. Zawiera znaczną liczbę stałych, konstruktor, wiele metod oraz dwie klasy zagnieżdżone (Subset i UnicodeBlock). „ Uwaga • Wysoki stopień złożoności klasy Character ma swoje źródło w tym, że Java obsługuje Unicode

(http://pl.wikipedia.org/wiki/Unicode). Dla uproszczenia pominięto większość mechanizmów klasy związanych z obsługą Unicode, ponieważ wykraczają one poza zakres tego rozdziału.

Klasa Character deklaruje jeden konstruktor Character(char value), za pomocą którego obiekt klasy Character inicjalizuje się wartością value. Uzupełnieniem konstruktora jest metoda charValue(), która zwraca wartość opakowanego znaku. Programiści, którzy implementują swoje pierwsze aplikacje, często używają wyrażeń w postaci ch >= '0' && ch = 'A' && ch '0' && ch = '0' && ch numBits) return null; // nie można zmieścić wyniku w numBits kolumnach int numLeadingZeros = numBits-result.length(); String zerosPrefix = ""; for (int j = 0; j < numLeadingZeros; j++) zerosPrefix += "0"; return zerosPrefix + result; }

Metoda toAlignedBinaryString() przyjmuje dwa argumenty. Pierwszy zawiera 32-bitową liczbę całkowitą, którą należy przekształcić na binarny ciąg znaków, zaś drugi argument wskazuje liczbę kolumn bitów, w których należy wyświetlić ten ciąg znaków. Najpierw jest wywoływana metoda toBinaryString(), która zwraca binarny ciąg znaków stanowiący odpowiednik wartości i, lecz bez poprzedzających zer. Następująca po nim metoda toAlignedBinaryString() sprawdza, czy wszystkie cyfry binarnego ciągu znaków zmieszczą się w kolumnach bitów, których liczbę wyznacza numBits. Jeżeli cyfr będzie więcej niż kolumn, metoda zwróci null. (Więcej informacji na temat metody length() oraz innych metod klasy String przedstawimy w rozdziale 7.). W dalszej kolejności metoda toAlignedBinaryString() oblicza, iloma zerami należy poprzedzić wartość result, po czym w pętli tworzy ciąg znaków złożony z takiej liczby zer. Na koniec metoda zwraca ciąg zer, które mają poprzedzić wynikowy ciąg znaków. Złożona operacja połączenia ciągów znaków za pomocą operatora przypisania (+=) na pierwszy rzut oka nie budzi wątpliwości, jednak tak naprawdę jest to rozwiązanie mało wydajne, ponieważ pośrednie obiekty tworzone w trakcie tej operacji są najpierw tworzone, a następnie niszczone. Zastosowaliśmy jednak tak niewydajny kod, aby w rozdziale 7. przeciwstawić mu rozwiązanie zdecydowanie lepsze pod względem wydajności. 276

ROZDZIAŁ 6. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ I

Gdy aplikacja zostanie uruchomiona, zwróci następujące dane wynikowe: 00000111 0000000011111111 null

Klasa Number Wszystkie spośród klas Float, Double, Integer, Long, Short i Byte udostępniają nie tylko metodę xValue() odnoszącą się do samej klasy, ale również metody xValue() odnoszące się do wszystkich pozostałych klas. Na przykład w klasie Float dostępne są, oprócz floatValue(), również metody doubleValue(), intValue(), longValue(), shortValue() oraz byteValue(). Wszystkie sześć metod to składowe klasy java.lang.Number, która jest abstrakcyjną klasą bazową dla klas Float, Double, Integer, Long, Short i Byte. Metody floatValue(), doubleValue(), intValue() i longValue() klasy Number są więc metodami abstrakcyjnymi. Klasa Number jest również klasą bazową dla klas BigDecimal i BigInteger (a także dla niektórych klas przeznaczonych do pracy współbieżnej, o czym więcej powiemy w rozdziale 9.). Klasa Number istnieje po to, by uprościć iterowanie przez kolekcję obiektów klas potomnych po Number. Na przykład można zadeklarować zmienną typu List i zainicjalizować ją instancją klasy ArrayList. W tak utworzonej kolekcji można przechowywać różne klasy potomne po Number i iterować przez kolekcję, wykorzystując do tego wielopostaciowość wywołań metod klas podrzędnych.

API References W rozdziale 2. opisano mechanizm odśmiecania, który usuwa obiekt ze sterty, jeśli nie istnieją już żadne odwołania do tego obiektu. W rozdziale 3. z kolei przedstawiono metodę finalize() klasy Object. Jest to metoda, którą wywołuje proces odśmiecania, zanim usunie obiekt ze sterty. Metoda finalize() pozwala obiektowi na wykonanie czynności sprzątających. W tym punkcie będzie kontynuowany wątek zapoczątkowany w rozdziałach 2. oraz 3. Zostanie też zaprezentowane API References. Interfejs ten umożliwia aplikacji komunikowanie się, w ograniczonym zakresie, z procesem odśmiecania. Na początek przedstawimy podstawowe pojęcia związane z API References. Następnie opiszemy klasy Reference i ReferenceQueue interfejsu, po czym zakończymy prezentacją klas SoftReference, WeakReference i PhantomReference.

Podstawowe pojęcia Gdy uruchamia się aplikację, tworzony jest dla niej główny zbiór odwołań (ang. root set of references). Zbiór ten jest kolekcją zmiennych lokalnych, parametrów, pól klas i pól instancji, które aktualnie istnieją i zawierają — potencjalnie puste — odwołania do obiektów. Zbiór główny zmienia się z upływem czasu i w miarę wykonywania przez aplikację kolejnych zadań. Na przykład po powrocie z metody ze zbioru znikają jej parametry. 277

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Wiele procesów odśmiecania odczytuje główny zbiór odwołań w momencie, gdy zostają uruchomione. Na podstawie głównego zbioru odwołań proces odśmiecania sprawdza, czy obiekt jest osiągalny (albo czy obiekt żyje, to znaczy czy istnieją jakieś odwołania do niego), czy też jest nieosiągalny (czyli nie istnieją żadne odwołania do niego). Proces odśmiecania nie może niszczyć obiektów, które są osiągalne — proces ten może usuwać ze sterty jedynie te obiekty, które, począwszy od głównego zbioru odwołań, nie są osiągalne. „ Uwaga • Obiektami osiągalnymi są także te obiekty, które są osiągalne pośrednio za pośrednictwem

zmiennych ze zbioru głównego. Inaczej mówiąc, chodzi o obiekty, które są osiągalne za pośrednictwem żywych obiektów osiągalnych bezpośrednio za pomocą tych zmiennych. Obiekt, który pozostaje nieosiągalny na żadnej ścieżce, bez względu na to, którą zmienną z głównego zbioru odwołań się wybierze, podlega przetworzeniu przez mechanizm odśmiecania.

Począwszy od wersji 1.2 języka Java, obiekty osiągalne dzieli się na obiekty silnie osiągalne, miękko osiągalne, słabo osiągalne i złudnie osiągalne. W odróżnieniu od obiektów silnie osiągalnych, obiekty osiągalne miękko, słabo i złudnie mogą podlegać przetwarzaniu przez mechanizm odśmiecania. W poniższych punktach scharakteryzowano cztery wspomniane typy osiągalności zależnie od mocy odwołania — od najsilniejszego do najsłabszego. • Obiekt jest silnie osiągalny (ang. strongly reachable), jeżeli jest osiągalny dla wątku

i wątek ten nie musi przechodzić przez obiekty API Reference — wątek korzysta z silnego odwołania (ang. strong reference) przypisanego zmiennej z głównego zbioru odwołań. Nowo utworzony obiekt (na przykład obiekt, do którego odwołuje się zmienna d z instrukcji Double d = new Double(1.0);) jest silnie osiągalny dla wątku, który ten obiekt stworzył. (Więcej na temat wątków powiemy w rozdziale 7.). • Obiekt jest miękko osiągalny (ang. softly reachable), jeżeli nie jest silnie osiągalny,

lecz staje się osiągalny za pośrednictwem odwołania miękkiego (ang. soft reference), czyli odwołania do obiektu, które jest przechowywane w obiekcie klasy SoftReference. Najsilniejszym odwołaniem do takiego obiektu jest odwołanie miękkie. Gdy ilość miejsca na stercie jest już niewielka, proces odśmiecania zwykle usuwa miękkie odwołania do najstarszych miękko osiągalnych obiektów, po czym finalizuje te obiekty (przez wywołanie metody finalize()) i je usuwa.

• Obiekt jest słabo osiągalny (ang. weakly reachable), jeżeli nie jest silnie ani miękko

osiągalny, natomiast staje się osiągalny za pośrednictwem odwołania słabego (ang. weak reference), czyli odwołania do obiektu, które jest przechowywane w obiekcie klasy WeakReference. Najsilniejszym odwołaniem do takiego obiektu jest odwołanie słabe. Proces odśmiecania usuwa słabe odwołania do słabo osiągalnych obiektów i niszczy je (a wcześniej finalizuje) przy następnym uruchomieniu procesu, nawet jeśli w pamięci jest dużo wolnego miejsca.

• Obiekt jest złudnie osiągalny (ang. phantom reachable), jeżeli nie jest silnie, miękko

ani słabo osiągalny, został sfinalizowany i proces odśmiecania jest gotowy na odzyskanie zajmowanej przez niego pamięci. Dodatkowo do obiektu złudnie osiągalnego

278

ROZDZIAŁ 6. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ I

wiedzie złudne odwołanie (ang. phantom reference), czyli odwołanie do obiektu przechowywane w obiekcie klasy PhantomReference. Najsilniejszym odwołaniem do obiektu złudnie osiągalnego jest odwołanie złudne. „ Uwaga • Jedyna różnica między odwołaniem miękkim a słabym sprowadza się do tego, że prawdopo-

dobieństwo usunięcia obiektu miękko osiągalnego przez proces odśmiecania jest niższe niż w przypadku obiektu słabo osiągalnego. Poza tym odwołanie słabe nie ma wystarczającej mocy, by utrzymać obiekt w pamięci.

Obiekty, do których odwołanie jest przechowywane w obiektach klas SoftReference, WeakReference lub PhantomReference, to tak zwane referenty.

Klasy Reference i ReferenceQueue API Reference zawiera pięć klas, które wchodzą w skład pakietu java.lang.ref. Jądrem tego pakietu są klasy Reference i ReferenceQueue. Klasa Reference jest abstrakcyjną klasą główną dla wchodzących w skład pakietu klas potomnych SoftReference, WeakReference i PhantomReference. Klasa ReferenceQueue to klasa, której instancje opisują strukturę kolejek danych. Jeśli instancja klasy ReferenceQueue zostanie skojarzona z obiektem klasy potomnej Reference (czyli, krócej mówiąc, z obiektem Reference), wówczas w momencie gdy referent, do którego wiedzie odwołanie enkapsulowane w obiekcie Reference, zostanie zniszczony przez proces odśmiecania, obiekt Reference zostanie dodany do kolejki. „ Uwaga • Obiekt klasy ReferenceQueue kojarzy się z obiektem Reference w ten sposób, że obiekt klasy

ReferenceQueue przekazuje się do konstruktora odpowiedniej klasy potomnej po klasie Reference.

Klasa Reference jest zadeklarowana jako ogólny typ Reference, w którym T oznacza typ referenta. Klasa udostępnia następujące metody: • void clear() przypisuje odwołaniu null. Obiekt klasy Reference, na którym jest wy-

woływana ta metoda, nie jest kolejkowany (wstawiany) do skojarzonej z nim kolejki odwołań (jeżeli oczywiście taka kolejka odwołań jest skojarzona z obiektem). (Proces odśmiecania czyści odwołania w sposób bezpośredni, a nie przez wywołanie metody clear(). Metoda clear() jest wywoływana przez aplikacje). • boolean enqueue() dodaje obiekt klasy Reference, na którym metoda została wywołana, do skojarzonej z nią kolejki odwołań. Metoda zwraca true, gdy obiekt klasy Reference został zakolejkowany, lub false w przeciwnym wypadku — wówczas jest to znak, że

obiekt został zakolejkowany już wcześniej albo w momencie jego tworzenia nie został skojarzony z żadną kolejką. (Proces odśmiecania kolejkuje obiekty klasy Reference w sposób bezpośredni, a nie przez wywołanie metody enqueue(). Metoda enqueue() jest wywoływana przez aplikacje).

279

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

• T get() zwraca odwołanie przechowywane w obiekcie klasy Reference. Jeżeli prze-

chowywane odwołanie zostało wcześniej wyczyszczone czy to przez aplikację, czy to przez proces odśmiecania, zwracaną wartością jest null. • boolean isEnqueued() zwraca true, jeśli obiekt klasy Reference został zakolejkowany

przez aplikację lub przez proces odśmiecania. W przeciwnym razie metoda zwraca wartość false, co oznacza, że obiekt klasy Reference nie został w momencie jego utworzenia skojarzony z żadną kolejką. „ Uwaga • Klasa Reference deklaruje także konstruktory. Ponieważ konstruktory te są prywatne w ramach

pakietu, klasami potomnymi po Reference mogą być wyłącznie klasy z pakietu java.lang.ref. Nałożenie takiego ograniczenia było konieczne dlatego, że instancje klas potomnych po Reference muszą ściśle współpracować z procesem odśmiecania.

Klasa ReferenceQueue jest zadeklarowana jako ogólny typ ReferenceQueue, w którym T wskazuje typ referenta. Klasa ReferenceQueue deklaruje konstruktor i metody opisane poniżej. • ReferenceQueue() inicjalizuje nową instrukcję klasy ReferenceQueue. • Reference forName(String typename)

Zwraca obiekt klasy Class skojarzony z typem typename. Jeżeli typ ten jest elementem pakietu, typename musi zawierać kwalifikowaną nazwę tego pakietu (na przykład java.lang.String). Jeżeli klasa lub interfejs nie zostały załadowane do pamięci, wówczas przed zwróceniem obiektu klasy Class metoda sama ją ładuje (wczytuje do pamięci zawartość pliku klasy), konsoliduje (łączy zawartości klas i interfejsów do postaci stanu fazy wykonania maszyny wirtualnej, aby umożliwić ich wykonanie) i inicjalizuje (nadaje polom klas wartości domyślne, wykonuje konstrukcje inicjalizujące klasy, a także dokonuje innych czynności inicjalizujących klasy). Jeżeli typu nie można znaleźć, metoda rzuca wyjątek java.lang. ´ClassNotFoundException. Jeżeli w trakcie konsolidacji wystąpi błąd, metoda rzuci wyjątek java.lang.LinkageError, natomiast gdy wyjątek pojawi się w trakcie statycznej inicjalizacji klasy, metoda rzuci wyjątek java.lang.ExceptionInInitializerError.

Annotation[] getAnnotations()

Zwraca tablicę, która zawiera wszystkie adnotacje zadeklarowane dla klasy reprezentowanej przez obiekt klasy Class. Zwrócona tablica może być pusta.

Constructor[] getConstructors()

Zwraca tablicę, która zawiera obiekty klasy Constructor odzwierciedlające wszystkie publiczne konstruktory klasy reprezentowanej przez obiekt klasy Class. Jeżeli zwrócona tablica ma zerową długość, oznacza to, że reprezentowana klasa nie posiada publicznych konstruktorów, obiekt klasy Class odzwierciedla klasę tablicową lub obiekt ten odzwierciedla typ podstawowy albo void.

Annotation[] getDeclaredAnnotations()

Zwraca tablicę, która zawiera wszystkie adnotacje bezpośrednio zadeklarowane w klasie reprezentowanej przez obiekt klasy Class. Tablica nie zawiera adnotacji odziedziczonych. Zwrócona tablica może być pusta.

Constructor[] getDeclaredConstructors()

Zwraca tablicę obiektów klasy Constructor, które odzwierciedlają wszystkie konstruktory zadeklarowane przez klasę reprezentowaną przez obiekt klasy Class. Są to konstruktory o dostępie publicznym, chronionym, domyślnym (czyli w ramach pakietu) i prywatnym. Elementy zwracanej tablicy nie są posortowane i nie mają żadnego określonego porządku. Jeżeli klasa posiada konstruktor domyślny, zostaje on włączony do tablicy wynikowej. Jeżeli zwrócona tablica ma zerową długość, oznacza to, że obiekt klasy Class reprezentuje interfejs, typ podstawowy, klasę tablicową lub void.

290

ROZDZIAŁ 7. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ II

Tabela 7.1. Metody klasy Class — ciąg dalszy Metoda

Opis

Field[] getDeclaredFields()

Zwraca tablicę obiektów klasy Field, które odzwierciedlają wszystkie pola zadeklarowane przez klasę lub interfejs reprezentowane przez obiekt klasy Class. Tablica zawiera pola o dostępie publicznym, chronionym, domyślnym (czyli w ramach pakietu) i prywatnym, lecz nie zawiera pól odziedziczonych. Elementy zwróconej tablicy nie są posortowane i nie mają żadnego określonego porządku. Jeżeli zwrócona tablica ma zerową długość, oznacza to, że w klasie lub interfejsie nie ma zadeklarowanych żadnych pól bądź obiekt klasy Class reprezentuje typ podstawowy, klasę tablicową lub void.

Method[] getDeclaredMethods()

Zwraca tablicę obiektów klasy Method, które odzwierciedlają wszystkie metody zadeklarowane przez klasę lub interfejs reprezentowany przez obiekt klasy Class. Tablica zawiera metody o dostępie publicznym, chronionym, domyślnym (czyli w ramach pakietu) i prywatnym, natomiast nie zawiera metod odziedziczonych. Elementy zwróconej tablicy nie są posortowane i nie mają żadnego określonego porządku. Jeżeli zwrócona tablica ma zerową długość, oznacza to, że w klasie lub interfejsie nie ma zadeklarowanych żadnych metod bądź obiekt klasy Class reprezentuje typ podstawowy, klasę tablicową lub void.

Field[] getFields()

Zwraca tablicę obiektów klasy Field, które reprezentują wszystkie publiczne pola klasy lub interfejsu zadeklarowane przez obiekt klasy Class, w tym pola publiczne odziedziczone po klasach i interfejsach przodków. Elementy zwróconej tablicy nie są posortowane ani nie mają żadnego określonego porządku. Jeżeli zwrócona metoda ma zerową długość, oznacza to, że obiekt klasy Class reprezentuje klasę lub interfejs, które nie posiadają żadnych pól dostępnych publicznie, bądź klasę tablicową, typ podstawowy lub void.

Method[] getMethods()

Zwraca tablicę obiektów klasy Method, które odzwierciedlają wszystkie publiczne metody klasy lub interfejsu reprezentowanego przez obiekt klasy Class, w tym metody publiczne odziedziczone po klasach i interfejsach przodków. W przypadku klas tablicowych zwracane są wszystkie publiczne metody składowe odziedziczone po klasie Object. Elementy zwróconej tablicy nie są posortowane ani nie mają żadnego określonego porządku. Jeżeli zwrócona metoda ma zerową długość, oznacza to, że obiekt klasy Class reprezentuje klasę lub interfejs, które nie posiadają metod dostępnych publicznie, bądź typ podstawowy lub void.

String getName()

Zwraca nazwę klasy reprezentowaną przez obiekt klasy Class.

Package getPackage()

Zwraca obiekt klasy Package opisujący pakiet, w którym zlokalizowana jest klasa reprezentowana przez obiekt klasy Class. Jeżeli klasa jest składową pakietu nienazwanego, metoda zwraca null. 291

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Tabela 7.1. Metody klasy Class — ciąg dalszy Metoda

Opis

Class clazz = Class.forName(args[0]); System.out.println("NAZWA: " + clazz.getName()); System.out.println("PAKIET: " + clazz.getPackage().getName()); System.out.println("POLA");

292

ROZDZIAŁ 7. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ II Field[] fields = clazz.getDeclaredFields(); for (int i = 0; i < fields.length; i++) System.out.println(fields[i]); System.out.println("KONSTRUKTORY"); Constructor[] constructors = clazz.getDeclaredConstructors(); for (int i = 0; i < constructors.length; i++) System.out.println(constructors[i]); System.out.println("METODY"); Method[] methods = clazz.getDeclaredMethods(); for (int i = 0; i < methods.length; i++) System.out.println(methods[i]); } catch (ClassNotFoundException cnfe) { System.err.println("nie można znaleźć " + args[0]); } } }

Na listingu 7.1 zaprezentowano aplikację, w której wykorzystano API Reflection, aby odczytać dane na temat klasy lub interfejsu i wyświetlić ich nazwę, nazwę ich pakietu, pola, konstruktory (tylko w przypadku klas) i metody. Aplikacja zwraca wyłącznie te pola, konstruktory i metody, które zadeklarowano w klasie, albo pola i metody zadeklarowane w interfejsie. W metodzie main() najpierw sprawdzamy, czy do aplikacji przekazano tylko jeden argument wiersza poleceń. Następnie jest wywoływana metoda forName(), która próbuje zwrócić obiekt klasy Class reprezentujący klasę lub interfejs wskazywane przez ten argument. Jeżeli wykonanie metody forName() się powiedzie, odwołanie do zwróconego obiektu jest przypisywane zmiennej clazz. Nie można nadać tej zmiennej nazwy class, ponieważ jest to słowo zastrzeżone. Metoda forName() rzuci instancję klasy kontrolowanego wyjątku ClassNotFoundException, gdy nie będzie mogła zlokalizować pliku klasy danej klasy (na przykład gdy plik klasy został wykasowany przed wykonaniem aplikacji). Ponadto metoda rzuca błąd LinkageError, jeśli plik klasy danej klasy ma nieprawidłową postać, a także ExceptionInInitializerError, gdy statyczna inicjalizacja klasy się nie powiedzie. „ Uwaga • Gdy rzucany jest błąd ExceptionInInitializerError, często oznacza to, że konstrukcja inicjali-

zująca klasę rzuciła niekontrolowany wyjątek. Na przykład działanie konstrukcji inicjalizującej klasę w kodzie przedstawionej poniżej klasy FailedInitialization skutkuje rzuceniem błędu ExceptionIn ´InitializerError, ponieważ metoda someMethod() rzuca wyjątek NullPointerException: public class FailedInitialization { static { someMethod(null); } public static void someMethod(String s) { int len = s.length(); // s zawiera null System.out.println(s + " ma długość " + len + " znaków"); } }

293

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

Jeżeli klasa lub interfejs znajdują się w pakiecie, wówczas w momencie uruchomienia aplikacji należy podać specyfikację tego pakietu. Na przykład jeśli aplikacja zostanie wywołana poleceniem java ExploreType java.lang.Boolean, aby uzyskać informacje na temat pól, konstruktorów i metod zadeklarowanych w klasie Boolean z pakietu java.lang, aplikacja zwróci następujące dane wynikowe: NAZWA: java.lang.Boolean PAKIET: java.lang POLA public static final java.lang.Boolean java.lang.Boolean.TRUE public static final java.lang.Boolean java.lang.Boolean.FALSE public static final java.lang.Class java.lang.Boolean.TYPE private final boolean java.lang.Boolean.value private static final long java.lang.Boolean.serialVersionUID KONSTRUKTORY public java.lang.Boolean(java.lang.String) public java.lang.Boolean(boolean) METODY public int java.lang.Boolean.hashCode() public boolean java.lang.Boolean.equals(java.lang.Object) public int java.lang.Boolean.compareTo(java.lang.Boolean) public int java.lang.Boolean.compareTo(java.lang.Object) public static boolean java.lang.Boolean.getBoolean(java.lang.String) public static java.lang.String java.lang.Boolean.toString(boolean) public java.lang.String java.lang.Boolean.toString() public static java.lang.Boolean java.lang.Boolean.valueOf(java.lang.String) public static java.lang.Boolean java.lang.Boolean.valueOf(boolean) public boolean java.lang.Boolean.booleanValue() public static boolean java.lang.Boolean.parseBoolean(java.lang.String) private static boolean java.lang.Boolean.toBoolean(java.lang.String)

Zawarte w tabeli 7.1 opisy metod getAnnotations() i getDeclaredAnnotations() wskazują, że obydwie metody zwracają tablicę elementów typu Annotation, czyli interfejsu, który należy do pakietu java.lang.annotation. Annotation to interfejs przodka dla Override, SuppressWarnings i innych typów adnotacji. W opisach metod w tabeli 7.1 jest również mowa o klasach Constructor, Field i Method. Instancje tych klas (które są składowymi pakietu java.lang.reflect) reprezentują odpowiednio konstruktory klas oraz pola i metody interfejsu lub klasy. Klasa Constructor reprezentuje konstruktor i jest zadeklarowana ogólnie jako Constructor, gdzie T wskazuje klasę, w której konstruktor reprezentowany przez klasę Constructor jest zadeklarowany. Klasa Constructor deklaruje szereg różnych metod, a niektóre z nich opisano poniżej. • Annotation[] getDeclaredAnnotations() zwraca tablicę wszystkich adnotacji zade-

klarowanych na konstruktorze. Jeżeli na konstruktorze nie zadeklarowano żadnych adnotacji, zwracana tablica ma zerową długość. • Class getDeclaringClass() zwraca obiekt klasy Class reprezentujący klasę, w której

zadeklarowano konstruktor. • Class[] getExceptionTypes() zwraca tablicę obiektów klas Class, które reprezentują typy wyjątków wskazanych na liście w klauzuli throws konstruktora. Jeżeli konstruktor nie posiada klauzuli throws, tablica ma zerową długość.

294

ROZDZIAŁ 7. „ PODSTAWOWE INTERFEJSY API — CZĘŚĆ II

• String getName() zwraca nazwę konstruktora. • Class[] getParameterTypes() zwraca tablicę obiektów klasy Class, które repre-

zentują parametry konstruktora. Jeżeli w konstruktorze nie zadeklarowano parametrów, zwrócona tablica ma zerową długość. Klasa Field reprezentuje pole i deklaruje szereg różnych metod, z których część opisano poniżej. • Object get(Object object) zwraca wartość pola we wskazanym obiekcie object. • boolean getBoolean(Object object) zwraca wartość pola typu Boolean we wskazanym obiekcie object. • byte getByte(Object object) zwraca wartość pola typu bajtowego we wskazanym obiekcie object. • char getChar(Object object) zwraca wartość pola typu znakowego we wskazanym obiekcie object. • double getDouble(Object object) zwraca wartość pola zmiennopozycyjnego o podwójnej precyzji we wskazanym obiekcie object. • float getFloat(Object object) zwraca wartość pola zmiennopozycyjnego we wskazanym obiekcie object. • int getInt(Object object) zwraca wartość pola całkowitoliczbowego we wskazanym obiekcie object. • long getLong(Object object) zwraca wartość pola typu długiej liczby całkowitej we wskazanym obiekcie object. • short getShort(Object object) zwraca wartość pola typu krótkiej liczby całkowitej we wskazanym obiekcie object.

Metoda get() zwraca wartość pola dowolnego typu. W przeciwieństwie do niej pozostałe metody wymienione na liście zwracają wartości pól o określonych typach. Wszystkie opisane metody rzucają wyjątek NullPointerException, jeśli object jest null i pole jest polem instancji, wyjątek IllegalArgumentException, gdy object nie jest instancją klasy lub interfejsu deklarującego pole (albo nie jest instancją klasy potomnej bądź też klasy implementującej interfejs), oraz wyjątek IllegalAccessException, gdy nie można uzyskać dostępu do pola (ponieważ jest ono na przykład polem prywatnym). Klasa Method reprezentuje metodę i deklaruje szereg różnych metod, z których część opisano poniżej. • int getModifiers() zwraca 32-bitową liczbę całkowitą, której pola bitów wskazują modyfikatory słów zastrzeżonych metody (takie jak public, abstract czy static). Pola bitowe należy interpretować przy użyciu klasy java.lang.reflect.Modifier. Na przykład za pomocą instrukcji (method.getModifiers() & Modifier.ABSTRACT) == Method.ABSTRACT można sprawdzić, czy metoda (reprezentowana przez obiekt klasy Method, do którego odwołanie znajduje się w zmiennej method) jest abstrakcyjna. Wyrażenie to zwróci wartość true, jeśli metoda okaże się metodą abstrakcyjną. 295

JAVA. PRZYGOTOWANIE DO PROGRAMOWANIA NA PLATFORMĘ ANDROID

• Class getReturnType() zwraca obiekt klasy Class, który reprezentuje typ wartości

zwracanej przez metodę. • Object invoke(Object receiver, Object... args) wywołuje metodę na obiekcie wskazywanym przez receiver (argument ten jest ignorowany, jeśli metoda jest metodą klasy) i przekazuje do niej zmienną liczbę argumentów wskazywanych przez args. Metoda invoke() rzuca wyjątek NullPointerException, gdy receiver jest null i wywoływana metoda jest metodą instancji, wyjątek IllegalAccessException, gdy metoda nie jest dostępna (ponieważ jest na przykład metodą prywatną), wyjątek IllegalArgument ´Exception, gdy do wywoływanej metody jest przekazywana nieprawidłowa liczba argumentów bądź z innych powodów, oraz wyjątek java.lang.reflect.Invocation ´TargetException, gdy wywołana metoda rzuci wyjątek. • boolean isVarArgs() zwraca true, gdy w deklaracji metody wskazano, że przyjmuje

ona zmienną liczbę argumentów. Klasa java.lang.reflect.AccessibleObject jest klasą przodka dla klas Constructor, Field i Method. Klasa przodka udostępnia metody, które wskazują dostępność konstruktora, pola lub metody (czyli informują, czy encje te są prywatne), a także sprawiają, że konstruktor, pole lub metoda, które są niedostępne, stają się dostępne. Klasa AccessibleObject udostępnia metody opisane poniżej. • T getAnnotation(Class annotationType) zwraca adnotację określonego typu za-

deklarowaną dla konstruktora, pola lub metody, jeżeli taką adnotację rzeczywiście zadeklarowano. W przeciwnym razie metoda zwraca null. • boolean isAccessible() zwraca wartość true, jeśli konstruktor, pole lub metoda są

dostępne. • boolean isAnnotationPresent(Class