Java. Uniwersalne techniki programowania 9788301184803

Książka przedstawia sposoby i techniki programowania użyteczne we wszelkich zastosowaniach języka Java. W publikacji akc

991 241 4MB

Polish Pages [671] Year 2015

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Java. Uniwersalne techniki programowania
 9788301184803

  • Commentary
  • Wersja z epub

Table of contents :
Strona tytułowa
Strona redakcyjna
Spis treści
Wstęp
1. Klasy
1.1. Abstrakcja i hermetyzacja
1.2. Enumeracje
1.3. Definiowanie klas
1.4. Wykorzystanie składowych statycznych
1.5. Przeciążanie metod i konstruktorów
1.6. Klasy i obiekty niezmienne
1.7. Inicjowanie
1.8. Singletony
1.9. Klasy opakowujące typy proste
2. Ponowne wykorzystanie klas
2.1. Dziedziczenie
2.2. Konwersje referencyjne
2.3. Stwierdzanie typu
2.4. Dziedziczenie w Javie
2.5. Przedefiniowanie metod
2.6. Kowariancja typów wyników
2.7. Przedefiniowanie metod w wyliczeniach
2.8. Przedefiniowanie a wyjątki
2.9. Przedefiniowanie a przeciążanie, przesłanianie i pokrywanie
2.10. Adnotacja @override
2.11. Metody wirtualne i polimorfizm
2.12. Kompozycja
2.13. Kompozycja a dziedziczenie
2.14. Reguły ponownego wykorzystania klas
3. Wyjątki
3.1. Obsługa wyjątków
3.2. Zgłaszanie wyjątków
3.3. Ponowne zgłaszanie wyjątków
3.4. Niskopoziomowe przyczyny i łańcuchowanie wyjątków
3.5. Wykorzystanie informacji o śladzie stosu
4. Interfejsy i klasy wewnętrzne
4.1. Metody i klasy abstrakcyjne
4.2. Pojęcie interfejsu
4.3. Problem wielodziedziczenia
4.4. Definiowanie i implementowanie interfejsów
4.5. Interfejsy jako typy danych
4.6. Implementacja metod w interfejsach
4.7. Mixiny
4.8. Właściwości metod domyślnych
4.9. Prywatne metody w interfejsach
4.10. Pojęcie klasy wewnętrznej
4.11. Przykładowe zastosowanie klasy wewnętrznej
4.12. Anonimowe klasy wewnętrzne
4.13. Lokalne klasy wewnętrzne
5. Typy i metody sparametryzowane (generics)
5.1. Definiowanie typów sparametryzowanych Typy surowe i czyszczenie typów
5.2. Ograniczenia parametrów typu
5.3. Restrykcje
5.4. Metody sparametryzowane
5.5. Uniwersalne argumenty typu
6. Elementy programowania funkcyjnego w Javie 8 Przegląd pragmatyczny
6.1. O programowaniu funkcyjnym
6.2. Interfejsy na pomoc
6.3. Lambda-wyrażenia: pierwsze spotkanie
6.4. O gotowych interfejsach funkcyjnych
6.5. O przetwarzaniu strumieniowym
7. Lambda-wyrażenia
7.1. Interfejsy funkcyjne i lambda-wyrażenia
7.2. Składnia i cechy lambda-wyrażeń
7.3. Referencje do metod i konstruktorów
7.4. Gotowe interfejsy funkcyjne
7.5. Interfejsy z pakietu java.util.function a wyjątki kontrolowane
7.6. Lambda-wyrażenia i obiekty typu Optional
8. Proste narzędzia
8.1. Analiza składniowa tekstów i wyrażenia regularne
8.2. Uproszczenia stosowania wyrażeń regularnych w klasach String i Scanner
8.3. Działania na liczbach
8.4. Daty i czas
8.4.1. Tradycyjna klasa Calendar i operacje na datach
8.4.2. Daty i czas w Javie 8 – elementy nowego API
8.5. Formatowanie liczb i dat
8.6. Metody tablicowe
9. Kolekcje
9.1. Architektura kolekcji (JCF) Interfejsy i implementacje
9.2. Programowanie w kategoriach interfejsów
9.3. Ogólne operacje na kolekcjach
9.4. Operacje opcjonalne oraz wyjątki zgłaszane przez metody kolekcyjne
9.5. Przekształcanie kolekcji. Kolekcje z tablic
9.6. Przykłady ogólnych operacji na kolekcjach
9.7. Iterowanie po kolekcjach
9.7.1. Tradycyjny iterator i rozszerzone for
9.7.2. Iteracje wewnętrzne
9.7.3. Spliteratory
9.7.4. Konkurencyjne modyfikacje
9.8. Listy
9.8.1. Podstawowe implementacje i operacje
9.8.2. Szczególne implementacje list: niuanse metody Arrays.asList(...)
9.8.3. Iteratory listowe
9.8.4. Przykłady operacji na listach
9.9. Kolejki
9.10. Zbiory typu HashSet, metody hashCode() i equals()
9.11. Porównywanie i porządkowanie elementów kolekcji
9.12. Zbiory uporządkowane i nawigowalne
9.13. Mapy
9.13.1. Wprowadzenie
9.13.2. Implementacje i interfejsy Ogólne operacje na mapach
9.13.3. Iterowanie po mapach
9.13.4. Użycie domyślnych metod interfejsu Map
9.13.5. Sortowanie map
9.14. Algorytmy, widoki, fabrykatory kolekcji
9.15. Własne implementacje kolekcji
10. Przetwarzanie strumieniowe
10.1. Pojęcie strumienia Rodzaje i cechy operacji strumieniowych
10.2. Uzyskiwanie strumieni
10.3. Przegląd operacji na strumieniach
10.4. Filtrowanie i leniwość strumieni
10.5. Metoda forEach dla strumieni
10.6. Sortowanie strumieni
10.7. Redukcja
10.8. Proste kolektory
10.9. Kolektory budujące mapy
10.10. Generatory
10.11. Strumienie równoległe
10.12. Przykłady innych użytecznych metod
11. Wejście-wyjście
11.1. Programowanie wejścia-wyjścia: obraz ogólny
11.2. Abstrakcyjne strumienie wejścia-wyjścia Operacje elementarne
11.3. Strumieniowe klasy przedmiotowe
11.4. Instrukcja try-with-resources Automatyczne zarządzanie zasobami a obsługa tłumionych wyjątków
11.5. Strumieniowe klasy przetwarzające – przegląd
11.6. Buforowanie
11.7. Binarne strumienie wejścia-wyjścia
11.8. Kodowanie-dekodowanie przy użyciu strumieni wejścia-wyjścia
11.9. Serializacja obiektów
11.10. Potoki
11.11. Analiza składniowa strumieni – StreamTokenizer
11.12. Obiekty plikowe i klasa File
11.13. Wygodne metody klasy java.nio.file.Files
11.14. Skaner
11.15. Przeglądanie katalogów
11.16. Archiwa
11.17. Pliki o dostępie swobodnym
11.18. Nowe wejście-wyjście (NIO): przegląd
11.19. NIO: bufory
11.20. NIO: kanały i bufory. Kanały plikowe
11.21. Widoki buforów bajtowych
11.22. NIO: bufory – uporządkowanie bajtów (endianess)
11.23. NIO: bufory znakowe Kodowanie i dekodowanie
11.24. NIO: operacje kanałowe na wielu buforach (scattering i gathering)
11.25. NIO: mapowanie plików
11.26. NIO: bezpośrednie transfery kanałowe
12. Programowanie współbieżne i równoległe
12.1. Procesy i wątki
12.2. Uruchamianie równoległych działań Tworzenie wątków
12.3. Zadania i wykonawcy
12.4. Zamykanie wykonawców Oczekiwanie na zakończenie zadań i serwis kompletacji (CompletionService)
12.5. Zadania powtarzalne, opóźnione i okresowe
12.6. Wykonawcy a pule wątków
12.7. ForkJoinPool i zadania rekursywne
12.8. Kompletery typu CountedCompleter
12.9. CountedCompleter jako kontynuacja
12.10. Kiedy i jak używać zadań typu ForkJoinTask? Praktyczny przykład użycia kompleterów
12.11. Zadania kompletowalne (CompletableFuture)
12.12. Przerywanie zadań z zewnątrz i kończenie pracy wątków
12.13. Synchronizacja
12.14. Synchronizacja za pomocą jawnego ryglowania
12.15. Rygle do odczytu i zapisu: ReentrantReadWriteLock i StampedLock
12.16. Synchronizatory wyższego poziomu
12.17. Unikanie synchronizacji: struktura kodu, volatile, atomiki i konkurencyjne kolekcje
12.18. Koordynacja pracy wątków – mechanizm wait-notify
12.19. Koordynacja: warunki
13. Dynamiczna Java
13.1. Mechanizm refleksji
13.2. Uchwyty metod
13.3. Znaczenie refleksji – praktyczne przykłady
13.4. Refleksja a generics
13.5. Dynamiczne klasy proxy
13.6. JavaBeans
13.6.1. Koncepcja JavaBeans
13.6.2. Nasłuch i wetowanie zmian właściwości za pomocą obsługi zdarzeń
13.6.3. Introspekcja
13.7. Adnotacje
13.7.1. Istota adnotacji i sposoby ich definiowana
13.7.2. Przetwarzanie adnotacji w fazie wykonania
13.7.3. Przetwarzanie adnotacji w fazie kompilacji Transformowanie kodu bajtowego
13.8. Skrypty w Javie
14. Lokalizacja i internacjonalizacja aplikacji
14.1. Lokalizacje
14.2. Jeszcze trochę o formatorach liczbowych
14.3. Waluty
14.4. Strefy czasowe
14.5. Kalendarze i zlokalizowane daty
14.6. Porównywanie i sortowanie napisów
14.7. Internacjonalizacja aplikacji i dodatkowe zasoby (resource bundle)
Literatura
Przypisy

Citation preview

Projekt okładki i stron tytułowych Grzegorz Laskowski Ilustracja na okładce Shutterstock/photovs Wydawca Łukasz Łopuszański Redaktor prowadzący Jolanta Kowalczuk Redaktor Joanna Cierkońska Koordynator produkcji Anna Bączkowska Przygotowanie wersji elektronicznej Ewa Modlińska Skład wersji elektronicznej na zlecenie Wydawnictwa Naukowego PWN Marcin Kapusta Książka, którą nabyłeś, jest dziełem twórcy i wydawcy. Prosimy, abyś przestrzegał praw, jakie im przysługują. Jej zawartość możesz udostępnić nieodpłatnie osobom bliskim lub osobiście znanym. Ale nie publikuj jej w internecie. Jeśli cytujesz jej fragmenty, nie zmieniaj ich treści i koniecznie zaznacz, czyje to dzieło. A kopiując jej część, rób to jedynie na użytek osobisty. Szanujmy cudzą własność i prawo Więcej na www.legalnakultura.pl Polska Izba Książki Copyright © by Wydawnictwo Naukowe PWN SA Warszawa 2015 eBook został przygotowany na podstawie wydania papierowego z 2015 r. (wyd. I) Warszawa 2015 ISBN 978-83-01-18480-3

Wydawnictwo Naukowe PWN SA 02-460 Warszawa, ul. Gottlieba Daimlera 2 tel. 22 69 54 321, faks 22 69 54 288 infolinia 801 33 33 88 e-mail: [email protected] www.pwn.pl

Spis treści

Wstęp 1. Klasy 1.1. Abstrakcja i hermetyzacja 1.2. Enumeracje 1.3. Definiowanie klas 1.4. Wykorzystanie składowych statycznych 1.5. Przeciążanie metod i konstruktorów 1.6. Klasy i obiekty niezmienne 1.7. Inicjowanie 1.8. Singletony 1.9. Klasy opakowujące typy proste 2. Ponowne wykorzystanie klas 2.1. Dziedziczenie 2.2. Konwersje referencyjne 2.3. Stwierdzanie typu 2.4. Dziedziczenie w Javie 2.5. Przedefiniowanie metod 2.6. Kowariancja typów wyników

2.7. Przedefiniowanie metod w wyliczeniach 2.8. Przedefiniowanie a wyjątki 2.9. Przedefiniowanie a przeciążanie, przesłanianie i pokrywanie 2.10. Adnotacja @override 2.11. Metody wirtualne i polimorfizm 2.12. Kompozycja 2.13. Kompozycja a dziedziczenie 2.14. Reguły ponownego wykorzystania klas 3. Wyjątki 3.1. Obsługa wyjątków 3.2. Zgłaszanie wyjątków 3.3. Ponowne zgłaszanie wyjątków 3.4. Niskopoziomowe przyczyny i łańcuchowanie wyjątków 3.5. Wykorzystanie informacji o śladzie stosu 4. Interfejsy i klasy wewnętrzne 4.1. Metody i klasy abstrakcyjne 4.2. Pojęcie interfejsu 4.3. Problem wielodziedziczenia 4.4. Definiowanie i implementowanie interfejsów 4.5. Interfejsy jako typy danych 4.6. Implementacja metod w interfejsach

4.7. Mixiny 4.8. Właściwości metod domyślnych 4.9. Prywatne metody w interfejsach 4.10. Pojęcie klasy wewnętrznej 4.11. Przykładowe zastosowanie klasy wewnętrznej 4.12. Anonimowe klasy wewnętrzne 4.13. Lokalne klasy wewnętrzne 5. Typy i metody sparametryzowane (generics) 5.1. Definiowanie typów sparametryzowanych Typy surowe i czyszczenie typów 5.2. Ograniczenia parametrów typu 5.3. Restrykcje 5.4. Metody sparametryzowane 5.5. Uniwersalne argumenty typu 6. Elementy programowania funkcyjnego w Javie 8 Przegląd pragmatyczny 6.1. O programowaniu funkcyjnym 6.2. Interfejsy na pomoc 6.3. Lambda-wyrażenia: pierwsze spotkanie 6.4. O gotowych interfejsach funkcyjnych 6.5. O przetwarzaniu strumieniowym 7. Lambda-wyrażenia 7.1. Interfejsy funkcyjne i lambda-wyrażenia

7.2. Składnia i cechy lambda-wyrażeń 7.3. Referencje do metod i konstruktorów 7.4. Gotowe interfejsy funkcyjne 7.5. Interfejsy z pakietu java.util.function a wyjątki kontrolowane 7.6. Lambda-wyrażenia i obiekty typu Optional 8. Proste narzędzia 8.1. Analiza składniowa tekstów i wyrażenia regularne 8.2. Uproszczenia stosowania wyrażeń regularnych w klasach String i Scanner 8.3. Działania na liczbach 8.4. Daty i czas 8.4.1. Tradycyjna klasa Calendar i operacje na datach 8.4.2. Daty i czas w Javie 8 – elementy nowego API 8.5. Formatowanie liczb i dat 8.6. Metody tablicowe 9. Kolekcje 9.1. Architektura kolekcji (JCF) Interfejsy i implementacje 9.2. Programowanie w kategoriach interfejsów 9.3. Ogólne operacje na kolekcjach 9.4. Operacje opcjonalne oraz wyjątki zgłaszane przez

metody kolekcyjne 9.5. Przekształcanie kolekcji. Kolekcje z tablic 9.6. Przykłady ogólnych operacji na kolekcjach 9.7. Iterowanie po kolekcjach 9.7.1. Tradycyjny iterator i rozszerzone for 9.7.2. Iteracje wewnętrzne 9.7.3. Spliteratory 9.7.4. Konkurencyjne modyfikacje 9.8. Listy 9.8.1. Podstawowe implementacje i operacje 9.8.2. Szczególne implementacje list: niuanse metody Arrays.asList(...) 9.8.3. Iteratory listowe 9.8.4. Przykłady operacji na listach 9.9. Kolejki 9.10. Zbiory typu HashSet, metody hashCode() i equals() 9.11. Porównywanie i porządkowanie elementów kolekcji 9.12. Zbiory uporządkowane i nawigowalne 9.13. Mapy 9.13.1. Wprowadzenie 9.13.2. Implementacje i interfejsy Ogólne

operacje na mapach 9.13.3. Iterowanie po mapach 9.13.4. Użycie domyślnych metod interfejsu Map 9.13.5. Sortowanie map 9.14. Algorytmy, widoki, fabrykatory kolekcji 9.15. Własne implementacje kolekcji 10. Przetwarzanie strumieniowe 10.1. Pojęcie strumienia Rodzaje i cechy operacji strumieniowych 10.2. Uzyskiwanie strumieni 10.3. Przegląd operacji na strumieniach 10.4. Filtrowanie i leniwość strumieni 10.5. Metoda forEach dla strumieni 10.6. Sortowanie strumieni 10.7. Redukcja 10.8. Proste kolektory 10.9. Kolektory budujące mapy 10.10. Generatory 10.11. Strumienie równoległe 10.12. Przykłady innych użytecznych metod 11. Wejście-wyjście 11.1. Programowanie wejścia-wyjścia: obraz ogólny

11.2. Abstrakcyjne strumienie wejścia-wyjścia Operacje elementarne 11.3. Strumieniowe klasy przedmiotowe 11.4. Instrukcja try-with-resources Automatyczne zarządzanie zasobami a obsługa tłumionych wyjątków 11.5. Strumieniowe klasy przetwarzające – przegląd 11.6. Buforowanie 11.7. Binarne strumienie wejścia-wyjścia 11.8. Kodowanie-dekodowanie przy użyciu strumieni wejścia-wyjścia 11.9. Serializacja obiektów 11.10. Potoki 11.11. Analiza składniowa strumieni – StreamTokenizer 11.12. Obiekty plikowe i klasa File 11.13. Wygodne metody klasy java.nio.file.Files 11.14. Skaner 11.15. Przeglądanie katalogów 11.16. Archiwa 11.17. Pliki o dostępie swobodnym 11.18. Nowe wejście-wyjście (NIO): przegląd 11.19. NIO: bufory 11.20. NIO: kanały i bufory. Kanały plikowe

11.21. Widoki buforów bajtowych 11.22. NIO: bufory – uporządkowanie bajtów (endianess) 11.23. NIO: bufory znakowe Kodowanie i dekodowanie 11.24. NIO: operacje kanałowe na wielu buforach (scattering i gathering) 11.25. NIO: mapowanie plików 11.26. NIO: bezpośrednie transfery kanałowe 12. Programowanie współbieżne i równoległe 12.1. Procesy i wątki 12.2. Uruchamianie równoległych działań Tworzenie wątków 12.3. Zadania i wykonawcy 12.4. Zamykanie wykonawców Oczekiwanie na zakończenie zadań i serwis kompletacji (CompletionService) 12.5. Zadania powtarzalne, opóźnione i okresowe 12.6. Wykonawcy a pule wątków 12.7. ForkJoinPool i zadania rekursywne 12.8. Kompletery typu CountedCompleter 12.9. CountedCompleter jako kontynuacja 12.10. Kiedy i jak używać zadań typu ForkJoinTask? Praktyczny przykład użycia kompleterów 12.11. Zadania kompletowalne (CompletableFuture)

12.12. Przerywanie zadań z zewnątrz i kończenie pracy wątków 12.13. Synchronizacja 12.14. Synchronizacja za pomocą jawnego ryglowania 12.15. Rygle do odczytu i zapisu: ReentrantReadWriteLock i StampedLock 12.16. Synchronizatory wyższego poziomu 12.17. Unikanie synchronizacji: struktura kodu, volatile, atomiki i konkurencyjne kolekcje 12.18. Koordynacja pracy wątków – mechanizm waitnotify 12.19. Koordynacja: warunki 13. Dynamiczna Java 13.1. Mechanizm refleksji 13.2. Uchwyty metod 13.3. Znaczenie refleksji – praktyczne przykłady 13.4. Refleksja a generics 13.5. Dynamiczne klasy proxy 13.6. JavaBeans 13.6.1. Koncepcja JavaBeans 13.6.2. Nasłuch i wetowanie zmian właściwości za pomocą obsługi zdarzeń 13.6.3. Introspekcja 13.7. Adnotacje

13.7.1. Istota adnotacji i sposoby ich definiowana 13.7.2. Przetwarzanie adnotacji w fazie wykonania 13.7.3. Przetwarzanie adnotacji w fazie kompilacji Transformowanie kodu bajtowego 13.8. Skrypty w Javie 14. Lokalizacja i internacjonalizacja aplikacji 14.1. Lokalizacje 14.2. Jeszcze trochę o formatorach liczbowych 14.3. Waluty 14.4. Strefy czasowe 14.5. Kalendarze i zlokalizowane daty 14.6. Porównywanie i sortowanie napisów 14.7. Internacjonalizacja aplikacji i dodatkowe zasoby (resource bundle) Literatura

Wstęp

W książce przedstawiono sposoby i techniki programowania użyteczne we wszelkich zastosowaniach języka Java. Materiał nie jest przeznaczony dla całkiem początkujących programistów Javy. Jednak choćby pobieżna orientacja w podstawach programowania w tym języku wystarczy, aby korzystając z materiału książki, znacznie pogłębić swoją wiedzę. Mając na względzie programistów niezbyt zaawansowanych w języku Java, każdy z poruszanych ważnych tematów, takich jak: • programowanie obiektowe i funkcyjne, • obsługa wyjątków, • przetwarzanie kolekcji, • programowanie operacji wejścia-wyjścia, • programowanie współbieżne i równoległe, • programowanie dynamiczne, • lokalizacja i internacjonalizacji aplikacji, jest omawiany od podstaw. Jednocześnie położono nacisk na kontekst praktyczny: jak i do czego stosować poszczególne narzędzia językowe? Szczególną uwagę skierowano na nowe elementy języka dostępne w wersji 8 i 9. Rozbudowane rozdziały o kolekcjach, wejściu-wyjściu, programowaniu współbieżnym i równoległym oraz programowaniu dynamicznym można traktować jako swoiste kompendia, które dają nie tylko orientację co do zestawu dostępnych uniwersalnych środków programowania, lecz także omawiają ważne szczegóły i niuanse ich stosowania. Będą one zapewne interesujące również dla Czytelników już zaawansowanych w programowaniu w Javie. Warto może też wymienić szczególnie ciekawe tematy omawiane w książce. Należą do nich: • statyczne i domyślne metody interfejsów oraz mixiny, a także szczegółowe przedstawienie gotowych implementacji takich metod w gotowych interfejsach (zwłaszcza kolekcyjnych); • możliwości i sposoby implementacji w interfejsach metod prywatnych (poczynając od Javy w wersji 9); • lambda-wyrażenia wraz z przeglądem gotowych interfejsów funkcyjnych i zastosowania ich domyślnych metod; • sposoby postępowania z wyjątkami kontrolowanymi w lambda-wyrażeniach; • obiekty typu Optional jako monady oraz ich zastosowanie; • spliteratory; • przetwarzanie strumieniowe (pakiet java.util.stream); • instrukcja try-with-resources jako ogólny sposób działania na dowolnych zasobach wymagających zamykania (z prezentacją koncepcji tłumionych wyjątków oraz rozszerzeń składniowych w Javie w wersji 9); • metody strumieniowego przetwarzania plików; • działania na katalogach za pomocą wizytorów, a także z użyciem przetwarzania strumieniowego; • działania na archiwach, w tym za pomocą systemu plikowego ZipFileSystem i metod interfejsu FileVisitor; • szczegółowe i obrazowe omówienie mechanizmu fork-join oraz koncepcji work-steeling;

• obiekty typu CountedCompleter jako kontynuacje oraz inne ich zastosowania; • zadania typu CompletableFuture (nowe w Javie 8) i ich praktyczne zastosowanie; • zasady i sposoby kończenia z zewnątrz równolegle wykonujących się zadań, w zależności od typu tych zadań; • wyspecjalizowane synchronizatory, w tym – nowe w Javie 8 – obiekty typu StampedLock; • zastosowanie refleksji oraz uchwytów metod (MethodHandle); • tworzenie dynamicznych klas i obiektów typu proxy; • zastosowania JavaBeans i delegacyjnego modelu obsługi zdarzeń zmian ich właściwości; • definiowanie i przetwarzanie adnotacji (w tym definiowanie procesorów adnotacji); • użycie skryptów z poziomu aplikacji Javy. Jak już wspomniano, lektura książki wymaga znajomości podstaw programowania w języku Java. Założono m.in., że Czytelnik jest świadomy znaczenia pojęcia referencji w Javie, dlatego są czasem stosowane uproszczone opisy elementów języka czy fragmentów kodu. Zamiast dość niezgrabnych (choć precyzyjnych) tekstów typu: „metodzie przekazujemy jako argument referencję do obiektu klasy String” lub „metoda zwraca referencję do obiektu klasy String”, są używane prostsze sformułowania, np. „metodzie przekazujemy napis, metoda zwraca napis”. Oczywiście, zawsze należy pamiętać o różnicy między obiektem a referencją do obiektu. Informacje na ten temat (a także omówienie innych ważnych podstaw języka) można znaleźć np. w książce autora Programowanie praktyczne od podstaw, wydanej przez WN PWN w 2014 roku. Prezentowane w niej bardziej zaawansowane tematy w obecnej publikacji zostały poszerzone i uzupełnione. W doborze treści i sposobu jej prezentacji autor wykorzystał swoje doświadczenia z prowadzenia wykładów i zajęć programistycznych w Polsko-Japońskiej Akademii Technik Komputerowych. Niektóre fragmenty książki powstały na ich podstawie. Praca jest przeznaczona dla Czytelników, którzy chcą samodzielnie rozwijać umiejętności programowania. Z powodzeniem może być też używana np. na jedno- lub dwusemestralnych kursach programowania na studiach wyższych. Krzysztof Barteczko Warszawa, sierpień 2015

ROZDZIAŁ 1 Klasy

Rozdział zawiera syntetyczne informacje o charakterystykach klas i ich składowych. Szczegółowo omówione zostały (nie zawsze intuicyjne) reguły inicjowania pól. Przy okazji przedstawiono ważne praktyczne narzędzia: enumeracje i klasy opakowujące typy pierwotne. Naszkicowane zostały też pewne sposoby programowania obejmujące m.in. klasy niemodyfikowalne i singletony.

1.1. Abstrakcja i hermetyzacja Java jest językiem obiektowym. Języki obiektowe posługują się pojęciem obiektu i klasy. Obiekt to konkretny lub abstrakcyjny byt, wyróżnialny w modelowanej rzeczywistości, mający określone granice i atrybuty (właściwości) oraz mogący świadczyć określone usługi, czyli wykonywać określone działania lub przejawiać określone zachowanie. Jak już wiemy, klasa opisuje wspólne cechy grupy podobnych obiektów. Klasa to opis takich cech grupy podobnych obiektów, które są dla nich niezmienne, np. zestaw atrybutów i metod, czyli usług, które mogą świadczyć. Przedstawione tu definicje[1] stanowią abstrakcyjne odzwierciedlenie cech rzeczywistości. Gdybyśmy mieli w języku programowania podobne pojęcia, moglibyśmy ujmować projekt rozwiązania rzeczywistego problemu i jego oprogramowanie w języku adekwatnym do problemu. I to zapewniają języki obiektowe. Jest to ich bardzo ważna cecha – zwana abstrakcją obiektową, znacznie ułatwiająca tworzenie oprogramowania. Programowanie polega na przetwarzaniu danych. Dane zawsze są określonych typów, a typ to nic innego jak rodzaj danych i działania, które można na nich wykonać. Z pragmatycznego punktu widzenie możemy więc powiedzieć, że klasa to typ, jej definicja opisuje właściwości typu danych (również funkcjonalne, tzn. dostępne operacje na danych tego typu). Języki obiektowe pozwalają na definiowanie własnych klas – własnych typów danych, co oznacza programowanie w języku problemu. O obiektach możemy myśleć jako o egzemplarzach określonych klas. Możemy mieć np. klasę pojazdów o następujących atrybutach: szerokość, wysokość, długość, ciężar, właściciel, stan (stoi, jedzie, zepsuty itp.) oraz udostępniających usługi: ruszania, zatrzymywania, zmiany właściciela (sprzedaż pojazdu) (zob. rys. 1.1).

Rys. 1.1. Początkowy schemat klasy Vehicle Zauważmy: dzięki abstrakcji obiektowej w programowaniu posługujemy się językiem zbliżonym do języka opisu rzeczywistego problemu. Oprócz odzwierciedlenia w programie „języka problemu” podejście obiektowe ma jeszcze jedną ważną zaletę: umożliwia zapewnienie, że atrybuty obiektów nie będą bezpośrednio dostępne poza klasą. W programie z obiektami „rozmawiamy” za pomocą wywołania na ich rzecz metod, obiekty same „wiedzą najlepiej”, jak zmieniać swoje stany. Dzięki temu nie możemy niczego nieopatrznie popsuć, co więcej – nie możemy zażądać od obiektu usługi, której on nie udostępnia. Dane (atrybuty) są ukryte i traktowane jako nierozdzielna całość z usługami. Nazywa się to hermetyzacją (lub enkapsulacją) i oznacza znaczne zwiększenie odporności programu na błędy. Sama koncepcja klasy jako zestawu pól i metod już zapewnia określony poziom hermetyzacji. Nie możemy np. do obiektów klasy Vehicle posłać komunikatu sing(), ponieważ metoda sing() nie występuje jako składowa w tej klasie. Dodatkowo języki obiektowe (w tym Java) pozwalają ukrywać dane (i metody) przed

powszechnym dostępem. Dostęp do składowych klasy regulują tzw. specyfikatory dostępu, których używamy w deklaracjach zmiennych, stałych i metod. Każda składowa klasy może być: • prywatna – dostępna tylko w danej klasie (specyfikator private); • zaprzyjaźniona – dostępna ze wszystkich klas danego pakietu; mówi się tu też o dostępie pakietowym lub domyślnym – domyślnym dlatego, że ten rodzaj dostępności występuje wtedy, gdy w deklaracji składowej nie użyjemy żadnego specyfikatora; • chroniona lub zabezpieczona – dostępna z danej klasy, wszystkich klas ją dziedziczących oraz z klas danego pakietu (specyfikator protected); • publiczna – dostępna zewsząd (specyfikator public). Funkcją prywatności jest zapewnienie: • ochrony przed zepsuciem (zazwyczaj pola powinny być prywatne; wyjątkiem są stałe) – użytkownik klasy nie ma dostępu do prywatnych pól i niczego nieświadomie nie popsuje; • właściwego interfejsu (metody „robocze” powinny być prywatne) – użytkownik klasy ma do dyspozycji tylko niezbędne (klarowne) metody, co ułatwia mu korzystanie z klasy; • ochrony przed konsekwencjami zmiany implementacji. Twórca klasy może zmienić zestaw i implementację prywatnych metod, nie zmieniając interfejsu publicznego: wszystkie programy napisane przy wykorzystaniu tego interfejsu nie będą wymagały żadnych zmian. Mamy też w Javie pojęcia klas publicznych i pakietowych. Klasa pakietowa jest dostępna tylko z klas pakietu. Klasa publiczna jest dostępna zewsząd (z innych pakietów). Klasę publiczną deklarujemy ze specyfikatorem public. Oprócz tego w Javie mamy specjalny rodzaj klas zwanych wyliczeniami (enumeracjami).

1.2. Enumeracje Enumeracja (wyliczenie) to nic innego jak konkretny zbiór nazwanych stałych. Posługując się tylko typem int, możemy np. wprowadzić coś w rodzaju wyliczenia pór roku: final int ZIMA = 0, WIOSNA = 1, LATO = 2, JESIEN = 3; i używać ich w programie: int poraRoku = LATO; Takie podejście ma jednak wady: • Brak kontroli poprawności danych – nic nie stoi na przeszkodzie, aby zmiennej poraRoku nadać nonsensowną wartość 111. • Jeżeli zmienimy wyliczenie (np. dodamy stałe i dotychczasowe wartości stałych się

zmienią, np. dodamy PRZEDWIOSNIE z wartością 1, a wartości WIOSNA, LATO, JESIEN zmienimy na 2, 3 , 4 ), to inne fragmenty kodu, już korzystające z tych stałych, nie będą prawidłowo działać. • Wprowadzanie i wyprowadzanie informacji o wartościach jest żmudne (konwersja String – int), proste wyświetlenie wartości jest całkowicie „nieinformacyjne”: println(ZIMA) da nam na wyjściu 0 (a co to?). • Wyliczenie definiowane za pomocą typu int nie ma właściwości programowania obiektowego (nie ma odrębnego typu, który by zawężał zbiór dopuszczalnych wartości i możliwych operacji na nich). W Javie usunięto te wady, pozostawiając jednocześnie zaletę efektywności działania (tak jak na liczbach całkowitych), wprowadzając specjalny rodzaj typów referencyjnych oznaczanych słowem kluczowym enum. Definicja typu wyliczeniowego polega na umieszczeniu po słowie enum w nawiasach klamrowych elementów wyliczenia rozdzielonych przecinkami: [ public] enum NazwaTypu { elt1, elt2, ..., eltN } gdzie: elt – elementy wyliczenia. Na przykład: enum Pora { ZIMA, WIOSNA, LATO, JESIEŃ } Tutaj Pora jest nazwą typu wyliczeniowego, a ZIMA, WIOSNA, LATO, JESIEŃ oznaczają stałe tego typu (zbiór nazwanych stałych). Zmienna zadeklarowana jako: Pora p; będzie mogła przyjmować wartości: Pora.ZIMA, Pora.WIOSNA, Pora.LATO, Pora.JESIEŃ (oraz null, ponieważ jest typu referencyjnego). Zauważmy, że wartości te to stałe statyczne i są one typu referencyjnego Pora. Wobec danych typów wyliczeniowych możemy stosować różne metody, m.in.: • Zastosowanie metody toString() wobec zmiennej typu wyliczeniowego zwróci jej wartość w postaci napisu (w naszym przykładowym przypadku może to być "ZIMA"); umożliwia to eleganckie i łatwe pokazywanie wartości typu wyliczeniowego, np. p =

Pora.ZIMA; System.out.println(p); wyświetli na konsoli napis ZIMA. • Zastosowanie statycznej metody valueOf(String s) zwróci wartość typu wyliczeniowego odpowiadającą podanemu jako argument napisowi, np. Pora.valueOf. • ("LATO") zwróci wartość Pora.LATO. • Metoda ordinal() zastosowana wobec zmiennej typu wyliczeniowego zwraca pozycję w wyliczeniu zajmowaną przez wartość tej zmiennej, przy czym pozycje numerowane są od 0 (np. po Pora p = Pora.WIOSNA; p.ordinal() zwróci 1). • Metoda statyczna values() zwraca zestaw wszystkich wartości danego typu wyliczeniowego (w naszym przykładzie zestaw ten składa się z elementów Pora.ZIMA, Pora.WIOSNA, Pora.LATO, Pora.JESIEŃ; będzie to tablica). • Do porównania wartości typów wyliczeniowych pod względem równości wystarczy operator == (tu przy porównaniach zawsze wchodzi w grę tylko identyczność obiektów, na które wskazują referencje).

Rys. 1.2. Wynik działania kodu 1.1 Przykładowy program umożliwia podanie nazwy pory roku i opisuje ją w okienku komunikatów (zob. kod 1.1). Po podaniu w dialogu wejściowym np. napisu "LATO" uzyskamy wynik jak na rys. 1.2: import javax.swing.JOptionPane; public class PoryRoku { enum Pora { ZIMA, WIOSNA, LATO, JESIEŃ }; public static String opisz(Pora p) { int nr = p.ordinal() + 1; String txt = p + " (ma numer " + nr + ")";

if (p == Pora.ZIMA) return txt + if (p == Pora.WIOSNA) return txt if (p == Pora.LATO) return txt + if (p == Pora.JESIEŃ) return txt return "To niemożliwe";

" + " +

" "

pada śnieg."; - kwitnie bez."; jest ciepło."; - pada deszcz.";

} public static void main(String[] args) { String nazwa = JOptionPane.showInputDialog("Podaj porę roku:"); Pora p = Pora.valueOf(nazwa); JOptionPane.showMessageDialog(null, opisz(p) ); } } Kod 1.1. Zastosowanie enumeracji Zwróćmy uwagę, że w powyższym przykładzie musieliśmy pisać Pora.LATO, Pora.ZIMA itd. W prostych przypadkach, gdy nie ma możliwości kolizji nazw, byłoby zapewne lepiej używać skróconych nazw (LATO, ZIMA), bo są one w takim kontekście perfekcyjnie jasne. Jednak aby to zrobić, trzeba statycznie importować nazwy z wyliczenia Pora, a statyczny import możliwy jest tylko z klas umieszczonych w nazwanych pakietach. Możemy więc zrobić tak, jak pokazuje kod 1.2. package enums; import static enums.Pora.*; enum Pora { ZIMA, WIOSNA, LATO, JESIEŃ }; public class PoryRoku1 { public static void main(String[] args) { Pora p = ZIMA; System.out.println(p); // wyprowadzi napis ZIMA } } Kod 1.2. Użycie niekwalifikowanych nazw elementów wyliczenia Tu jednak definicję wyliczenia Pora musieliśmy umieścić poza klasą PoryRoku1. Czy przeczy to zasadzie, że w plikach źródłowych Javy poza deklaracjami pakietu i importów mogą być tylko definicje klas? Ależ skąd. Wyliczenia (enum) to tak naprawdę również klasy, tylko o pewnych specjalnych właściwościach. A skoro tak, to można z nimi postępować prawie identycznie jak z innymi klasami, czyli np. umieszczać w odrębnych plikach, definiować dodatkowe pola, konstruktory i metody. A jak wywołać konstruktor? Cóż, w przypadku enum nie ma możliwości użycia wyrażenia new. Konstruktor jest wołany przez użycie nawiasów

okrągłych przy definiowaniu elementów wyliczenia. Korzystając z tych możliwości, nieco rozbudujemy wyliczenie pór roku, umieszczając je w nazwanym pakiecie i odrębnym pliku źródłowym (kod 1.3). package enums; public enum PoraRoku { ZIMA("zimno jest, może padać śnieg", 5), WIOSNA("zakwita roślinnośc", 2), LATO("może być ciepło, ale niekoniecznie", 3), JESIEŃ("jest chłodno, często pada descz", 2); private String opis; private int liczbaMies; private PoraRoku(String opis, int liczbaMies) { this.opis = opis; this.liczbaMies = liczbaMies; } public String getOpis() { return opis; } public int getLiczbaMies() { return liczbaMies; } } Kod 1.3. Enum z konstruktorem i metodami Przy opracowaniu tego wyliczenia dla każdego jego elementu wołany jest konstruktor, co skutkuje inicjowaniem pól opis i liczbaMies. Zwróćmy uwagę, że konstruktor jest prywatny, co oznacza, że nie można go wywołać spoza tej klasy (enum). W ten sposób zabronione jest tworzenie wyliczeń za pomocą wyrażenia new. Kod 1.4 przedstawia wykorzystanie tak zdefiniowanego wyliczenia PoraRoku. package enums; import static enums.PoraRoku.*; import javax.swing.*; public class PoryRoku2 { public static void main(String[] args) { String nazwa = JOptionPane.showInputDialog("Podaj porę roku:"); PoraRoku p = valueOf(nazwa); System.out.println(p + " trwa miesięcy " +

p.getLiczbaMies() + ", " + p.getOpis()); System.out.println(JESIEŃ + " i " + ZIMA + " trwają miesięcy " + (JESIEŃ.getLiczbaMies() + ZIMA.getLiczbaMies())); } } Kod 1.4. Przykład zastosowania enum z metodami Wynik działania programu po wprowadzeniu w dialogu wejściowym słowa LATO pokazuje listing 1.1. LATO trwa miesięcy 3, może być ciepło, ale niekoniecznie JESIEŃ i ZIMA trwają miesięcy 7 Listing 1.1. Wynik działania kodu 1.4 Wyliczenia mają jeszcze inne dodatkowe właściwości, związane abstrakcyjnymi i przedefiniowaniem metod, o czym będzie mowa w rodz. 2.7.

z

metodami

1.3. Definiowanie klas Stosując regułę ukrywania danych i specyfikatory dostępu, możemy teraz przedstawić przykładową definicję klasy Person (zob. kod 1.5), a następnie definicję klasy Vehicle. public class Person { private String name; private String pesel; Person(String aname, String id) { name = aname; pesel = id; } public String getName() { return name; } public String getPesel() { return pesel; } } Kod 1.5. Przykładowa definicja klasy Person w myśl reguł hermetyzacji Atrybuty obiektów klasy Person przedstawiono jako pola prywatne. Spoza klasy nie ma do

nich dostępu. Podczas tworzenia obiektu jego elementy odpowiadające tym polom są inicjowane za pomocą wywołania konstruktora. Później zmiany tych elementów danych nie są już możliwe, możemy tylko uzyskać dane za pomocą publicznych metod getName()i getPesel(). Modyfikując i rozbudowując klasę Vehicle w myśl reguł hermetyzacji (zob. kod 1.6), osiągniemy następujące rezultaty: • Uczynimy atrybuty pojazdów prywatnymi. • Możliwe stany pojazdu (stoi, jedzie, zepsuty) powinny stanowić publicznie dostępne atrybuty, wspólne dla szystkich obiektów klasy; wygodnie będzie tu użyć – zamiast typu public static int – publicznego wyliczenia (enum): package hermet; public enum VehState { BROKEN, STOPPED, MOVING } • Dostarczymy – jako publicznego – tylko niezbędnego zestawu metod (przy okazji do poprzedniegi projektu klasy Vehicle dodamy metodę crash() symulującą zderzenie pojazdów). • Ukryjemy przed powszechnym dostępem „robocze” metody: setState(), zmieniającą stan obiektu, i error(), zgłaszającą błędy. package hermet; import static hermet.VehState.*; public class Vehicle { private int width, height, length, weight; private Person owner; private VehState state; public Vehicle(int w, int h, int l, int ww) { this(null, w, h, l, ww); } public Vehicle(Person p, int w, int h, int l, int ww) { owner = p; width = w; height = h; length = l; weight = ww; state = STOPPED; } public void start() { setState(MOVING); } public void stop() { setState(STOPPED);

} public void repair() { if (state != BROKEN) error("Nie można reperować sprawnego pojazdu"); else state = STOPPED; } public VehState getState() { return state; } public void sellTo(Person p) { owner = p; } // dodana metoda crash() symulująca zderzenie samochodów public void crash(Vehicle v) { if (this.state != MOVING && v.state != MOVING) error("Żaden z pojazdów nie jest w ruchu - nie ma zderzenia"); this.state = BROKEN; v.state = BROKEN; } public String toString() { String s = (owner == null ? "sklep" : owner.getName()); return "Pojazd, właścicielem którego jest " + s + " jest w stanie " + state; } private void setState(VehState newState) { if (state == BROKEN) error("Nie jest możliwe przejście ze stanu " + state + " do stanu " + newState); else state = newState; } protected void error(String msg) { throw new IllegalArgumentException(msg); } } Kod 1.6. Definicja klasy Vehicle w myśl reguł hermetyzacji Zwróćmy uwagę, że: • Atrybuty pojazdu (wysokość, waga itd.) nadawane są przy tworzeniu obiektu i żadna inna

klasa nie może ich później zmienić. • Stan pojazdu jest zmieniany tylko na skutek wywołania publicznych metod start(), stop(), repair(), przy czym sam obiekt dba, by nie doszło do niewłaściwych zmian (zob. definicje tych metod oraz prywatnej metody setState(...)). • Metoda setState() jest prywatna – nie dopuszczamy możliwości arbitralnej zmiany stanu pojazdu spoza klasy, a procedurę zmiany stanu wyróżniliśmy w tej prywatnej metodzie, by oszczędzić na kodowaniu (ten sam kod wołamy w kilku miejscach). • Dla oszczędności kodowania została również wprowadzona robocza metoda error(), wołaną w kilku miejscach dla zgłoszenia błędów; nadano jej specyfikator dostępu protected (a nie private), żeby mogła być używana w klasach dziedziczących Vehicle.

1.4. Wykorzystanie składowych statycznych Składowe klasy mogą być statyczne i niestatyczne. Niestatyczne zawsze wiążą się z istnieniem jakiegoś obiektu (pola odpowiadają elementom obiektu, metody muszą być wywoływane na rzecz obiektu, są komunikatami do obiektu). Składowe statyczne (pola i metody) są wspólne dla wszystkich obiektów oraz: • są deklarowane przy użyciu specyfikatora static, • mogą być używane nawet wtedy, gdy nie istnieje żaden obiekt klasy. Uwaga. Ze statycznych metod nie wolno odwoływać się do niestatycznych składowych klasy (obiekt może nie istnieć). Możliwe są natomiast odwołania do innych statycznych składowych. Spoza klasy do jej statycznych składowych możemy odwoływać się na dwa sposoby: • NazwaKlasy.NazwaSkładowej, • gdy istnieje jakiś obiekt: tak samo jak do niestatycznych składowych (ale jest to mylące i niepolecane).

Skorzystamy z koncepcji składowych statycznych po to, by każdemu pojazdowi w naszym programie nadawać unikatowy numer (np. od jednego), a także zawsze mieć rozeznanie, ile obiektów typu Vehicle dotąd utworzyliśmy: public class Vehicle { private static int count; // ile obiektów dotąd utworzyliśmy private int currNr; // bieżący numer pojazdu //...

public Vehicle(Person p, int w, int h, int l, int ww) { // .... // Każde utworzenie nowego obiektu zwiększa licznik o 1 // bieżąca wartość licznika nadawana jest jako numer pojazdu // numer pojazdu jest niestatycznym polem klasy, a więc elementem obiektu currNr = ++count; } //..... // zwraca unikatowy numer pojazdu public int getNr() { return currNr; } // zwraca liczbę dotąd utworzonych obiektów // metoda jest statyczna, by móc zwrócić 0 // gdy nie ma jeszcze żadnego obiektu public static int getCount() { return count; }

1.5. Przeciążanie metod i konstruktorów W klasie (i/lub jej klasach pochodnych) możemy zdefiniować metody o tej samej nazwie, ale różniące się liczbą i/lub typami parametrów. Nazywa się to przeciążaniem metod. Po co istnieje taka możliwość? Wyobraźmy sobie, że na obiektach klasy par liczb całkowitych (znanej nam z poprzednich rozdziałów) chcielibyśmy wykonywać operacje: • dodawania innych obiektów-par, • dodawania (do składników pary) kolejno dwóch podanych liczb całkowitych, • dodawania (do każdego składnika pary) tej samej podanej liczby całkowitej. Gdyby nie było przeciążania metod, musielibyśmy dla każdej operacji wymyślać inną nazwę metody. A przecież istota operacji jest taka sama (więc wystarczy nazwa add), a jej użycie powinno być jasne z kontekstu (określanego przez argumenty). Dzięki przeciążaniu można w klasie Para zdefiniować np. metody: void add(Para p) metodę, parę

// dodaje do pary, na rzecz której wywołano

// podaną jako argument void add(int i) // do obu składników pary dodaje podaną liczbę void add(int i, int k) // pierwszą podaną liczbę dodaje do pierwszego składnika // pary a drugą – do drugiego i użyć gdzie indziej w naturalny sposób:

Para p;. Para jakasPara; .... p.add(3); // wybierana jest ta metoda, która pasuje (najlepiej) do argumentów p.add(1,2); p.add(jakasPara); Innym przykładem przeciążonej metody jest println(...). Ma ona bardzo wiele wersji – z argumentami różnych typów (m.in. wszystkich prostych, String i Object). I bardzo dobrze, bo w przeciwnym przypadku musielibyśmy pisać np. printInt(3) i printString("Ala"), aby wyświetlić odpowiednio liczbę całkowitą i napis. Identyfikatory metod definiowanych w klasie muszą być od siebie różne. Wyjątkiem są metody przeciążone, tj. takie, które mają tę samą nazwę (identyfikator), ale różne typy i/lub liczbę argumentów. Z przeciążaniem metod związany jest pewien problem. Otóż wywołanie do odpowiedniej wersji metody dopasowuje kompilator, na podstawie liczby i typów argumentów. Musimy przy tym uważać, bo kiedy liczba parametrów w różnych wersjach metody przeciążonej jest taka sama, a ich typy zbliżone – to może się okazać, że źle interpretujemy działanie programu. Na przykład co się stanie, jeśli mamy dwie metody o nazwie show, pierwsza z parametrem typu short, a druga z parametrem typu int, i wywołujemy metodę show z argumentem typu char? Powiedzieliśmy przed chwilą, że zostanie wywołana metoda, której parametry najlepiej pasują do argumentów wywołania. Ponieważ typ short jest „bliższy” typowi char niż typ int, mogłoby się wydawać, że zostanie wywołana metoda show(short). Tymczasem – jak wiemy – wykonywana jest promocja argumentu typu char do typu int i będzie wywołana metoda show(int). Jeżeli nie znamy dobrze mechanizmów automatycznych konwersji, to w metodach przeciążonych specyfikujmy różną liczbę parametrów lub całkowicie różne typy parametrów. Ogólnie, algorytm wyboru przez kompilator odpowiedniej metody przeciążonej jest dość skomplikowany i może mieć zaskakujące, nieintuicyjne konsekwencje (zob. specyfikację języka Java [10]). Również z tego powodu należy wyraźnie różnicować liczbę lub typy argumentów. Zwróćmy też uwagę, że przeciążanie „rozciąga się” na różne rodzaje metod. Dwie metody – statyczna i niestatyczna – o tej samej nazwie, ale o różnych typach lub liczbie argumentów są przeciążone. Podobnie jak metody możemy przeciążać konstruktory. Znaczy to, że w jednej klasie możemy mieć kilka wersji konstruktorów z różnymi parametrami. W ten sposób udostępniamy różne sposoby inicjowania obiektów klasy. W takim przypadku, by nie powtarzać wspólnego kodu w różnych konstruktorach, wygodna okazuje się możliwość wywoływania konstruktora z innego konstruktora. Do takiego wywołania stosujemy słowo kluczowe this, co ilustruje poniższy fragment składni:

Vehicle(int w, int h, int l, int ww) { this(null, w, h, l, ww); } Vehicle(Person p, int w, int h, int l, int ww) { owner = p; width = w; height = h; length = l; weight = ww; state = STOPPED; } Konstrukcja this(...) powinna być pierwszą instrukcją w ciele konstruktora.

1.6. Klasy i obiekty niezmienne Wróćmy jeszcze na chwilę do definicji klasy Person. Jak widzieliśmy – w ramach naszej aplikacji – jej obiekty po utworzeniu nie mogą zmieniać swoich stanów (w tym przypadku wartości pól name i pesel). Takie niezmienne (immutable) klasy odgrywają ważną rolę w programowaniu, bo m.in: • gwarantują, że ich obiekty po dodaniu do zbiorów i do map (jako klucze map) nigdy nie zmienią swoich stanów, co – jak pamiętamy – jest warunkiem, aby zbiory oraz klucze w mapach zachowywały spójność (oczywiście w pokazanej tu klasie Person trzeba by było jeszcze zdefiniować metody equals() i hashCode(), tak by obiekty tej klasy mogły być dodawane do HashSet lub występować jako klucze w HashMap); • żadna metoda, uzyskując taki obiekt jako argument, nie może zmienić jego stanów, a zatem unikamy przypadkowych błędów; • posługiwanie się nimi w programowaniu współbieżnym nie wymaga synchronizacji, która zwykle jest trudnym zadaniem (zob. rozdział o programowaniu współbieżnym). Pokazana klasa Person nie jest absolutnie niezmienna. Zgodnie z zasadami powinniśmy w niej jeszcze w kilku miejscach użyć słowa kluczowego final: final public class Person { // tu final zabrania dziedziczenia klasy Person // a tu przez final zabezpieczamy się przed błędami w kodzie klasy: private final String name; private final String pesel; // ... } Stosowanie klas niezmiennych znacznie ułatwia programowanie, ale oczywiście nie zawsze możemy to robić. Na przykład w przypadku klasy Vehicle jej zmienność (zmienne stany pojazdów: stoi, jedzie itp.) wydaje się naturalnym rozwiązaniem.

1.7. Inicjowanie Podczas tworzenia obiektu: • pola klasy mają zagwarantowane inicjowanie na wartości ZERO (0, false – dla typu boolean, null – dla referencji); • zwykle w konstruktorze dokonuje się reinicjacji pól; • ale można również posłużyć się jawną inicjacją podczas deklaracji pól. W trakcie tworzenia obiektu klasy Vehicle (new Vehicle(...) – por. kod 1.6) elementy nowego obiektu, odpowiadające polom klasy, uzyskają wartości 0 (dla elementów typu int) oraz null dla elementu odpowiadającego referencjom (owner i state). Następnie zostanie wywołany konstruktor, w którym dokonujemy właściwego inicjowania. Można by było napisać np.: private VehState state = STOPPED; // jawna inicjacja pola i usunąć z konstruktora instrukcję state = STOPPED. Z inicjowaniem wiąże się również pojęcie bloków inicjacyjnych. W Javie w zasadzie nie można używać instrukcji wykonywalnych (m.in. sterujących) poza ciałami metod i konstruktorów. Od tej zasady są jednak dwa wyjątki: • użycie niestatycznego bloku inicjacyjnego, • użycie statycznego bloku inicjacyjnego. Niestatyczny blok inicjacyjny wprowadzamy, ujmując kod wykonywalny w nawiasy klamrowe i umieszczając taką konstrukcję w definicji klasy poza ciałem jakiejkolwiek metody (czy konstruktora). Kod bloku zostanie wykonany zaraz po utworzeniu i domyślnym zainicjowaniu obiektu, przed wywołaniem jakiegokolwiek konstruktora. Taka możliwość może się okazać przydatna, gdy mamy kilka konstruktorów i chcemy wyróżnić pewien kod, który będzie inicjował obiekt niezależnie od użytego konstruktora i przed użyciem któregokolwiek z nich. Na przykład możemy w bloku inicjacyjnym wyodrębnić nieco bardziej zaawansowaną postać inicjacji zmiennej state, opisującej stan pojazdu (zob. kod 1.7). // ... niezbędne importy public class Vehicle { // ... private VehState state; // // // // // //

Niestatyczny blok inicjacyjny -- w niedzielę wszystkie samochody inicjalnie stoją -- w poniedziałek te o parzystych numerach inicjalnie jadą, inne - stoją -- w pozostałe dni tygodnia: wszystkie jadą

{ // początek bloku int dayOfWeek = Calendar.getInstance().get(Calendar.DAY_OF_WEEK); switch (dayOfWeek) { case Calendar.SUNDAY : state = STOPPED; break; case Calendar.MONDAY : state = (currNr % 2 == 0 ? MOVING : STOPPED); break; default : state = MOVING; break; } } // koniec bloku // Konstruktory i metody klasy public Vehicle(...) { // ... } // ... } Kod 1.7. Niestatyczny blok inicjacyjny W pokazanym przypadku takie wyodrębnienie niestatycznego bloku inicjacyjnego ma pewien sens (wtedy kod ew. kilku konstruktorów jest może bardziej czytelny), ale zazwyczaj nie jest to zbyt interesujące rozwiązanie. Wyjątkiem jest zaawansowane inicjowanie pól wprowadzanych w anonimowej klasie wewnętrznej (w tym przypadku niestatyczny blok inicjacyjny zastępuje konstruktor, którego przecież nie można zdefiniować, bo klasa nie ma nazwy – zob. dalej o anonimowych klasach wewnętrznych). Ogólnie ciekawszym rozwiązaniem wydaje się statyczny blok inicjacyjny. Czasem pojawia się potrzeba wykonania jakiegoś kodu jeden raz, przy pierwszym odwołaniu do klasy. Przy inicjowaniu pól statycznych możemy skorzystać z dowolnych wyrażeń składających się ze zmiennych i stałych statycznych, oraz z wywołań statycznych metod, ale – oczywiście – nie sposób użyć instrukcji wykonywalnych (np. sterujących). Statyczny blok inicjacyjny wprowadzamy słowem kluczowym static z następującym po nim kodem ujętym w nawiasy klamrowe. Kod ten będzie wykonywany jeden raz przy pierwszym odwołaniu do klasy (np. użyciu metody statycznej lub utworzeniu pierwszego obiektu). Oczywiście z takiego bloku możemy odwoływać się wyłącznie do zmiennych statycznych (obiekt jeszcze nie istnieje). Przykład. Wyobraźmy sobie, że pojazdy są numerowane w kolejności ich tworzenia. Aby to osiągnąć, wprowadzimy w klasie Vehicle pole statyczne initNr (początkowy numer, od którego zaczyna się numeracja pojazdów) oraz pole currNr – bieżący numer pojazdu: class Vehicle { // ... private static int initNr; //... private int currNr = ++initNr; }

Przy pierwszym odwołaniu do klasy zmienna initNr uzyskuje wartość 0, a przy każdym tworzeniu obiektu jej wartość zwiększa się o 1 i zostaje przypisana do zmiennej currNr (kolejny utworzony pojazd uzyskuje numer o 1 większy od poprzednio utworzonego). Załóżmy dalej, że początkowe numery pojazdów zależą od domyślnej lokalizacji (ustawień regionalnych) aplikacji (ustawienia takie są inne dla Stanów Zjednoczonych, a inne dla, powiedzmy, Włoch). Będziemy więc inicjować zmienną statyczną initNr w zależności od lokalizacji (w tym przypadku oczywiście initNr nie oznacza liczby obiektów utworzonych w programie, ale początkowy numer pojazdu). Taki problem można rozwiązać właśnie za pomocą statycznego bloku inicjacyjnego (zob. kod 1.8). public class Vehicle { private static int initNr; // Statyczny blok inicjacyjny // za jego pomocą inicjujemy zmienną initNr w taki sposób, // by numery pojazdów zaczynały się w zależności // od domyślnej lokalizacji aplikacji // np. jeśli aplikacja jest wykonywana w lokalizacji włoskiej, // numery zaczynają się od 10000. static { Locale[] loc = { Locale.UK, Locale.US, Locale.JAPAN, Locale.ITALY, }; int[] begNr = { 1, 100, 1000, 10000, }; initNr = 200; // jeżeli aplikacja działa w innej lokalizacji niż wymienione // w tablicy, zaczynamy numery od 200 Locale defLoc = Locale.getDefault(); // jaka jest domyślna lokalizacja? for (int i=0; i < loc.length; i++) if (defLoc.equals(loc[i])) { initNr = begNr[i]; break; } }// koniec bloku // pola niestatyczne private int currNr = ++count; private int width, height, length, weight; // ... // Konstruktory i metody // ... // zmieniona metoda toString() pokazuje numer pojazdu.

public String toString() { String s = (owner == null ? "sklep" : owner.getName()); return "Pojazd " + currNr + ", którego właścicielem jest " + s + " jest w stanie " + state; } } // koniec klasy Kod 1.8. Statyczny blok inicjacyjny Uwaga. Obiekty klasy Locale z pakietu java.util oznaczają tzw. lokalizacje (ustawienia regionalne). W kodzie dostarczamy tablicę lokalizacji loc inicjowaną statycznymi stałymi klasy Locale. Metoda getDefaultLocale() zwraca aktualne systemowe ustawienia regionalne (aktualną lokalizację). Poczynając od Javy 7, są to ustawienia związane z graficznym interfejsem systemu (czyli np. w amerykańskiej wersji Windows nawet przy polskich ustawieniach języka, klawiatury itp. zostanie zwrócone Locale.US). Szczegóły podano w rozdz. 14. Ogólnie inicjatory pól statycznych oraz statyczne bloki inicjacyjne nazywają się inicjatorami statycznymi, a niestatyczne bloki inicjacyjne i inicjatory niestatycznych pól – inicjatorami niestatycznymi. Niezwykle ważne są podane tu reguły inicjowania: •

Inicjowanie klasy powoduje jednokrotne zainicjowanie elementów statycznych, tzn. najpierw wszystkie pola statyczne uzyskują wartości domyślne, a następnie wykonywane są inicjatory statyczne w kolejności ich występowania w klasie. Inicjowanie klasy następuje w wyniku jej załadowania przez JVM, co może się zdarzyć np. przy uruchomieniu głównej klasy programu lub pierwszym odwołaniu z programu do klasy, np. na skutek wywołania jej metody statycznej lub utworzenia obiektu (np. Vehicle v – new Vehicle(...)). • Tworzenie każdego obiektu (new) powoduje nadanie niestatycznym polom klasy wartości domyślnych, następnie wykonanie inicjatorów niestatycznych w kolejności ich występowania w klasie, po czym wykonywany jest konstruktor. • W inicjatorach statycznych można się odwoływać do wszystkich statycznych metod klasy, ale tylko do tych statycznych pól, których deklaracje poprzedzają inicjator. • W inicjatorach niestatycznych można się odwoływać do wszystkich metod klasy, do wszystkich pól statycznych (niezależnie od miejsca ich występowania), ale tylko do tych pól niestatycznych, których deklaracje poprzedzają inicjator. • W konstruktorze można się odwoływać do wszystkich metod i pól klasy. Reguł inicjowania nie należy lekceważyć, bo niewłaściwa kolejność inicjatorów w kodzie może prowadzić do subtelnych błędów. Kod 1.9 ułatwia zrozumienie powyższych reguł. public class InitOrder { private static int s = 100;

private static final int C; private int a = 1; InitOrder() { report("Konstruktor: s, C, a, b mają wartości :", s, C, a, b); } private int b = 2; { report("Blok inicjacyjny: s, C, a =", s, C, a); } static { report("Statyczny blok inicjacyjny, s, C = ", s, C); C = 101; // opóźnione inicjowanie stałej! } private static void report(String msg, int ... args ) { System.out.print(msg + " "); for (int i : args) { System.out.print(i + " "); } System.out.println(); } public static void main(String[] args) { report("Wywołanie metody main"); new InitOrder(); } } Kod 1.9. Kolejność inicjowania Kod 1.9 wyświetli w wyniku: Statyczny blok inicjacyjny, zmienna s = 100 Wywołanie metody main Blok inicjacyjny: s, C, a = 100 101 1 Konstruktor: s, C, a, b mają wartości : 100 101 1 2 Gdybyśmy jednak w kodzie 1.9 dodali na początku klasy pole statyczne inicjowane referencją do nowo tworzonego obiektu tej klasy: public class InitOrder {

private static InitOrder iord = new InitOrder(); private static int s = 100; private static final int C; private int a = 1; private int b = 2; // .. } wynik wyglądałby tak: Blok inicjacyjny: s, C, a = 0 0 1 Konstruktor: s, C, a, b mają wartości : 0 0 1 2 Statyczny blok inicjacyjny, s = 100 Wywołanie metody main Blok inicjacyjny: s, C, a = 100 101 1 Konstruktor: s, C, a, b mają wartości : 100 101 1 2 W tym przypadku niestatyczny blok inicjacyjny oraz konstruktor są wywoływane przed blokiem statycznym, ale nie przeczy to podanym regułom inicjowania. Faktycznie: najpierw wszystkie pola statyczne uzyskują wartości domyślne (zera), następnie inicjowane jest pierwsze pole statyczne iord. Tutaj inicjowanie polega na opracowaniu wyrażenia new InitOrder(). W nowo utworzonym przez nie obiekcie niestatyczne pola klasy uzyskują domyślne wartości (zera), następnie wykonywane są niestatyczne inicjatory i wołany jest konstruktor. Inne inicjatory statyczne klasy InitOrder jeszcze nie zostały wykonane, więc zarówno niestatyczny blok inicjacyjny, jak i konstruktor raportują zerowe (domyślne) wartości pól statycznych s i C. Zwróćmy uwagę na jeszcze jeden niuans. Gdyby pole statyczne S zadeklarować jako final: private static InitOrder iord = new InitOrder(); private static final int s = 100; private static final int C; początkowe wiersze wydruku wynikowego byłyby trochę inne: Blok inicjacyjny: s, C, a, b = 100 0 1 2 Konstruktor: s, C, a, b mają wartości : 100 0 1 2 Dzieje się tak dlatego, że mamy tu do czynienia z wyrażeniem stałym, opracowywanym w fazie kompilacji (stała S uzyskuje wartość już w fazie kompilacji). Ale dlaczego stała C nadal ma wartość 0? To zrozumiałe, jej inicjowanie zostało opóźnione i stała ta uzyskuje wartość (101) w inicjacyjnym bloku statycznym, który się jeszcze nie wykonał. W sytuacji dziedziczenia klas (to pojęcie zostanie szczegółowo omówione w następnym rozdziale) sekwencja działań przy inicjowaniu jest następująca: • Najpierw wykonywane są inicjatory statyczne w kolejności klas w hierarchii dziedziczenia i kolejności ich występowania w poszczególnych klasach.

• Następnie wykonywane są inicjatory niestatyczne oraz wywoływane konstruktory w kolejności klas w hierarchii dziedziczenia. A zatem przy takiej strukturze: class A { static { ... } static int a = ... int b = ... { ... } public A() { } } class B extends A { static { ... } static int a = ... int b = ... { ... } public B() { } } po wykonaniu new B() uzyskamy następującą kolejność inicjowania: Statyczny blok inicjacyjny klasy A Inicjacja pola statycznego klasy A Statyczny blok inicjacyjny klasy B Inicjacja pola statycznego klasy B inicjacja pola niestatycznego klasy Niestatyczny blok inicjacyjny klasy Konstruktor klasy A inicjacja pola niestatycznego klasy Niestatyczny blok inicjacyjny klasy Konstruktor klasy B

A A B B

Przy dziedziczeniu należy również pamiętać o następujących kwestiach: • Wywołanie konstruktora nadklasy z konstruktora klasy pochodnej odbywa się za pomocą super(arg_konstruktora);, co musi być pierwszą instrukcją w ciele konstruktora podklasy. • Jeśli nie ma takiego wywołania, to domyślnie wołany jest konstruktor bezparametrowy

nadklasy. • Konstruktor bezparametrowy jest automatycznie dodawany do klasy, jeśli nie zdefiniowano w niej żadnych konstruktorów. • Jeśli jednak zdefiniowano konstruktory, to konstruktor bezparametrowy nie jest dodawany. W takiej sytuacji: class A { A(int x) { ... } } class B extends A { B() { // nie ma super(...) } } powstanie więc błąd w kompilacji, bo domyślnie ma być wywoływany konstruktor bezparametrowy klasy A, a jest on w tej klasie niezdefiniowany.

1.8. Singletony Wzorzec singleton zapewnia, że klasa będzie miała tylko jedną instancję (można utworzyć tylko jeden obiekt tej klasy), a jednocześnie udostępnia globalny, jednolity sposób uzyskiwania i odwoływania się do tego obiektu z różnych fragmentów kodu (innych klas). Przykładowo singletonem może być cennik używany przez różne klasy jakiegoś systemu rozliczeniowego (faktycznie w takiej sytuacji cennik powinien być jednolity, a więc zawsze przy odwołaniach do niego powinniśmy uzyskiwać jeden i ten sam obiekt). Standardowa implementacja cennika jako singletona może wyglądać tak: public final class PriceList extends HashMap { public static final PriceList INSTANCE = new PriceList(); private PriceList() {} } Konstruktor klasy PriceList jest prywatny, a zatem nie może być wywołany z żadnej innej klasy. Dzięki temu powstanie tylko jeden obiekt tej klasy – właśnie w trakcie statycznego inicjowania podczas jej ładowania przez JVM. W klasie PriceList dziedziczymy HashMap, w której pod kluczami z nazwami towarów umieszczone będą ich ceny. Dzięki temu kod klasy PriceList jest bardzo zwięzły. No i dodatkowo, klasa ta jest finalna, a więc nikt jej nie może odziedziczyć i np. podsuwać wadliwe ceny. Teraz przy inicjowaniu systemu możemy utworzyć cennik:

PriceList.INSTANCE.put("mleko", 3.0); PriceList.INSTANCE.put("krówki", 7.0); // ... PriceList.INSTANCE.put("schab", 15.0); i korzystać z niego w innych klasach (modułach) systemu, np. tak: double payment(String towar, double ilosc) { return PriceList.INSTANCE.get(towar) * ilosc; } // ... double cost = payment("schab", 1.5); Oczywiście w realnym (produkcyjnym) programowaniu trzeba by zastosować bardziej zaawansowane konstrukcje, np. odpowiednią klasę dla reprezentacji towarów z jakimiś ich identyfikatorami i możliwością uwzględnienia różnych jednostek (sztuki, kilogramy itp.).

1.9. Klasy opakowujące typy proste Bardzo często potrzebne jest traktowanie liczb jako obiektów. Możemy np. zechcieć utworzyć tablicę, która zawiera i liczby, i napisy. Wiemy, że można utworzyć tablicę dowolnych obiektów, np.: Object[] arr = new Object[3]; Ale liczby są reprezentowane przez typy proste (i nie są obiektami). Kolekcje mogą zawierać tylko referencje do obiektów, więc znowu powstaje pytanie, jak wstawić do kolekcji liczby. Dlatego właśnie w standardowym pakiecie java.lang umieszczono specjalne klasy, opakowujące typy proste i czyniące z nich obiekty. Są to klasy: • Integer • Short • Byte • Long • Float • Double • Boolean Obiekty tych klas reprezentują (w sposób obiektowy) dane odpowiednich typów. Mówimy tu o opakowaniu liczby (czy też danej typu boolowskiego), bo liczba (lub wartość true|false) jest umieszczone „środku” obiektu odpowiedniej klasy, jako jego element. Klasy opakowujące nie dostarczają żadnych metod operowania na liczbach, operatory arytmetyczne możemy zaś stosować tylko wobec liczbowych typów prostych. A przecież często potrzebujemy liczby jako obiektu, a jednocześnie jako typu prostego – do wykonania

operacji. Musimy więc umieć zapakować liczbę do obiektu klasy opakowującej (by użyć jej wtedy, gdy wymagany jest obiekt) i wyciągnąć ją stamtąd (gdy potrzebne są obliczenia). Typy proste możemy pakować i rozpakowywać „ręcznie”. Na przykład obiektowy odpowiednik liczby 5 typu int uzyskamy, tworząc obiekt klasy Integer: Integer a = new Integer(5); Z obiektu takiej klasy możemy liczbę „wyciągnąć” za pomocą odpowiednich metod, np. int i = a.intValue(); // zwraca wartość typu int, "zawartą" w obiekcie a Podobnie: Double dd = new Double(7.1); double d = dd.doubleValue(); // d == 7.1 Jest to jednak nieco żmudne, dlatego w Javie, poczynając od wersji 5, wprowadzono mechanizm zwany autoboxingiem. Autoboxing to automatyczne przekształcanie typów prostych na typy obiektów klas, opakowujących typy proste, i odwrotnie. Przykładowo możemy pisać tak: int n = 1; Integer in = n; // (1) n = in +1; // (2) zamiast in.intValue() + 1 W instrukcji (1) następuje tzw. boxing, czyli opakowanie wartości typu prostego w nowy obiekt klasy (nie musimy pisać new Integer(n), robi to za nas kompilator). W instrukcji (2) zachodzi natomiast tzw. unboxing, czyli automatyczne pobranie wartości typu prostego z obiektu klasy opakowującej (nie musimy pisać in.intValue()). Takie automatyczne przekształcenia zachodzą nie tylko przy przypisaniach, ale również przy przekazywaniu argumentów (metodom i konstruktorom) i zwrocie wyników z metod. Warto zauważyć, że opakowanie typów prostych zachodzi również przy przypisaniach (w tym przekazywaniu argumentów i zwrocie wyników) do zmiennej typu Object. Na przykład przypisanie: Object o = 1; spowoduje utworzenie obiektu klasy Integer opakowującego liczbę 1 i przypisanie referencji do tego obiektu na zmienną o. Jednak wypakowanie nie jest już automatyczne. W kontekście:

Object o = 1; przy próbie podstawienia na zmienną typu int: int

x = o;

w kompilacji wystąpi błąd. Aby tego uniknąć, musimy użyć rzutowania do typu Integer: int x = (Integer) o; Choć głównego powodu istnienia klas opakowujących upatrywać można w konieczności obiektowej reprezentacji typów prostych, to – niejako przy okazji – w klasach tych zdefiniowano wiele użytecznych metod. Są to np. statyczne metody parse ..., przekształcające napisową reprezentację liczb w ich wartości binarne: int Integer.parseInt (String), long Long.parseLong(String) itd. W klasach tych znaleźć można również wiele metod do operowania na bitach liczb, przekształcania na różne systemay liczbowe oraz przekształcania wartości na typy proste (np. doubleValue() zwraca wartość jako double). Użytecznych dodatkowych metod dostarcza klasa Character (opakowująca typ char). Należą do nich metody stwierdzania rodzaju znaku, np.: isDigit() isLetter() isLetterOrDigit() isWhiteSpace() itd.) isUpperCase() isLowerCase()

// czy znak jest znakiem cyfry // czy znak jest znakiem litery // czy znak jest litera lub cyfra // czy to "biały" znak (spacja, tabulacja // czy to duża litera // czy to mała litera

Metody te zwracają wartości true lub false. Dostępna jest też metoda Character.getType(char c), która zwraca stałą oznaczającą bardziej szczegółowy typ znaku, np. Character.CURRENCY_SYMBOL. Klasa Character opakowuje dwubajtowy typ char (czyli znaki Unicode z tzw. Basic Multi Plane), ale ma statyczne metody pozwalające działać na tzw. dodatkowych znakach (o heksadecymalnych kodach z zakresu od 100000 do 10FFFF), reprezentujących pary danych typu char. W klasach opakowujących typy numeryczne zdefiniowano także wiele użytecznych stałych statycznych. Należą do nich stałe zawierające maksymalne i minimalne wartości danego typu. Mają one nazwy MAX_VALUE i MIN_VALUE, więc nie musimy pamiętać zakresu wartości danego typu.

ROZDZIAŁ 2 Ponowne wykorzystanie klas

Podejście obiektowe umożliwia ponowne wykorzystanie (reusing) już gotowych klas przy tworzeniu nowych, co znacznie oszczędza pracy przy kodowaniu, a także czyni programowanie mniej podatnym na błędy. Są dwa sposoby ponownego wykorzystania klas: • kompozycja, • dziedziczenie. Rozważymy najpierw dziedziczenie, po czym krótko powiemy o kompozycji.

2.1. Dziedziczenie Dziedziczenie polega na przejęciu właściwości i funkcjonalności obiektów innej klasy i ewentualnej ich modyfikacji i/lub uzupelnieniu w taki sposób, by były bardziej wyspecjalizowane. Jest to relacja nazywana generalizacją-specjalizacją: a jednocześnie B specjalizuje A. A jest generalizacją B.

B „jest typu” A, „B jest A”,

Niech klasa Publication opisuje właściwości publikacji, które kupuje i sprzedaje księgarnia: public class Publication { private private private private private private

String title; String publisher; int year; String ident; double price; int quantity;

public Publication() { } public Publication(String t, String pb, int y, String i, double pr, int q) { title = t; publisher = pb; year = y; ident = i;

price = pr; quantity = q; } public String getTitle() { return title; } public String getPublisher() { return publisher; } public int getYear() { return year; } public String getIdent() { return ident; } public double getPrice() { return price; } public void setPrice(double p) { price = p; } public int getQuantity() { return quantity; } public void buy(int n) { quantity += n; } public void sell(int n) { quantity -= n; } } Zauważmy, że za pomocą tej klasy nie możemy w pełni opisać książek. Książki są szczególną, „wyspecjalizowaną” wersją publikacji, bo oprócz tytułu, wydawcy, ceny itd. mają jeszcze jedną właściwość – autora (lub autorów). Gdybyśmy w programie chcieli opisywać zakup i sprzedaż książek, to powinniśmy utworzyć nową klasę opisującą książki, np. o nazwie Book. Moglibyśmy to robić od podstaw (definiując w klasie Book pola author, title, ident, price i wszystkie metody operujące na nich,

jak również metody sprzedaży i kupowania). Ale po co? Przecież klasa Publication dostarcza już większości potrzebnych nam pól i metod. Odziedziczymy ją zatem w klasie Book i dodamy tylko te nowe właściwości (pola i metody), których nie ma w klasie Publication, a które powinny charakteryzować książki. Słowo kluczowe extends służy do wyrażenia relacji dziedziczenia jednej klasy przez drugą. Piszemy: class B extends A { ... } co oznacza, że klasa B dziedziczy (rozszerza) klasę A.

Mówimy: • Klasa A jest bezpośrednią nadklasą, superklasą, klasą bazową klasy B. • Klasa B jest bezpośrednią podklasą, klasą pochodną klasy A.

Zapiszmy zatem: public class Book extends Publication { // definicja klasy Book } Co należy podać w definicji nowej klasy? Takie właściwości jak tytuł, wydawca, rok wydania, identyfikator, cena, liczba egzemplarzy publikacji „na stanie”, metody uzyskiwania informacji o tych cechach obiektów oraz metody sprzedaży i zakupu przejmujemy z klasy Publication. Zatem nie musimy ich na nowo definiować. Pozostało nam tylko zdefiniować nowe pole opisujące autora (niech nazywa się author) oraz metodę, która umożliwia uzyskanie informacji o autorze (powiedzmy getAuthor()). class Book extends Publication { private String author; public String getAuthor() { return author; } } Czy to wystarczy? Nie, bo jeszcze musimy powiedzieć, w jaki sposób mają być inicjowane obiekty klasy Book. Aha, potrzebny jest konstruktor. Naturalnie, utworzenie obiektu-książki wymaga podania:

• autora, • tytułu, • wydawcy, • roku wydania, • identyfikatora (numeru ISBN) • ceny, • liczby książek aktualnie „na stanie”. Czyli konstruktor powinien mieć postać: public Book(String aut, String tit, String pub, int y, String id, double price, int quant){ .... } Zwróćmy jednak uwagę: pola tytułu, wydawcy, roku, identyfikatora, ceny i liczby są prywatnymi polami klasy Publication. Z klasy Book nie mamy do nich dostępu. Jak je zainicjować? Pola nadklasy (klasy bazowej) inicjujemy za pomocą wywołania z konstruktora klasy pochodnej konstruktora klasy bazowej (nadklasy). Użycie w konstruktorze następującej konstrukcji składniowej: super(lista_argumentów); oznacza wywołanie konstruktora klasy bazowej z argumentami lista_argumentów. Jeśli występuje, musi być pierwszą instrukcją konstruktora klasy pochodnej. Jeśli nie występuje, przed utworzeniem obiektu klasy pochodnej zostanie wywołany konstruktor bezparametrowy klasy bazowej. Konstruktor klasy Book musi więc wywołać konstruktora nadklasy, by zainicjować jej pola, a następnie zainicjować pole author. A zatem pełna definicja klasy Book wygląda następująco (zob. kod 2.1). public class Book extends Publication { private String author; public Book(String aut, String tit, String pub, int y, String id, double price, int quant) { super(tit, pub, y, id, price, quant); author = aut; } public String getAuthor() { return author; }

} Kod 2.1. Dziedziczenie – definicja klasy Book Zwróćmy uwagę: wykorzystanie klasy Publication (przez jej odziedziczenie) oszczędziło nam wiele pracy. Nie musieliśmy ponownie definiować pól i metod z klasy Publication w klasie Book. Po takim zdefiniowaniu klasy Book możemy utworzyć jej obiekt: Book b = new Book("James Gossling", "Moja Java", "WNT", 2002, "ISBN6893", 51.0, 0); Ten obiekt zawiera (zob. rys. 2.1): • elementy określane przez pola klasy dziedziczonej (Publication), czyli: title, publisher, year, ident, price, quantity, • element określany przez pole klasy Book – author.

Rys. 2.1. Dziedziczenie – elementy obiektu klasy Book Podkreślmy: jest to jeden obiekt klasy Book. Wiemy na pewno, że możemy użyć na jego rzecz metody z klasy Book – getAuthor(). Ale ponieważ klasa Book dziedziczy klasę Publication, obiekty klasy Book mają wszelkie właściwości obiektów klasy Publication, więc możemy na ich rzecz używać również metod zdefiniowanych w klasie Publication. Nic zatem nie stoi na przeszkodzie, by napisać program (zob. kod 2.2), który z tej właściwości korzysta: class TestBook { public static void main(String[] args) { Book b = new Book("James Gossling", "Moja Java", "WNT", 2002, "ISBN6893", 51.0, 0); int n = 100; b.buy(n);

double koszt = n * b.getPrice(); System.out.println("Na zakup " + n + " książek:"); System.out.println(b.getAuthor()); System.out.println(b.getTitle()); System.out.println(b.getPublisher()); System.out.println(b.getYear()); System.out.println(b.getIdent()); System.out.println("---------------\nwydano: " + koszt); b.sell(90); System.out.println("---------------"); System.out.println("Po sprzedaży zostało " + b.getQuantity() + " pozycji"); } } Kod 2.2. Dziedziczenie – zastosowanie klasy Book i który skompiluje się i wykona poprawnie, dając w wyniku listing 2.1. Na zakup 100 książek: James Gossling Moja Java WNT 2002 ISBN6893 --------------wydano: 5100.0 --------------Po sprzedaży zostało 10 pozycji Listing 2.1. Wynik działania kodu 2.2. Możemy powiedzieć, że obiekty klasy Book są również obiektami klasy Publication (w tym sensie, że mają wszelkie właściwości obiektów klasy Publication).

2.2. Konwersje referencyjne Referencje do obiektów klasy Book możemy przypisywać zmiennym, oznaczającym obiekty klasy Publication (zawierającym referencje do obiektów klasy Publication), np.: Book b = new Book(...); Publication p = b; Nazywa się to referencyjną konwersją rozszerzającą (widening reference conversion). Słowo „konwersja” oznacza, że dochodzi do przekształcenia z jednego typu na inny typ (np. z typu Book dona typ Publication). Konwersja jest rozszerzająca, bo przekształcamy typ „pochodny” (referencja do obiektu podklasy) na typ „wyższy” (referencja do obiektu nadklasy).

A ponieważ chodzi o typy referencyjne, mówimy o referencyjnej konwersji rozszerzającej. Obiektowe konwersje rozszerzające dokonywane są automatycznie przy: • przypisywaniu zmiennej–referencji odniesienia do obiektu klasy pochodnej; • przekazywaniu argumentów metodzie, gdy parametr metody jest typu „referencja do obiektu nadklasy argumentu”; • zwrocie wyniku, gdy wynik podstawiamy na zmienną będącą referencją do obiektu nadklasy zwracanego wyniku. Ta zdolność obiektów Javy do „stawania się” obiektem swojej nadklasy jest niesłychanie użyteczna. Wyobraźmy sobie, że z klasy Publication oprócz klasy Book wprowadziliśmy jeszcze klasę Journal (czasopisma). Klasa Journal dziedziczy klasę Publication i dodaje do niej – zamiast pola, opisującego autora – pola opisujące wolumin i numer wydania danego czasopisma. Być może będziemy mieli jeszcze inne rodzaje publikacji, np. muzyczne, wydane na płytach CD (powiedzmy klasę CDisk znowu dziedziczącą klasę Publication i dodającą jakieś informacje właściwe dla muzyki, np. czas odtwarzania). Możemy teraz np. napisać uniwersalną metodę pokazującą różnicę w dochodach ze sprzedaży wszystkich zapasów dowolnych dwóch publikacji. public double incomeDiff(Publication p1, Publication p2) { double income1 = p1.getQuantity() * p1.getPrice(); double income2 = p2.getQuantity() * p2.getPrice(); return income1 – income2; } i wywoływać ją dla dowolnych (różnych rodzajów) par publikacji: Book b1 = Book b2 = Journal j CDisk cd1 CDisk cd2 double diff = diff = diff =

new Book(...); new Book(...); = new Journal(...); = new CDisk(...); = new CDisk(...);

diff = 0; incomeDiff(b1, b2); incomeDifg(b1, j); incomeDiff(cd1, b1);

Gdyby nie było obiektowych konwersji rozszerzających, to dla każdej możliwej kombinacji „rodzajowej” par musielibyśmy napisać inną metodę incomeDiff, np.: double incomeDiff(Book, Book), double incomeDiff(Book, Journal), double incomeDiff(Book, CDisk). Zwróćmy uwagę, że w przedstawionej metodzie incomeDiff można wobec p1 i p2 użyć metod klasy Publication (bo tak są zadeklarowane parametry), ale nie można używać

metod klas pochodnych, nawet wtedy, gdy p1 i p2 wskazują na obiekty klas pochodnych, np.: .... { Book b = new Book(...); jakasMetoda(b); .... } void jakasMetoda(Publication p1) { String autor = p1.getAuthor(); // Błąd kompilacji – niezgodność typów; ... // na rzecz obiektu klasy Publication ... // nie wolno użyć metody getAuthor() } // bo takiej metody nie ma w klasie Publication Zaradzić temu możemy, stosując referencyjną konwersję zawężającą, przy czym konieczne jest jawne zastosowanie operatora rzutowania do typu Book: void jakasMetoda(Publication p1) { String autor = (Book) p1.getAuthor(); // ... } Teraz kompilator nie będzie się skarżył, ponieważ wyraźnie daliśmy do zrozumienia, że chcemy potraktować obiekt p1 jako obiekt klasy Book, a w klasie Book jest metoda getAuthor(). Co by się jednak stało, gdyby tej metodzie przekazać obiekt klasy Journal? Kompilator nie będzie protestował, bo Journal pochodzi od Publication. W fazie kompilacji nie jest też możliwe sprawdzenie, co naprawdę siedzi w zmiennej p1 – czy to jest referencja do obiektu klasy Book, czy też może jakiejś innej klasy pochodnej od Publication. Prawda wyjdzie na jaw dopiero w fazie wykonania programu: rzutowanie na typ Book spowoduje błąd o nazwie ClassCastException i przerwanie programu, ponieważ w p1 znajduje się referencja do obiektu klasy Journal i nie da się jej rzutować na referencję do obiektu typu Book.

2.3. Stwierdzanie typu Można się przed takimi sytuacjami zabezpieczyć, używając operatora stwierdzania typu (instanceof).

Wyrażenie: ref

instanceof

T

ma wartość true, jeśli referencja ref nie jest null i może być w fazie wykonania programu rzutowana do typu T bez zgłoszenia wyjątku (błędu) ClassCastException. Przy tym: • Wyrażenie null instanceof dowolny_typ_refrencyjny zawsze ma wartość false. • Wyrażenie x instanceof T będzie błędne składniowo (wystąpi błąd w kompilacji), jeśli typ referencji x i typ T nie są związane stosunkiem dziedziczenia. • Wyrażenie x instanceof T będzie miało wartość false, jeśli faktyczny typ referencji x jest nadtypem typu T. • Na razie terminy „typ”, „podtyp” i „nadtyp” możemy kojarzyć z pojęciami klasy, podklasy i nadklasy. Dosyć formalną definicję w praktycznym użyciu możemy rozumieć tak: x instanceof T zwróci true, jeśli referencja x jest typu T lub dowolnego podtypu T. Przykładowy kod 2.3 pokazuje, w jaki sposób można użyć operatora instanceof, aby wyświetlić właściwą informację o książce (ma autora) i o czasopiśmie (ma numer oraz wolumin – zwracane przez getVolNr()). static void info(Publication p) { String info = p.getTitle(); if (p instanceof Book) info += ‚\n’ + ((Book) p).getAuthor(); else if (p instanceof Journal) info += ‚\n’ + ((Journal) p).getVolNr(); // ... System.out.println(info); } public static void main(String[] args) { info(new Book(...)); info(new Journal(...)); } Kod 2.3. Użycie operatora instanceof Innym sposobem stwierdzania typu jest zastosowanie metody getClass(). Zwraca ona faktyczny typ obiektu w postaci referencji do obiektu klasy Class. Obiekty tej klasy oznaczają klasy. Zatem w kontekście:

Book b = new Book(...); Publication p = b; Class c = p.getClass(); zmienna c będzie oznaczać klasę Book. Łatwo możemy się dowiedzieć nazwy klasy: Class klasa = p.getClass(); String nazwa = klasa.getName(); // będzie zwrócona nazwa klasy // (w naszym przypadku Book) i w ten sposób sprawdzić, o jaką klasę chodzi. W takim sprawdzaniu można też użyć literałów klasowych. Mają one postać: nazwa_klasy.class Na przykład: java.lang.String.class java.lang Book.class domyślnego

// oznacza klasę String z pakietu // oznacza klasę Book z pakietu

Między stwierdzaniem typu przez instanceof i getClass występuje istotna różnica.Zobaczmy to na przykładzie. Załóżmy, że mamy jeszcze dodatkowo klasę Tabloid dziedzicząca klasę Journal i obiekt tej klasy: Tabloid t = new Tabloid(...); Wtedy: • t instanceof Journal zwróci true, bo brane są pod uwagę wszystkie podtypy Journal, a t jest typu Tabloid, czyli podtypu Journal; • t.getClass() == Journal.class będzie miało wartość false, bo sprawdzana jest konkretna klasa, którą w tym przypadku jest Tabloid; • t.getClass().getName() zwróci napis „Tabloid”.

2.4. Dziedziczenie w Javie W Javie każda klasa może bezpośrednio odziedziczyć tylko jedną klasę, ale pośrednio może mieć dowolnie wiele nadklas, co wynika z hierarchii dziedziczenia. Ta hierarchia zawsze zaczyna się od klasy Object (której definicja znajduje się w zestawie stanardowych klas

Javy). Zatem w Javie wszystkie klasy pochodzą pośrednio od klasy Object. Wobec tego hierarchia dziedziczenia omawianych tu klas wygląda jak na rys. 2.2. Z tego wynika, że referencję do obiektu dowolnej klasy można przypisać zmiennej typu Object (zawierającej referencję do obiektu klasy Object). Z właściwości tej korzysta wiele „narzędziowych” metod zawartych w klasach standardu Javy.

Rys. 2.2. Hierarchia dziedziczenia klas publikacji

2.5. Przedefiniowanie metod Niech teraz naszym zadaniem będzie zdefiniowanie klasy samochodów. Samochody są specjalnym rodzajem pojazdów, zatem obiekty klasy samochod przejmują wszystkie właściwości obiektów klas pojazdów, dodatkowo dostarczając jakichś własnych cech specyficznych. Projektując klasę dotyczącą samochodów (klasę Car), możemy skorzystać z gotowej klasy Vehicle (nie musimy na nowo pisać metod, definiować pól itd.). Skupiamy się na specyficznych cechach samochodów, ich cechy jako pojazdów „w ogóle” przejmując z klasy Vehicle. Przyjmijmy, że wyróżniającymi cechami samochodów są: • numer rejestracyjny, • użycie paliwa (w razie braku paliwa samochód nie może ruszyć), a następnie: • dodajmy odpowiednie pola do klasy Car oraz odpowiedni konstruktor, • dodajmy metodę fill() pozwalającą tankować paliwo, • przedefiniujmy metodę start() tak, by bez paliwa samochód nie mógł ruszyć, oraz metodę toString(), by wyświetlały właściwe dla samochodów informacje. Denicję klasy Car przedstawia kod 2.4. class Car extends Vehicle { private String nrRej; private int tankCapacity; private int fuel; public Car(String nr, Person owner, int w, int h, int l, int weight, int tankCap) { super(owner, w, h, l, weight); nrRej = nr; tankCapacity = tankCap; } public void fill(int amount) { fuel += amount; if (fuel > tankCapacity) fuel = tankCapacity; } public void start() { if (fuel > 0) super.start(); else error("Brak paliwa"); }

public String toString() { return "Samochód nr rej " + nrRej + " - " + getState(); } } Kod 2.4. Klasa Car – przedefiniowanie metod z klasy Vehicle Przedefiniowanie metody (overriding) nadklasy w klasie pochodnej oznacza dostarczenie w klasie pochodnej definicji nieprywatnej i niestatycznej metody z taką samą sygnaturą (czyli nazwą i listą parametrów) jak sygnatura nieprywatnej i niestatycznej metody nadklasy, ale ewentualnie z inną definicją ciała metody (innym kodem, który jest wykonywany przy wywołaniu metody), przy czym: • typy wyników tych metod muszą być takie same lub kowariantne (co oznacza m.in., że typ wyniku metody z podklasy może być podtypem wyniku metody nadklasy); • przedefiniowanie nie może ograniczać dostępu: specyfikator dostępu metody przedefiniowanej w podklasie musi być taki sam lub szerszy (np. public zamiast protected) niż metody przedefiniowywanej; • metoda przedefiniowana (z podklasy) nie może zgłaszać więcej lub bardziej ogólnych wyjątków kontrolowanych niż metoda przedefiniowywana (z nadklasy), może natomiast – niezależnie od definicji metody nadklasy – zgłaszać dowolne wyjątki niekontrolowane. W klasie Car przedefiniowano metody start() i toString() z klasy Vehicle. Zwróćmy uwagę, że w metodzie start() klasy Car uruchamiamy samochód, gdy bak nie jest pusty. Nie mogliśmy jednak napisać w metodzie start() klasy Car: if (fuel > 0) start(); bo to oznaczałoby rekurencyjne wywołanie metody start() klasy Car. Ogólnie uruchamianiem wszelkich pojazdów zajmuje się metoda start() z klasy Vehicle i właśnie ją chcemy wywołać. Odwołanie do metody klasy bazowej, która w klasie pochodnej jest przedefiniowana, zapisujemy za pomocą specyfikatora super. Na przykład super.start()użyte w klasie Car wywoła metodę start() z klasy Vehicle, a samo start() wywoła metodę start() z klasy Car. Obiekt klasy Car składa się z elementów zdefiniowanych przez pola klasy Vehicle oraz elementów zdefiniowanych przez pola klasy Car. Wobec obiektów klasy Car możemy używać: •

wszystkich nieprywatnych (i nieprzedefiniowanych) (np. crash(...)), • przedefiniowanych w klasie Car metod klasy Vehicle, • własnych metod klasy Car. Pokazuje to fragment programu:

metod

klasy

Vehicle

Car c = new Car("WA1090", new Person("Janek", "09090909"), 100, 100, 100, 100, 50), d = new Car("WB2010", new Person("Ania", "10101010"), 100, 100, 100, 100, 50); try { c.start(); } catch (Exception exc) { System.out.println(c + " - " + exc.getMessage()); } c.fill(10); System.out.println(c); c.start(); System.out.println(c); c.stop(); System.out.println(c); d.fill(20); System.out.println(d); d.start(); System.out.println(d); c.start(); System.out.println(c); c.crash(d); System.out.println(c + "\n" + d); c.repair(); d.repair(); System.out.println(c + "\n" + d); c.start(); d.start(); System.out.println(c + "\n" + d); który wyświetli: Samochód Samochód Samochód Samochód Samochód Samochód Samochód Samochód Samochód Samochód Samochód Samochód Samochód

nr nr nr nr nr nr nr nr nr nr nr nr nr

rej rej rej rej rej rej rej rej rej rej rej rej rej

WA1090 WA1090 WA1090 WA1090 WB2010 WB2010 WA1090 WA1090 WB2010 WA1090 WB2010 WA1090 WB2010

-

STOPPED - Brak paliwa STOPPED MOVING STOPPED STOPPED MOVING MOVING BROKEN BROKEN STOPPED STOPPED MOVING MOVING

W przedstawionym tu testowym fragmencie jest dużo wierszy. Można skrócić i ułatwić

pisanie takich programów, jeśli wybrane metody będą zwracać referencję do obiektu, na którego rzecz zostały wywołane. Jeśli zrobimy to w klasie Vehicle: public Vehicle start() { setState(MOVING); return this; } public Vehicle stop() { setState(STOPPED); return this; } public Vehicle repair() { // ... return this; } to kompilator zwróci nam uwagę na błąd w przedefiniowaniu metody start() w klasie Car. Istotnie metoda ma tę samą nazwę i parametry co start() z klasy Vehicle, ale inny typ wyniku. Musimy ten typ zmienić: oczywiście może to być Vehicle, ale lepiej byłoby użyć typu Car, bo wtedy na rzecz wyniku tej metody możemy wołać jakieś inne metody specyficzne dla klasy Car, np. hipotetyczną metodę accelerate() – przyśpiesz.

2.6. Kowariancja typów wyników I tu właśnie przyda nam się wspomiana w definicji przedefiniowania kowariancja typów wyników przedefiniowanych metod. W sytuacji: class A { S met() {...} } class B extends A { T met() {...} } metoda met ma kowariantny typ wyniku, jeśli T jest podtypem S. Kowariancja to współzmienność – typ wyniku metody zmienia się w tym samym kierunku hierarchii dziedziczenia, co kierunek dziedziczenia klas, w których jest definiowana.

Zatem całkiem spokojnie możemy w klasie Car zapisać: public Car fill(int amount) { // ... return this; } public Car start() { // kowariantny typ wyniku przedefiniowanej metody // ... return this; } public Car stop() { super.stop(); return this; } public Car repair() { super.repair(); return this; } i następnie tworzyć np. takie fragmenty kodu: c.fill(10).start().crash(d.fill(20).start()); System.out.println(c + "\n" + d); c.repair().fill(10).start(); d.repair().start(); System.out.println(c + "\n" + d); d.stop().fill(10); // ... co da: Samochód Samochód Samochód Samochód

nr nr nr nr

rej rej rej rej

WA1090 WB2010 WA1090 WB2010

-

BROKEN BROKEN MOVING MOVING

Zauważmy użyteczność kowariancji typów wyniku: przedefiniowaliśmy w klasie Car metody stop() i repair() tylko po to, by zapewnić odpowiedni typ ich wyniku, dzięki czemu możliwe stały się odwołania repair().fil() i stop().fill().

2.7. Przedefiniowanie metod w wyliczeniach

W klasach Vehicle i Car metoda toString() wykorzystywała niejawnie wywołanie metody toString() wyliczenia VehState np. w takich fragmentach: " jest w stanie " + state; "Samochód nr rej " + nrRej + " - " + getState(); Tu zarówno state, jak i wynik wywołania getState() jest typu enum VehState, a to wyliczenie, jak już wiemy (por. rodz. 1.2), jest specjalną klasą. Dziedziczy ona klasę Enum z pakietu java.lang i przy dołączaniu wartości elementów wyliczenia do napisów niejawnie wywoływana jest metoda toString()z klasy Enum. W wyniku widzieliśmy takie napisy jak: STOPPED, BROKEN, MOVING. Skoro z wyliczeniami możemy postępować (prawie) tak jak z normalnymi klasami, to nic nie stoi na przeszkodzie, aby w VehState przedefiniować metodę toString() z klasy Enum (zob. kod 2.5). public enum VehState { BROKEN("ZEPSUTY"), STOPPED("STOI"), MOVING("JEDZIE"); private String opis; VehState(String s) { opis = s; } public String toString() { return opis; } } Kod 2.5. Przedefiniowanie metody toString() w enum Teraz fragmenty ilustrujące wykorzystanie klas Vehicle i Car będą wyświetlać bardziej zrozumiałą (językowo jednolitą) informację, np.: Samochód Samochód Samochód Samochód

nr nr nr nr

rej rej rej rej

WA1090 WB2010 WA1090 WB2010

-

ZEPSUTY ZEPSUTY JEDZIE JEDZIE

2.8. Przedefiniowanie a wyjątki Jak już wspomniano, przedefiniowanie metody nie może poszerzać zakresu wyjątków kontrolowanych wymienionych w klauzuli throws (przypomnijmy, że wyjątki kontrolowane to te, których klasy pochodzą od klasy Exception, ale nie od RuntimeException).

Oznacza to, że: • Jeżeli metoda z klasy bazowej nie ma klauzuli throws, to metoda przedefiniowująca ją w klasie pochodnej nie może wymienić w swojej klauzuli throws żadnych wyjątków kontrolowanych. • Jeżeli metoda z klasy bazowej wymienia w swojej klauzuli throws jakieś wyjątki kontrolowane, to metoda przedefiniowująca ją w klasie pochodnej nie może wymienić żadnej nadklasy tych wyjątków ani żadnych dodatkowych innych klas wyjątków kontrolowanych, może natomiast wymienić dowolne wyjątki pochodzące z podklas wyjątków zgłaszanych przez metodę z klasy bazowej. • Niezależnie od metody z klasy bazowej metoda przedefiniowana w klasie pochodnej może nie zgłaszać żadnych wyjątków i nie mieć klauzuli throws. • Metoda przedefiniowana w klasie pochodnej zawsze może zgłaszać wyjątki niekontrolowane i ewentualnie wymieniać je w swojej klauzuli throws (co nie jest obowiązkowe). Ilustruje to przykładowy kod, w którym: • wyjątek IOException pochodzi od Exception, • wyjątek FileNotFoundException pochodzi od IOException, • wyjątek NumberFormatException jest pochodzi od RuntimeException. class A void void void void void }

{ met1() met2() met3() met4() met5()

{} throws FileNotFoundException {} throws IOException {} throws Exception {} {}

class B extends A { void met1() throws Exception {} // błąd: ilustruje pkt. 1 void met2() throws IOExcepetion {} // błąd: ilustruje pkt. 2 void met3() throws FileNotFoundException, IOException {} // ok, ilustruje pkt 2 void met4() {} // ok, ilustruje pkt 3 void met5() throws NumberFormatException { } // ok, ilustruje pkt 4 }

2.9. Przedefiniowanie a przeciążanie, przesłanianie i pokrywanie

Przedefiniowanie metod (overriding) należy odróżniać od ich przeciążania (overloading), a także od dwóch innych pojęć: przesłaniania (shadowing) i pokrywania (hiding) identyfikatorów (zmiennych, metod, klas). Niewątpliwie najłatwiej odróżnić metody przeciążone. Mają one po prostu te same nazwy, ale inną liczbę i/lub typy parametrów. Zwróćmy uwagę, że: • Przeciążone metody mogą należeć do tej samej klasy lub różnych klas (z których jedna pośrednio lub bezpośrednio dziedziczy inną). • Przeciążanie nie wyklucza przedefiniowania: jeśli np. w klasie A zdefiniowano dwie publiczne metody z tą samą nazwą (co oznacza, że są przeciążone, bo sygnatury metod deklarowanych w jednej klasie muszą się różnić), to w klasie B dziedziczącej klasę A możemy jedną z nich dodatkowo przeciążyć (czyli podać w deklaracji inny zestaw parametów), a drugą przedefiniować (pozostawiając jej sygnaturę bez zmian i dostarczając innej definicji kodu metody). W tym względzie Java różni się od C++. Terminu „przesłanianie” używa się w Javie wtedy, gdy w zasięgu deklaracji identyfikatora zmiennej, metody lub klasy (ogólniej: typu) pojawia się inna deklaracja tego identyfikatora (widzieliśmy przykłady przesłaniania identyfikatorów pól przez identyfikatory zmiennych lokalnych). W przeciwieństwie do przesłaniania terminów „przedefiniowanie” (metody) i „pokrycie” (metody lub pola) używa się w sytuacji dziedziczenia. Czym różni się pokrycie metody od jej przedefiniowania? Otóż dostarczenie w podklasie definicji metody statycznej o tej samej sygnaturze i tym samym lub kowariantnym typie wyniku co metoda statyczna z nadklasy nazywa się pokryciem metody. Pokrywanie nie dotyczy metod niestatycznych, co więcej – jeśli w podklasie dostarczymy definicję metody statycznej o tej samej sygnaturze co metoda niestatyczna nadklasy, to wystąpi błąd w kompilacji. Rozróżnienie pokrycia i przedefiniowania metody związane jest też z pojęciem polimorfizmu (zob. następne punkty): metody przedefiniowane są wywoływane polimorficznie, pokrycie zaś oznacza tylko zastąpienie wywołania metody pokrytej. Pokrywanie może dotyczyć również pól: oznacza ono wtedy deklarację w podklasie pola o takim samym identyfikatorze jak pole z nadklasy. Pokrycie identyfikatorów pól różni się zarówno od pokrywania identyfikatorów metod, jak i przedefiniowania metod. Pole statyczne może pokryć pole niestatyczne i odwrotnie. Pole pokrywające pole nadklasy może mieć całkiem inny typ niż pokryte pole nadklasy. Metoda prywatna nigdy nie może być pokryta ani przedefiniowana w podklasie. Deklaracja w podklasie metody o tej samej sygnaturze co metoda prywatna nadklasy oznacza praktycznie wprowadzenie „niezależnego” bytu do naszego programu (zatem możemy mieć np. całkiem inny typ wyniku niż w metodzie prywatnej o tej samej sygnaturze z nadklasy).

2.10. Adnotacja @override

Przedefiniowując metody w podklasach, warto używać adnotacji @Override. Załóżmy, że programista chce przedefiniować metodę toString() w klasach A, B i C, ale gdzieś się pomylił w pisowni, gdzieś podał złą listę parametrów itp. package tostring; class A { public String ToString() { return "Obiekt klasy A"; } } class B { public String toString() { return "Obiekt klasy B"; } } class C extends B { public String toString(String ... myMsg) { String s = "Obiekt klasy C"; if (myMsg.length == 1) s += myMsg[0]; return s; } } Po takim użyciu tych klas: System.out.println( new A()); System.out.println( new B()); System.out.println( new C()); nasz programista da – zapewne wbrew intencjom – następujące wyniki: tostring.A@a90653 // błąd w nazwie metody - wołana jest toString() z klasy Object, ktorą dziedziczy A Obiekt klasy B // tu dobrze Obiekt klasy B // ale tu metoda toString() jest przeciążona w klasie C, a nie przedefiniowana dlatego dostajemy napis z metody toString z klasy B Aby uniknąć tego rodzaju błędów w codziennym programowaniu, warto się przyzwyczaić i stosować konwencje nazewnicze Javy (wtedy nigdy nie użyjemy jako nazwy metody ToString). Oprócz tego pomocna jest właśnie adnotacja @Override. Za pomocą tej adnotacji informujemy kompilator, że naszą intencją jest przedefiniowanie metody. Jeśli warunki przedefiniowania nie są spełnione (nie ma metody o identycznej sygnaturze i identycznym lub kowariantnym typie wyniku w nadklasach), kompilator poinformuje o błędzie. Użycie @Override w poprzednim przykładzie ilustruje rysunek rys. 2.3.

Rys. 2.3. Użycie adnotacji @Override I na koniec warto podkreślić, że istotą przedefiniowania metod jest modyfikacja lub uszczegółowienie funkcjonalności. Jest to wielka zaleta, bez tego programowanie obiektowe nie byłoby możliwe. Ale równocześnie otwiera pole dla tzw. słabej hermetyzacji klasy bazowej. W klasie Vehicle nie ma metody sing(), więc pojazdy nie śpiewają? Ależ niektóre mogą śpiewać: wystarczy odpowiednio przedefiniować metodę start() w podklasie! Aby uniknąć popełniania podobnych błędów w przypadkach, gdy zmiany funkcjonalności

fragmentów kodu nie są potrzebne czy są nawet niebezpieczne, w deklaracji metod stosuje się słowo kluczowe final. Słowo kluczowe final użyte w deklaracji metody zabrania jej przedefiniowania.

2.11. Metody wirtualne i polimorfizm W klasie Car przedefiniowaliśmy metodę start() z klasy Vehicle (dla samochodów sprawdza ona, czy jest paliwo, by ruszyć, nie robi tego dla pojazdów w ogóle). Przedefiniowaliśmy też metodę toString() (dla obiektów klasy Car zwraca ona inne napisy niż dla ogólniejszych obiektów klasy Vehicle). Jeżeli teraz: Car c = new Car(...); // utworzymy nowy obiekt klasy Car Vehicle v = c; // dokonamy obiektowej konwersji rozszerzającej to jaki będzie wynik użycia metod start() i toString() wobec obiektu oznaczanego v: v.start(); System.out.println(v.toString()); Czy zostaną wywołane metody z klasy Vehicle (formalnie metody te są wywoływane na rzecz obiektu klasy Vehicle), czy z klasy Car (referencja v formalnego typu „referencja do obiektu Vehicle” faktycznie wskazuje na obiekt klasy Car)? Rozważmy przykład „z życia” zapisany w programie, a mianowicie schematyczną symulację wyścigu pojazdów. Uczestnicy: rowery (obiekty klasy Rower), samochody (obiekty klasy Car), rydwany (obiekty klasy Rydwan). Wszystkie klasy są pochodne od Vehicle. Każda z tych klas inaczej przedefiniowuje metodę start() z klasy Vehicle (np. Rower może w ogóle jej nie przedefiniowywać, Car – tak jak w poprzednich przykładach, Rydwan – w jakiś inny sposób). Sygnał do startu wszystkich pojazdów daje starter. W programie moglibyśmy to symulować przez: •

uzyskanie tablicy wszystkich pojazdów startujących w wyścigu (np. getAllVehiclesToStart()); • przebiegnięcie przez wszystkie elementy tablicy i posłanie do każdego z obiektów przez nie reprezentowanych komunikatu start(). Na przykład: Vehicle[] allveh = getAllVehiclesToStart(); for (Vehicle v : allveh) v.start(); Jeżeli nasz program ma odwzorowywać rzeczywistą sytuację wyścigu (sygnał startera, po którym wszystkie pojazdy, jeśli mogą, ruszają), to oczywiście – mimo że v jest formalnego typu Vehicle – powinny być wywołane metody start() z każdej z odpowiednich podklas klasy

Vehicle (właściwa metoda start() dla danego rodzaju pojazdu). I tak jest rzeczywiście w Javie (zob. rys. 2.4).

Rys. 2.4. Polimorficzne wywołanie metod Ale jak to jest możliwe? Z punktu widzenia łączenia przez kompilator odwołań do metody (np. start()) oraz jej definicji (wykonywalnego kodu) sytuacja jest następująca: 1. Kompilator wie tylko, że start() jest komunikatem do obiektu typu Vehicle. 2. Powinien więc związać odwołanie v.start() z definicją metody start() z klasy Vehicle. Jakże inaczej? Przecież wartość v może zależeć od jakichś warunków występujących w trakcie wykonania programu (nieznanych kompilatorowi). Mając na przykład dwie klasy dziedziczące klasę Vehicle, Car i Rydwan, możemy napisać: public static void main(String args[]) { Car c = new Car(...); Rydwan r = new Rydwan(...); Vehicle v; if (args[0].equals("Rydwan")) v = r; else v = c; v.start(); } Kompilator nie może wiedzieć, jaki konkretnie jest typ obiektu wskazywanego przez v (czy Car czy Rydwan). I nie wie! W jaki sposób zatem uzyskujemy efekt opisany wcześniej (zgodny z życiowym doświadczeniem), czyli np. wywołanie metody start() z klasy Car, jeśli v wskazuje na obiekt klasy Car, natomiast wywołanie metody start() z klasy Rydwan, jeśli v wskazuje na obiekt klasy Rydwan? Otóż metoda start() z klasy Vehicle jest metodą wirtualną, a dla takich metod wiązanie odwołań z kodem następuje w fazie wykonania, a nie w fazie kompilacji. Nazywa się to dynamic binding lub late binding (dynamiczne lub późne wiązanie). Mówi się, że odwołania do metod wirtualnych są polimorficzne, a słowo „polimorficzne” używane jest w tym sensie, iż konkretny rezultat odwołania może przybierać różne kształty w zależności od tego, jaki jest faktyczny typ obiektu, na którego rzecz wywołano metodę

wirtualną. Istotnie, jak widzieliśmy: v.start() raz może oznaczać start samochodu, a innym razem start rydwanu czy roweru. Wszystkie metody w Javie są wirtualne z wyjątkiem: • metod statycznych (bo przecież nie dotyczą obiektów), • metod deklarowanych ze specyfikatorem final (co oznacza, że postać metody jest ostateczna i nie może być ona przedefiniowana w klasie pochodnej, a jak nie ma przedefiniowania, to niepotrzebna jest wirtualność), • metod prywatnych (do których odwołania z innych metodach danej klasy nie są polimorficzne, bo metody prywatne nie mogą być przedefiniowane).

Znaczenie polimorfizmu można zilustrować prostym przykładem. Rozważmy pewną hierarchię dziedziczenia opisującą takie właściwości różnych zwierząt, jak nazwa gatunku, sposób komunikowania się ze światem oraz imię. Dzięki odpowiedniemu określeniu bazowej klasy Zwierz przy definiowaniu klas pochodnych (takich jak Pies czy Kot) mamy całkiem niewiele roboty (zob. kod 2.6). (Uwaga. W dalszych przykładach pomijamy specyfikatory dostępu, bo nie mają znaczenia dla omawianych tu treści). class Zwierz { String name = "bez imienia"; Zwierz() { } Zwierz(String s) { name = s; } String getTyp() { return "Jakis zwierz"; } String getName() { return name; } String getVoice() { return "?"; } // Metoda speak symuluje wydanie głosu przez wypisanie odpowiedniego komunikatu void speak() { System.out.println(getTyp()+" "+getName()+" mówi "+getVoice()); } } class Pies extends Zwierz { Pies() { } Pies(String s) { super(s); } String getTyp() { return "Pies"; } String getVoice() { return "HAU, HAU!"; } } class Kot extends Zwierz { Kot() { }

Kot(String s) { super(s); } String getTyp() { return "Kot"; } String getVoice() { return "Miauuuu..."; } } Kod 2.6. Polimorficzne zwierzęta – wersja wstępna W klasie Main wypróbujemy naszą hierarchię klas zwierząt przy symulowaniu rozmów między poszczególnymi osobnikami. Rozmowę symuluje statyczna funkcja animalDialog, która ma dwa argumenty – obiekty typu Zwierz oznaczające aktualnych „dyskutantów” (zob. kod 2.7). public class Main { public static void main(String[] arg) { Zwierz z1 = new Zwierz(), z2 = new Zwierz(); Pies pies = new Pies(), kuba = new Pies("Kuba"), reksio = new Pies("Reksio"); Kot kot = new Kot(); animalDialog(z1, z2); animalDialog(kuba, reksio); animalDialog(kuba, kot); animalDialog(reksio, pies); } static void animalDialog(Zwierz z1, Zwierz z2) { z1.speak(); z2.speak(); System.out.println("---------------------------------------"); } } Kod 2.7. Znaczenie polimorfizmu – prosty przykład Wynik działania tej aplikacji pokazuje listing 2.2. Jakis zwierz bez imienia mówi ? Jakis zwierz bez imienia mówi ? ---------------------------------------Pies Kuba mówi HAU, HAU! Pies Reksio mówi HAU, HAU! ---------------------------------------Pies Kuba mówi HA HAU! Listing 2.2. Wynik działania kodu 2.7 Cóż jest ciekawego w tym przykładzie? Otóż dzięki wirtualności metod getTyp() i getVoice() metoda speak(), określona w klasie Zwierz, prawidłowo działa dla różnych zwierząt (obiektów podklas klasy Zwierz). Jest to nie tylko ciekawe, ale i wygodne:

jedna definicja metody speak() załatwiła nam wszystkie potrzeby (dotyczące dialogów różnych zwierząt). Co więcej, będzie tak samo użyteczna dla każdej nowej podklasy Zwierza, którą kiedykolwiek w przyszłości wprowadzimy. Nie jest to kwestia wymyślonych przykładów. Z praktyczną użytecznością polimorfizmu stykamy się w Javie od samego początku. Na przykład metoda println z argumentem Object wyświetla tekst opisujący przekazany obiekt. Tekst ten dostaje przez odwołanie do metody toString() za pośrednictwem statycznej metody valueOf(Object) z klasy String: W klasie String: public static String valueOf(Object obj) {return (obj == null) ? "null" : obj.toString(); } W klasie PrintStream (lub PrintWriter): public void println(Object obj) {String txt = String.valueOf(x); // wyświetlenie tekstu txt } Metoda toString() jest po raz pierwszy zdefiniowana w klasie Object – jej wynikiem jest tam napis w postaci: nazwa_klasy@unikatowy_identyfikator_obiektu. Klasę Object dziedziczą wszystkie klasy (pośrednio lub bezpośrednio). W klasach tych można więc zawsze przedefiniować metodę toString(). A przedefiniowane metody wywoływane są polimorficznie, zawsze więc uzyskamy właściwy opis obiektu (określony w danej klasie), lub – jeśli nie zdefiniowano w niej metody toString – opis z pierwszej nadklasy, w której jest ona zdefiniowana. Na przykład, mając taką hierarchię dziedziczenia jak na schemacie: public class Object { // toString() zwraca id obiektu } class A { // domyślnie dziedziczy Object // nieprzedefiniowana metody toString } class B { public String toString() { return "Obiekt klasy B"; } } class C extends B { public String toString() { return "Obiekt klasy C"; }

} class D extends B { // nieprzedefiniowana metody toString } po: Object[] obArr = { new A(), new B(), new C(), new D() }; for (Object o : obArr) System.out.println(o); uzyskamy: A@6e1408 // brak przedefiniowania metody toString() – wywoływane jest toString() z klasy Object Obiekt klasy B // jest wywoływana toString() z klasy B, a nie Object (choć formalnie o jest typu Object) Obiekt klasy C // jw., tylko z klasy C Obiekt klasy B // w klasie D nie ma toString(), polimorficznie wywoływana jest toString() z klasy B.

2.12. Kompozycja Z koncepcyjnego punktu widzenia kompozycja oznacza, że „obiekt jest zawarty w innym obiekcie”. Jest to relacja „całość – część” ( B „zawiera” A). Na przykład obiekty typu Biurko zawierają obiekty typu Szuflada. Kompozycję uzyskujemy w prosty sposób – przez definiowanie pól obiektowych w klasie. Robimy to bez przerwy, np. stosując pola typu String. Nie tylko wykorzystanie obiektów klasy String w naszych klasach stanowi o ich ponownym użyciu. Może ono również znaleźć zastosowanie przy dostarczaniu takich metod klasy, które upraszczają uzyskiwanie informacji, np. w takim kontekście: class Text { // ... String cont; // ... String getCont() { return cont; } } Text txt = new Text(...); aby uzyskać długość tekstu musimy teraz napisać:

txt.getCont().length(); „Skrót”, który można dostarczyć, polega na zdefiniowaniu w klasie Text metody length(): public int length() { return cont.length(); } Taki rodzaj definicji nazywa się delegowaniem wywołań metod. Teraz możemy pisać prościej: txt.length(); W różnych środowiskach uruchomieniowych delegowanie odwołań przy kompozycji jest łatwe, bo zapewniona jest automatyczna generacja kodu na podstawie naszych wyborów z dialogów.

2.13. Kompozycja a dziedziczenie Można odnieść wrażenie, że dziedziczenie jest znacznie potężniejszym (dającym większe możliwości) sposobem ponownego wykorzystania klas niż kompozycja. I że – choć bardziej skomplikowane od kompozycji – daje programiście większą elastyczność oraz łatwość tworzenia i wykorzystania kodów „ponownego użytku”. Niewątpliwie wrażenie to nie jest mylne. Z dziedziczeniem są jednak związane pewne problemy. Jednym z nich jest tzw. słaba hermetyzacja kodu klasy bazowej. Rozpatrzmy przykład. Mamy ogólną klasę kontenerów na butelki i chcemy mieć różne specyficzne klasy takich kontenerów (np. skrzynki piwa lub CocaColi albo może lodówki sklepowe itd.). Oczywiście, skrzynka piwa jest kontenerem na butelki, zatem naturalne jest tu dziedziczenie (klasa BeerBox odziedziczy BottleContainer) i wykorzystanie w innej klasie (nazwanej dalej Inhe1) – kod 2.8 (s. 69 –70). class BottleContainer { private int bottlesCount; public BottleContainer(int n) { bottlesCount = n; } public int getCount() { return bottlesCount; } public String toString() {

return "BottleContainer, bottles = " + bottlesCount; } } class BeerBox extends BottleContainer { public BeerBox(int n) { super(n); } public String toString() { return "BeerBox, bottles = " + getCount(); } } class Inhe1 { public static void main(String[] args) { BeerBox bb = new BeerBox(10); int n = bb.getCount(); //... } } Tu założyliśmy, że ogólnie kontenery mogą zawierać tylko pełne butelki (stąd typ int w klasie BottleContainer). Może kiedyś zmienimy interfejs klasy bazowej w taki sposób, że metoda getCount() będzie miała inny typ wyniku – double (niepełne butelki). class BottleContainer { private double bottlesCount; public BottleContainer(double n) { bottlesCount = n; } public double getCount() { return bottlesCount; } } W tym przykładzie klasa BeerBox nie będzie wymagała żadnych zmian, ale w klasie Inhe1 wykorzystującej BeerBox wystąpią błędy w kompilacji (bo spodziewanym typem wyniku jest int, a nie double i trzeba by użyć konwersji zawężającej, aby to się skompilowało). Mówi się, że klasa BeerBox „słabo hermetyzuje” kod klasy BottleContainer, gdyż zmiana interfejsu tej klasy uniemożliwia ponowne wykorzystanie hierarchii dziedziczenia w innych klasach. Co gorsza, po takiej ewentualnej zmianie wadliwy staje się kod klas już skompilowanych i być może działających od długiego czasu. Okazuje się, że kompozycja

pozwala uniknąć takich problemów. W naszym „butelkowym” przykładzie zastosowanie kompozycji może wyglądać tak, jak przedstwia kod 2.8. class BottleContainer { private int bottlesCount; public BottleContainer(int n) { bottlesCount = n; } public double getCount() { return bottlesCount; } } class BeerBox { // wykorzystanie klasy BottleContainer przez kompozycję private BottleContainer cont; public BeerBox(int n) { cont = new BottleContainer(n); } public int getCount() { return cont.getCount(); } } class Compos1 { public static void main(String[] args) { BeerBox bb = new BeerBox(10); int n = bb.getCount(); // ... } } Kod 2.8. Kompozycja pozwala uniknąć słabej hermetyzacji klasy bazowej Mamy tu dwie klasy, które tworzą „zestaw ponownego użycia”: klasę BottleContainer i BeerBox. Przez inne klasy (np. Compos1) bezpośrednio używana jest klasa BeerBox. Klasa BottleContainer używana jest pośrednio. Jeśli teraz – podobnie jak w przykładzie z dziedziczeniem – w klasie BottleContainer zmienimy interfejs (np. zmienimy typ wyniku zwracanego przez metodę getCount() na double), to będziemy musieli zmienić

coś w implementacji klas bezpośrednio wykorzystywanych przez inne klasy (tu: BeerBox), ale ich interfejsy pozostaną bez zmian i w związku z tym nie trzeba będzie niczego zmieniać w klasach wykorzystujących nasz zestaw klas pojemników na butelki: class BottleContainer { private double bottlesCount; // ... public double getCount() { return bottlesCount; } } class BeerBox { private BottleContainer cont; // ... public int getCount() { return (int) cont.getCount(); // drobna zmiana implementacji } } // nie wymaga zmian po zmianie interfejsu klasy BottleContainer class Compos1 { public static void main(String[] args) { BeerBox bb = new BeerBox(10); int n = bb.getCount(); } } Jest to sytuacja korzystniejsza niż „słaba hermetyzacja” kodu przy dziedziczeniu, bo zwykle klasy takie jak BottleContainer i BeerBox są kontrolowane przez twórców jakiegoś API, użytkownicy API zaś zwykle korzystają z klas bezpośredniego ponownego użycia (w naszym przykładzie BeerBox). Oczywiście przy dziedziczeniu łatwiejsze niż przy kompozycji jest dodawanie nowych klas jakiegoś API (w naszym przykładzie np. lodówek, półek z wodą mineralną, pojemników z Colą). Łatwiejsze i bardziej naturalne jest też zastosowanie polimorfizmu. A czy kompozycja w ogóle umożliwia wykorzystanie odwołań polimorficznych? Na pierwszy rzut oka nie bardzo, ale w dalszych rozdziałach okaże się, że dzięki koncepcji interfejsów jest to możliwe.

2.14. Reguły ponownego wykorzystania klas Po pierwsze, należy dostosowywać sposób ponownego wykorzystania do dziedziny problemu. Dziedziczenia używamy wtedy, kiedy jest spełniona relacja „B jest A”, ale czasem okazuje się, że taka relacja jest pozorna lub może podlegać zmianom w cyklu życiowym obiektów. Dobrym testem jest postawienie pytania, czy zawsze, w każdych okolicznościach działania naszego programu można sensownie myśleć o rozszerzającej konwersji referencyjnej typu podklasy na typ nadklasy. Po drugie, z wyborem sposobu wykorzystania klas związane są kwestie efektywności: kompozycja okazuje się niekiedy bardziej pracochłonna i wolniejsza przy wykonaniu programu, ale z kolei w niektórych okolicznościach pozwala oszczędzać pamięć (tworzenie obiektu definiowanego w ramach kompozycji jako pole klasy może być odroczone w czasie do momentu, gdy będzie on potrzebny, co w danym przebiegu programu może nie nastąpić nigdy; przy dziedziczeniu zawsze wydzielana jest pamięć dla pól nadklasy). Po trzecie wreszcie, nie należy nadużywać ani dziedziczenia, ani kompozycji. Jak widzieliśmy, dziedziczenie ma swoje wady, związane przede wszystkim ze „słabą hermetyzacją” (a także „kruchością” klasy bazowej – niekiedy zmiana jej interfejsu powoduje konieczność zmian w interfejsach wielu klas dziedziczących, np. jeśli zmienimy typ zwracanego wyniku w metodzie, która w podklasach jest przedefiniowywana). Z kolei kompozycja – która pozwala unikać takich problemów – jest często mniej naturalna i w związku z tym kody klas są trudniejsze do „prowadzenia” (uzupełnień, modyfikacji), a rozbudowa API o nowe klasy dużo trudniejsza niż przy zastosowaniu dziedziczenia.

ROZDZIAŁ 3 Wyjątki

Programowanie w Javie sprowadza się do tworzenia obiektów i wywoływania na ich rzecz metod albo też, gdy nie mamy obiektów, wywoływania metod statycznych. W konstruktorze i w trakcie wykonania metody może powstać jakiś błąd. W Javie błędy wykonania programu obsługuje się za pomocą obsługi wyjątków.

3.1. Obsługa wyjątków Wyjątek to sygnał o błędzie w trakcie wykonania programu. Wyjątek powstaje na skutek jakiegoś nieoczekiwanego błędu. Jest zgłaszany lub – mówiąc inaczej – sygnalizowany. Ponadto wyjątek jest (może lub musi być) obsługiwany. Prosty schemat obsługi wyjątków: try { // ... w bloku try ujmujemy instrukcje, które mogą spowodować wyjątek } catch(TypWyjątku exc) { // ... w klauzuli catch umieszczamy obsługę wyjątku } Gdy w wyniku wykonania instrukcji w bloku try powstanie wyjątek typu TypWyjatku, sterowanie zostanie przekazane do kodu umieszczonego w powyższej klauzuli catch. Przykłady A. Brak jawnej obsługi wyjątku (kod 3.1). Powstały błąd (wyjątek) powoduje zakończenie programu, a JVM wyświetla komunikat o jego przyczynie. public class NoCatch { public static void main(String[] args) { int a = 1, b = 0, c = 0; c = a/b; System.out.println(c); } } Kod 3.1. Brak jawnej obsługi wyjątku Wynik na konsoli:

Exception in thread "main" java.lang.ArithmeticException: / by zero at NoCatch.main(NoCatch.java:6) B.Zabezpieczamy się przed możliwymi skutkami całkowitoliczbowego dzielenia przez zero, obsługując wyjątek ArithmeticException (kod 3.2). public class Catch1 { public static void main(String[] args) { int a = 1, b = 0, c = 0; String wynik; try { c = a/b; wynik = "" + c; } catch (ArithmeticException exc) { wynik = "***"; } System.out.println(wynik); } } Kod 3.2. Obsługa wyjątku ArithmeticException Mechanizm obsługi wyjątków może być wykorzystywany w bardzo różny i elastyczny sposób.Typowym przykładem jest weryfikacja danych wprowadzanych przez użytkownika. Jeśli użytkownik wprowadza na przykład napisy oznaczające liczby całkowite, to następnie na przykład za pomocą metody parseInt musimy je przekształcić na reprezentację binarną. Jeśli przy tym wprowadzony napis nie reprezentuje liczby całkowitej, to powstaje wyjątek NumberFormatException. Powinniśmy go obsługiwać. Pokazuje to kod 3.3. public static void main(String[] args) { String s1 = JOptionPane.showInputDialog("Podaj pierwszą liczbę"); if (s1 != null) { String s2 = JOptionPane.showInputDialog("Podaj drugą liczbę"); if (s2 != null) { int n1; int n2; try { n1 = Integer.parseInt(s1); n2 = Integer.parseInt(s2); } catch (NumberFormatException exc) { System.out.println("Błędne dane - kończę działanie");

return; } JOptionPane.showMessageDialog(null, "Suma: " + (n1 + n2)); } } } Kod 3.3. Obsługa wyjątku NumberFormatException Wyjątki są obiektami klas wyjątków, zatem nazwy NumberFormatException, ArithmeticException itd. są nazwami klas, a zmienna exc we wcześniejszych przykładach jest faktycznie zmienną – zawiera referencję do obiektu odpowiedniej klasy wyjątku. Wobec takiej zmiennej możemy użyć rozlicznych metod, które dostarczą nam informacji o przyczynie powstania wyjątku. Niektóre z nich są przedstawione w tab. 3.1. Tab. 3.1. Wybrane metody klas wyjątków Metoda String getMessage()

Sposób działania zwraca napis zawierający informację o wyjątku (np. błędne dane lub indeks)

wyświetla na konsoli informacje o wyjątku oraz sekwencje void printStackTrace() wywołań metody, która doprowadziła do powstania wyjątku (stos wywołań). Wersje tej metody pozwalają te informacje zapisywać w plikach (logach) String toString()

zwraca informację o wyjątku (zazwyczaj nazwę klasy wyjątku oraz dodatkową informację uzyskiwaną przez getMessage())

Throwable getCause()

zwraca wyjątek niższego poziomu, który spowodował powstanie tego wyjątku, albo null, jeśli takiego wyjątku niższego poziomu nie było lub nie został zidentyfikowany (zastosowaniem tej metody nie będziemy się teraz zajmować – przydaje się ona np. przy obsłudze wyjątków SQL)

Sprawdźmy na przykładzie, jakie informacje możemy uzyskać o wyjątku (zob. kod 3.4). class ReportExc { public ReportExc() { wykonaj(); }

public void wykonaj() { try { int num = Integer.parseInt("1aaa"); } catch (NumberFormatException exc) { System.out.println("Co podaje getMessage()"); System.out.println( exc.getMessage()); System.out.println("Co podaje toString()"); System.out.println(exc); System.out.println("Wydruk śladu stosu (kolejność wywołań metod)"); exc.printStackTrace(); System.exit(1); } } public static void main(String[] args) { new ReportExc(); } } Kod 3.4. Informacje o wyjątku Program ten wyświetli informacje przedstawione na listingu 3.1. Co podaje getMessage() For input string: "1aaa" Co podaje toString() java.lang.NumberFormatException: For input string: "1aaa" Wydruk śladu stosu (kolejność wywołań metod) java.lang.NumberFormatException: For input string: "1aaa" at java.lang.NumberFormatException.forInputString(Unknown Source) at java.lang.Integer.parseInt(Unknown Source) at java.lang.Integer.parseInt(Unknown Source) at ReportExc.wykonaj(ReportExc.java:11) at ReportExc.(ReportExc.java:6) at ReportExc.main(ReportExc.java:24) Listing 3.1. Wynik działania kodu 3.4 Warto zwrócić uwagę, że wydruk „śladu” stosu (stack trace) odtwarza całą sekwencję wywołań metody, która doprowadziła do powstania wyjątku. Czytamy od góry: wyjątek został zgłoszony w metodzie parseInt() klasy java.lang.Integer. Ta informacja dotyczy klas standardowych pakietów Javy, a nie naszego kodu, ale bywa pomocna. Metoda parseInt została wywołana przez metodę wykonaj() z klasy ReportExc w wierszu 11 pliku źródłowego (tu po raz pierwszy widzimy nasz plik źródłowy na wydruku – zatem tu, w wierszu 11, tkwi przyczyna błędu). Jaki to błąd? Nieprawidłowy format danych. Jakich? 1aaa. To wszystko mamy podane.

Zauważmy jeszcze, że słowo oznacza konstruktor, że metoda wykonaj() została wywołana z tego konstruktora (w wierszu 6) klasy ReportExc, a konstruktor był wywołany z metody main w wierszu 24 pliku źródłowego. Klasy wyjątków tworzą dość rozbudowaną hierarchię dziedziczenia (zob. rys. 3.1, na którym pokazano jej fragmenty).

Rys. 3.1. Hierarchia klas wyjątków Wszystkie klasy wyjątków pochodzą od klasy Throwable. Mamy następnie dwie wyróżnione klasy, Error i Exception. Od klasy Exception pochodzi klasa RuntimeException oraz wiele innych. Podstawowa różnica między tymi klasami polega na tym, że wyjątki klas pochodnych od Throwable, ale nie od Error i RuntimeException są tzw. wyjątkami kontrolowanymi, co oznacza, że musimy zapewnić ich obsługę (kompilator zgłosi błąd, jeśli tego nie zrobimy). Pozostałych wyjątków obsługiwać nie musimy (ale możemy), co już widzieliśmy na przykładzie NumberFormatException czy ArithmeticException. Zapewnienie obsługi wyjątku oznacza: • albo ujęcie instrukcji, która może go spowodować, w blok try-catch; • albo przesunięcie obsługi do kodu, który wywołuje blok, w którym zawarta jest ta instrukcja. Zobaczmy przykład. W kodzie 3.5 chcemy wyświetlić jakiś tekst, wstrzymać wykonanie programu na 2 sekundy i następnie wyświetlić inny tekst. W celu uśpienia programu zastosujemy statyczną metodę sleep z klasy Thread z argumentem równym liczbie milisekund, którą program ma odczekać przed podjęciem dalszego wykonania. public class ControlledExc { public void printWaitAndPrint() { System.out.println("Za 2 sekundy wypiszę następny tekst");

Thread.sleep(2000); System.out.println("Następny tekst"); } public static void main(String[] args) { new ControlledExc().printWaitAndPrint(); } } Kod 3.5. Wyjątki kontrolowane bez odpowiedniej obsługi spowodują błędy w kompilacji Niestety, ten kod się nie skompiluje poprawnie, ponieważ metoda sleep może zgłosić kontrolowany wyjątek klasy InterruptedException (przerwanie stanu oczekiwania). Rysunek 3.2 pokazuje błąd kompilacji sygnalizowany w Eclipse.

Rys. 3.2. Wyjątki kontrolowane – błędy kompilacji 1 Po kliknięciu ikony błędu Eclipse zaproponuje dwa możliwe rozwiązania problemu (zob. rys. 3.3).

Rys. 3.3. Możliwości obsługi wyjątków kontrolowanych Wybranie opcji Surround... spowoduje dopisanie bloku try-catch wokół wywołania metody sleep() (propozycja widoczna po prawej). Wybranie opcji Add throws declaration spowoduje natomiast dopisanie deklaracji throws InterruptedException do nagłówka metody printWaitAndPrint()i oznacza, że przy powstaniu tego wyjątku zostanie on przez nią zgłoszony. To z kolei przeniesie obowiązek obsługi do miejsca, które wywołuje metodę printWaitAndPrint(). Deklaracja throws używana jest w nagłówku definicji metody (lub konstruktora) i ma postać: throws TypWyj1, TypWyj2, ... , TypWyjN Oznacza to, że dana metoda może zgłaszać wyjątki podanych typów., np.: public void metoda() throws InterruptedException { // ... ciało metody // ... może tu powstać wyjątek InterruptedException } Po wyborze opcji Add throws declaration metoda printWaitAndPrint będzie miała postać: public void printWaitAndPrint() throws InterruptedException { System.out.println("Za 2 sekundy wyświetlę następny tekst"); Thread.sleep(2000); System.out.println("Następny tekst"); } i kompilator nie będzie się czepiał jej kodu. Ale ponieważ metodę tę wywołujemy z main:

public static void main(String[] args) { new ControlledExc().printWaitAndPrint(); } problem przeniesie się w miejsce wywołania (wyjątki kontrolowane muszą być obsługiwane – tu czy gdzie indziej) – zob. rys. 3.4.

Rys. 3.4. Wyjątki kontrolowane – błędy kompilacji 2 Poprawny kod będzie zatem wyglądał tak jak kod 3.6. public class ControlledExc { public void printWaitAndPrint() throws InterruptedException { System.out.println("Za 2 sekundy wyświetlę następny tekst"); Thread.sleep(2000); System.out.println("Następny tekst"); } public static void main(String[] args) { try { new ControlledExc().printWaitAndPrint();

} catch (InterruptedException exc) { exc.printStackTrace(); } } } Kod 3.6. Możliwa wersja poprawnej obsługi wyjątków Naturalnie moglibyśmy obsłużyć ten wyjątek również w samej metodzie printWaitAndPrint() i wtedy niepotrzebna byłaby deklaracja throws ani obsługa w main(..). Z obsługą wyjątków wiążą się jeszcze dwie ważne kwestie. Przede wszystkim w bloku try {...} może powstać (być zgłoszonych) wiele wyjątków różnych typów. Dlatego możemy mieć wiele klauzul catch. A dodatkowo możemy (ale nie musimy) zapisać klauzulę finally, w której zawrzemy jakiś kod, który ma się wykonać niezależnie od tego, czy przy wykonaniu bloku try jakiś wyjątek powstał, czy nie. Kod w klauzuli finally służy zwykle do porządkowania zasobów (np. zamykania połączeń bazodanowych czy sieciowych). Zatem ogólna postać bloku try {...} wygląda tak: try { // instrukcje wykonywane w bloku try } catch (TypWyj1 exc) { // obsluga wyjątku typu TypWyj1 } catch (TypWyj2 exc) { // obsluga wyjątku typu TypWyj2 } catch (MTypWyj1 | MTypWyj2 | ... | MTypWyjN exc) { // obsługa wyj. MTypWyj1 ... MTypWyjN } // ... } catch (TypWyjN exc) { // obsluga wyjatku typu TypWyjN } finally { // kod wykonywany niezależnie do tego czy wyjatek powstał czy nie } Uwaga. Dopuszczalne formy bloku try obejmują try z jedną lub więcej klauzulą catch: • try z jedną lub więcej klauzulą catch i finally, • try bez catch, ale z finally. Sekwencja działania jest następująca: • Wykonywane są kolejne instrukcje bloku try. • Jeśli przy wykonaniu którejś z instrukcji zostanie zgłoszony wyjątek, wykonanie bloku try jest przerywane w miejscu zgłoszenia wyjątku.

• Sterowanie przekazywane jest do pierwszej w kolejności klauzuli catch, w której w nawiasach okrągłych po słowie catch podany jest jeden typ wyjątku lub któryś z typów wyjątku rozdzielonych znakiem | pasuje do typu powstałego wyjątku. Słowo „pasuje” oznacza tu, że podana w klauzuli klasa wyjątku jest taka sama jak klasa powstałego wyjątku lub jest jej dowolną nadklasą. • Inne klauzule catch nie są wykonywane. • Obsługująca wyjątek klauzula catch może zrobić wiele rzeczy, m.in. zmienić sekwencję sterowania (np. przez return). Jeśli nie zmienia sekwencji sterowania, to wykonanie programu będzie kontynuowane od następnej instrukcji po bloku try. • Niezależnie od tego, czy wyjątek powstał, czy nie, i niezależnie od tego, czy obsługująca go klauzula catch zwraca sterowanie, czy nie, zawsze zaraz po zakończeniu działania bloku try zostanie wykonana klauzula finally (o ile jest). Jeśli obsługujemy wyjątek za pomocą try-catch, to albo w klauzuli catch powinniśmy poprawić błąd, albo ponownie go zgłosić, albo – w ostateczności – wyświetlić informację o błędzie. Nigdy nie pozostawiajmy pustego catch!

3.2. Zgłaszanie wyjątków

Do zgłaszania wyjątków służy instrukcja sterująca throw. Ma ona postać: throw excref; gdzie excref to referencja do obiektu klasy wyjątku, np.: throw new NumberFormatException("Wadliwy format liczby: " + liczba); W istocie instrukcja throw jest sposobem specyficznego przekazywania sterowania do jakichś punktów programu (do miejsc obsługi wyjątku). Należy jednak korzystać z niej wyłącznie w celu sygnalizowania błędów. Zwykle w naszym kodzie będziemy sprawdzać warunki powstania błędu i jeśli były spełnione (wystąpił błąd), zgłaszać wyjątek. Jak zobaczymy, można tworzyć własne klasy wyjątków i zgłaszać własne wyjątki. Nie należy jednak tego nadużywać, bo w Javie dostępna jest duża liczba gotowych, standardowo nazwanych klas wyjątków i warto z nich właśnie korzystać. Typowe, gotowe do wykorzystania klasy wyjątków opisujących częste rodzaje błędów fazy wykonania programu zawiera tab. 3.2. Tab. 3.2. Wybrane standardowe typy wyjątków Klasa wyjątku IllegalArgumentException

Znaczenie przekazany metodzie lub konstruktorowi argument jest niepoprawny

IllegalStateException

stan obiektu jest wadliwy w kontekście wywołania danej metody

NullPointerException

referencja ma wartość null w kontekście, który tego zabrania

IndexOutOfBoundsException

indeks wykracza poza dopuszczalne zakresy

ConcurrentModificationException modyfikacja obiektu jest zabroniona UnsupportedOperationException

operacja (na obiekcie) jest niedopuszczalna (obiekt nie udostępnia tej operacji)

Możemy także wykorzystywać inne klasy, takie jak NumberFormatException (błąd formatu liczby) czy NoSuchElementException (wyjątek sygnalizowany, gdy przy rozbiorze tekstów lub w iteracjach po kolekcjach danych staramy się sięgnąć do nieistniejącego elementu). Zwróćmy uwagę, że wszystkie wymienione wyżej wyjątki są niekontrolowane, bo pochodzą od klasy RuntimeException, co ułatwia korzystanie z nich, ponieważ nie zmusza programisty do ich obsługi. Nie musimy również takich wyjątków podawać w klauzuli throws w deklaracji metody, która je zgłasza (ale możemy, co sprzyja lepszej dokumentacji kodu). Ważne jednak, by tworząc i zgłaszając wyjątek jakiejś standardowej klasy, podać przy wywołaniu konstruktora informację o przyczynie wyjątku. Przykładowo, w klasie definiującej konta bankowe możemy mieć metodę withdraw (double d), za pomocą której jest dokonywana wypłata z konta w kwocie d. Oczywiście nie można wypłacić sumy mniejszej lub równej 0. Załóżmy, że nie można też wypłacić więcej, niż wynosi aktualny stan konta. Oba błędy możemy sygnalizować zgłoszeniem wyjątku IllegalArgumentException (zob. kod 3.7). public class Account { // ... private double balance; // ... public void withdraw(double d) throws IllegalArgumentException { if (d = words.length) throw new IllegalArgumentException(errMsg + "\nIndeks: " + i); words[i] = w;; } Ale jest też prostszy i może bardziej elegancki sposób, polegający właśnie na ponownym zgłoszeniu wyjątku: public void set(int i, String w) { try { if (w.equals("")) throw new IllegalArgumentException("Pusty String"); words[i] = w; } catch (Exception exc) { throw new IllegalArgumentException("Metoda Words.set(int, String). Wadliwy argument:\n" + exc); } } Tutaj sprawdzenie zakresu indeksów oraz niedopuszczalnej wartości null pozostawiamy Javie. W przypadku błędnych argumentów powstaną wyjątki NullPointerException lub ArrayIndexOutOfBoundsException. Sprawdzamy tylko, czy napis nie jest przypadkiem pusty (jeśli tak – zgłosimy wyjątek). Wszystkie te wyjątki będziemy obsługiwać w bloku try-catch, a obsługa polega na zgłoszeniu nowego wyjątku IllegalArgumentException, który będzie uogólniał wszystkie specyficzne błędy (jako błąd argumentu) i podawał także informację o konkretnej przyczynie błędu. Należy podkreślić, że często „przetłumaczenie” powstającego wyjątku na wyższy poziom abstrakcji jest istotne. Oto np. metody next() i previous() zwykle zwracają następny i poprzedni element jakiejś struktury danych. W przypadku naszej tablicy słów w klasie Words będzie to następne lub poprzednie słowo. Całkiem szybko moglibyśmy napisać tak: class Words { private String[] words;

private int currIndex = 0; // ... public String next() { return words[currIndex++]; } public String previous() { return words[--currIndex]; } Wtedy w metodach next() i previus() mogą wystąpić wyjątki ArrayIndexOutOfBoundsException. Ale to, że słowa przechowujemy w tablicy (a nie w jakiejś innej strukturze danych), jest tylko właściwością tej konkretnej implementacji. Ogólniejszy kontrakt dla metod next lub previous powinien od implementacji abstrahować – niemożność uzyskania następnego lub poprzedniego elementu-słowa powinna być zgłaszana raczej jako wyjątek typu NoSuchElementException. Zatem powinniśmy napisać raczej tak: public String next() { String word = null; try { word = words[currIndex++]; } catch (ArrayIndexOutOfBoundsException exc) { throw new NoSuchElementException( "Brak elementu na pozycji " + exc.getMessage() ); } return word; } i podobnie zmodyfikować metodę previous(). Bardzo ważne jest też, by przy zgłaszaniu wyjątku zachować dopuszczalny stan obiektu, żeby ewentualna obsługa wyjątku mogła naprawić błąd. W pokazanych wyżej kodach metod next i previous warunek ten nie jest spełniony, np. po wprowadzeniu dwóch słów "a" i "b" poniższy fragment: Words w = new Words("a b"); try { System.out.println(w.next()); System.out.println(w.next()); System.out.println(w.next()); } catch (Exception exc) { System.out.println(exc);

System.out.println("Do tyłu jeden krok:"); System.out.println("Previous daje: " + w.previous()); System.out.println("I teraz next(): " + w.next()); } nie da spodziewanych wyników, ponieważ po powstaniu błędu w metodzie next stan obiektu (currIndex) jest wadliwy. Wynik będzie następujący: a b java.util.NoSuchElementException: Brak elementu na pozycji 2 Do tyłu jeden krok: java.util.NoSuchElementException: Brak elementu na pozycji 2 Przyczyna błędu leży w niedopuszczalnych zmianach bieżącego indeksu. Powinniśmy raczej napisać tak (zmieniając również kod metody previous()): public String next() { String word = null; try { word = words[currIndex]; currIndex++; } catch (ArrayIndexOutOfBoundsException exc) { throw new NoSuchElementException("Brak elementu na pozycji " + exc.getMessage() ); } return word; } public String previous() { String word = null; try { word = words[--currIndex]; } catch (ArrayIndexOutOfBoundsException exc) { currIndex++; throw new NoSuchElementException(;"Brak elementu na pozycji " + exc.getMessage() ); } return word; } Wówczas taki fragment programu: Words w = new Words("a b"); try { System.out.println(w.next());

System.out.println(w.next()); System.out.println(w.next()); }catch (Exception exc) { System.out.println(exc); System.out.println("Do tyłu jeden krok:"); System.out.println("Previous daje: " + w.previous()); System.out.println("I teraz next(): " + w.next()); } System.out.println("Odwrotnie"); try { System.out.println(w.previous()); System.out.println(w.previous()); System.out.println(w.previous()); }catch (Exception exc) { System.out.println(exc); System.out.println("Do przodu jeden krok:"); System.out.println("Next daje: " + w.next()); System.out.println("I teraz Previous(): " + w.previous()); } da właściwe wyniki: a b java.util.NoSuchElementException: Brak elementu na pozycji 2 Do tyłu jeden krok: Previous daje: b I teraz next(): b Odwrotnie b a java.util.NoSuchElementException: Brak elementu na pozycji -1 Do przodu jeden krok: Next daje: a I teraz Previous(): a Warto też pokazać technikę selekcji dopuszczalnych operacji za pomocą zgłaszania wyjątków. Możemy np. łatwo dostarczyć klasę niemodyfikowalnych słów UnmodifiableWords, dziedzicząc klasę Words i przedefiniowując metodę set(..) w następujący sposób: class UnmodifiableWords extends Words { public UnmodifiableWords(String s) { super(s); } public void set(int i, String w) { throw new UnsupportedOperationException("Operacja set jest

niedopuszczalna w klasie " + getClass().getName() ); } }

3.4. Niskopoziomowe przyczyny i łańcuchowanie wyjątków Każdy wyjątek może zawierać odniesienie do obiektu klasy innego wyjątku (wyjątku niższego poziomu). Jest to zazwyczaj wyjątek, który spowodował błąd, ale został przechwycony w metodzie przy zgłaszaniu wyjątku logicznie wyższego poziomu. Ten „niskopoziomowy” wyjątek nazywa się przyczyną (cause) wyjątku wyższego poziomu i może być: • zapakowany do wyjątku wyższego poziomu za pomocą konstruktora z jednym z argumentów typu Throwable albo za pomocą metody initCause(Throwable) wywołanej na rzecz stworzonego wyjątku wyższego poziomu; • uzyskany przy obsłudze wyjątku wyższego poziomu za pomocą metody Throwable getCause(). Łańcuchowanie wyjątków ilustruje nowa wersja metody set z omawianej klasy Words oraz jej wykorzystanie: class Words { // ... public void set(int i, String w) { try { if (w.equals("")) throw new IllegalArgumentException("Pusty String"); words[i] = w; } catch (Throwable lowLevelException) { IllegalArgumentException highLevelExc = new IllegalArgumentException("Metoda Words.set(int, String).Wadliwy argument" ); highLevelExc.initCause(lowLevelException); throw highLevelExc; } } // ... } // gdzie indziej: Words w = new Words(data);

try { w.set(10, "ala"); } catch (IllegalArgumentException exc) { System.out.println(exc); System.out.println("Przyczyna - " + exc.getCause()); } Ten fragment programu może wyświetlić następujące informacje: java.lang.IllegalArgumentException: Metoda Words.set(int, String).Wadliwy argument Przyczyna - java.lang.ArrayIndexOutOfBoundsException: 10

3.5. Wykorzystanie informacji o śladzie stosu Metoda getStackTrace() umożliwia programistyczny dostęp do informacji o sekwencji wywołań metody, która spowodowała dany wyjątek. Metoda zwraca tablicę, której kolejne elementy zawierają informacje o kolejnych punktach wykonania programu prowadzących do wyjątku. Elementy tablicy są typu StackTraceElement i na ich rzecz możemy używać metod podanych w tab. 3.3. Tab. 3.3. Wybrane metody klasy StackTraceELement Metoda

Sposób działania

String getClassName()

zwraca nazwę klasy, w której znajduje się punkt wykonania opisywany przez ten element

String getFileName()

zwraca nazwę pliku źródłowego, zawierającego ten punkt wykonania

getLineNumber()int zwraca numer wiersza w pliku źródłowym String getMethodName()

zwraca nazwę metody zawierającej punkt wykonania opisywany przez ten element

Korzystając z tego mechanizmu, moglibyśmy np. napisać uniwersalną metodę zgłaszania wyjątku „wadliwe argumenty”: private void throwIllegalArgumentException(RuntimeException lowLevelException) { StackTraceElement[] st = lowLevelException.getStackTrace(); String errMsg = "Wadliwy argument metody " +

st[0].getMethodName() + "\nZdefiniowanej w klasie " + st[0].getClassName() + " z pliku " + st[0].getFileName() + "\nArgument dostarczono przy wywolaniu metody " + st[0].getMethodName() + " z metody " + st[1].getMethodName() + " klasy " + st[1].getClassName() + " w wierszu " + st[1].getLineNumber() + " pliku " + st[1].getFileName(); throw (IllegalArgumentException) new IllegalArgumentException(errMsg).initCause(lowLevelException); } i użyć jej tak: public void set(int i, String w) { try { if (w.equals("")) throw new IllegalArgumentException("Pusty String"); words[i] = w; } catch (RuntimeException lowLevelException) { throwIllegalArgumentException(lowLevelException); } } // kod w metodzie main Words w = new Words(data); try { w.set(10, "ala"); } catch (IllegalArgumentException exc) { System.out.println(exc.getMessage()); System.out.println("Przyczyna - " + exc.getCause()); } co może dać takie wyniki: Wadliwy argument metody set Zdefiniowanej w klasie Words z pliku Throwing.java Argument dostarczono przy wywolaniu metody set z metody main klasy Throwing w wierszu 133 pliku Throwing.java Przyczyna - java.lang.ArrayIndexOutOfBoundsException: 10 Dostęp do elementów śladu stosu umożliwia uzyskanie informacji skąd (z jakiej metody i z jakiej klasy) została wywołana dana metoda. W tym celu stosujemy prosty trik – zgłoszenie i obsłużenie dowolnego wyjątku, np.:

public void metoda() { String callingClass = null; String callingMethod = null; try { throw new Throwable(); } catch (Throwable t) { StackTraceElement[] st = t.getStackTrace(); callingClass = st[1].getClassName(); callingMethod = st[1].getMethodName(); } if (callingClass.equals(getClass().getName()) && callingMethod.equals("")){ System.out.println("Nie wywołuj mnie z konstruktora mojej klasy"); } else if (callingMethod.equals("main")) { System.out.println("Nie wywołuj mnie z metody main"); } }

ROZDZIAŁ 4 Interfejsy i klasy wewnętrzne

Interfejsy pełnią istotną funkcję w programowaniu. Dzięki ich zastosowaniu można tworzyć uniwersalne i elastyczne kody. Są również podstawą dla wprowadzonych w Javie 8 lambdawyrażeń. Ciekawy jest też rozwój koncepcji interfejsów w Javie w wersjach 8 i 9. A w zastosowaniu interfejsów ważne znaczenie mają klasy wewnętrzne.

4.1. Metody i klasy abstrakcyjne Metoda abstrakcyjna nie ma implementacji (ciała) i powinna być zadeklarowana ze specyfikatorem abstract. abstract int getSomething(); // nie ma ciała - tylko średnik Klasa, w której zadeklarowano jakąkolwiek metodę abstrakcyjną, jest klasą abstrakcyjną i musi być opatrzona specyfikatorem abstract, np.: abstract class SomeClass { int n; abstract int getSomething(); void say() { System.out.println("Coś tam") }; }

Do czego służą metody abstrakcyjne? Metody abstrakcyjne to takie, których konkretnej implementacji jeszcze nie określiliśmy (lub nie chcemy tego przesądzać), ale wiemy, że powinny się znaleźć w zestawie metod każdej konkretnej klasy dziedziczącej klasę abstrakcyjną. Konkretna implementacja (definicja w klasie kodu metody) może być bardzo różna, w zależności od konkretnego rodzaju obiektów, które opisuje dana klasa. Klasa abstrakcyjna nie musi mieć metod abstrakcyjnych. Wystarczy zadeklarować ją ze specyfikatorem abstract. Abstrakcyjność klasy oznacza, iż nie można tworzyć jej egzemplarzy (obiektów). Moglibyśmy więc zadeklarować klasę Zwierz ze specyfikatorem abstract, myśląc w ten sposób: nie chcemy bezpośrednio tworzyć obiektów klasy Zwierz. Cóż to jest Zwierz? To dla nas jest – być może – czysta abstrakcja.

Abstrakcyjna klasa Zwierz może być natomiast dziedziczona przez klasy konkretne, np. Pies czy Kot albo może Tygrys, co daje im już pewne zagwarantowane cechy i funkcjonalność. Dopiero z tymi konkretnymi typami zwierząt możemy się jakoś obchodzić, a zestaw metod wprowadzonych w klasie Zwierz daje nam po temu ustalone środki. Skoro Zwierz jest abstrakcyjny, to zestaw jego metod (tu: do jakiegoś stopnia) też może być abstrakcyjny: abstract class Zwierz { String name = "bez imienia"; Zwierz() {} Zwierz(String s) { name = s; } abstract String getTyp(); abstract String getVoice(); String getName() { return name; } void speak() { System.out.println(getTyp()+" "+getName()+" mówi "+getVoice()); } } Metody getTyp() i getVoice() są abstrakcyjne (nie dostarczyliśmy ich implementacji, bo zależy ona od konkretnego Zwierza). Co więcej, są – jak domyślnie wszystkie metody w Javie – wirtualne, czyli: • mogą mieć różne definicje przy konkretyzacji, • nieznany jest (jeszcze) dokładnie sposób ich działania, • niekoniecznie już istnieją. W tym kontekście metoda speak() staje się jeszcze ciekawsza. Oto używamy w niej nieistniejących jeszcze metod! Możemy się odwoływać do czegoś, co może dopiero powstanie. Co może mieć wiele różnorodnych konkretnych kształtów, teraz nam jeszcze nieznanych. Konkretyzacje następują w klasach pochodnych, gdzie implementujemy (definiujemy) abstrakcyjne metody. Klasa dziedzicząca klasę abstrakcyjną musi zdefiniować wszystkie abstrakcyjne metody tej klasy albo sama będzie klasą abstrakcyjną i wtedy jej definicja musi być opatrzona specyfikatorem abstract. A zatem, by móc tworzyć i posługiwać się obiektami klas Pies i Kot, musimy zdefiniować w tych klasach abstrakcyjne metody klasy Zwierz. class Pies extends Zwierz { // ... String getTyp() { return "Pies"; } String getVoice() { return "HAU, HAU!"; }

} class Kot extends Zwierz { // ... String getTyp() { return "Kot"; } String getVoice() { return "Miauuuu..."; } } Możliwość deklarowania metod abstrakcyjnych można też traktować jako pragmatyczne ułatwienie. Nie musimy wymyślać (i zapisywać) sztucznej funkcjonalności, sztucznego działania na zbyt abstrakcyjnym poziomie (jak np. return "jakiś zwierz" czy return "?").

4.2. Pojęcie interfejsu Interfejs klasy to sposób komunikowania się z jej obiektami. Inaczej – zestaw jej metod dostępnych do użycia z poziomu innej klasy. Implementacja określa konkretne definicje metod interfejsu oraz zestaw niestatycznych pól klasy. W Javie słowo „interfejs” ma też dodatkowe, specjalne „techniczne” znaczenie, odwołujące się zresztą do ogólnego pojęcia interfejsu. Mianowicie za pomocą słowa kluczowego interface możemy wprowadzić deklarację zestawu metod abstrakcyjnych (a jak zobaczymy dalej – nie tylko abstrakcyjnych). To jakby trochę biedniejsza klasa, a podstawowy sens takiego zabiegu polega na tym, że inne klasy mogą implementować taki interfejs (czyli jego metody) i w ten sposób spełniać wymagania ustalonego kontraktu. Na przykład, jeśli wymagamy by jakaś klasa definiowała metodę public void run(), możemy to zapewnić, implementując interfejs Runnable, który deklaruje taką metodę, a inne moduły czy klasy Javy będą wtedy „wiedziały”, że na rzecz obiektu tej klasy można wywołać metodę run().

4.3. Problem wielodziedziczenia Oprócz podstawowego powodu stosowania interfejsów są i inne. Zacznijmy od problemu wielodziedziczenia, rozważając następujący przykład. Nie wszystkie zwierzęta wydają głos. Zatem umieszczanie (abstrakcyjnej) metody getVoice() oraz metody speak() w klasie Zwierz nie jest najlepszym rozwiązaniem. Co więcej, nie tylko zwierzęta mówią. Chciałoby się więc mieć klasę obiektów wydających głos, którą mógłby dziedziczyć np. Wodospad i Pies. Ale Pies jest Zwierzem (dziedziczy Zwierza) i nie może odziedziczyć klasy obiektów „wydających głos”. W Javie nie ma bowiem wielodziedziczenia klas: każda klasa może dziedziczyć bezpośrednio tylko jedną klasę.

Unikanie wielodziedziczenia klas w Javie wynika z niejednoznaczności związanych z niektórymi postaciami wielodziedziczenia (np. z tzw. problemem diamentu lub rombu wielodziedziczenia, która to nazwa wynika z kształtu relacji między klasami – zob. rys. 4.1). Wyobraźmy sobie na chwilę, że w Javie jest wielodziedziczenie klas i spróbujmy zbudować klasę Child, które niewątpliwie dziedziczy po Father i po Mother, klasach będących swoistymi specjalizacjami klasy Human.

Rys. 4.1. Romb wielodziedziczenia Niech w klasie Human znajduje się abstrakcyjna metoda String getSex(), a w klasach Father i Mother zdefiniujemy tę metodę tak, by zwracała właściwą dla każdego z rodziców płeć – np. „kobieta” lub „mężczyzna”. abstract class Human { public abstract String getSex(); }

class Father extends Human { public String getSex() { return „male”; } } class Mother extends Human { public String getSex() { return „female”; } } class Child extends Mother, Father { // hipotetyczne wielodziedziczenie // klasa Child nie przedefiniowuje metody getSex()} Pytanie: jaki wynik dla obiektu-dziecka (klasy Child) zwróci wywołanie metody getSex()? Oczywiście, w tym konkretnym przypadku nie wiadomo, chociaż można dość łatwo z tej sytuacji wybrnąć, np. zmuszając do przedefiniowania metody getSex() w klasie Child. Większy kłopot przy wielodziedziczeniu sprawiają pola niestatyczne: class A { public int a; } class B extends A {

// ma pole a

} class C extends A {

//

ma pole a

} class D extends B i C { // hipotetyczne wielodziedziczenie } Obiekt d z klasy D ma element definiowany przez pole a. Jeden czy dwa? Jeśli jeden, to który? Czy ten, który należy do obiektu B, czy pochodzący z obiektu C, czy może to jest ten sam element definiowany przez pole a klasy A? I jak rozumieć naturalne odwołanie d.a ? Oczywiście zawsze można przyjąć jakieś arbitralne ustalenia (np. wynikające z kolejności wymieniania klas przy wielodziedziczeniu). Ale Java tego nie rozstrzyga: po prostu unika wielodziedziczenia klas (ale uwaga: poczynając od wersji 8, mamy możliwość wielodziedziczenia konkretnych implementacji metod, o czym będzie mowa dalej). Pies jest Zwierzem (dziedziczy właściwości Zwierza). W Javie nie może dodatkowo odziedziczyć funkcjonalności klasy „obiektów wydających głos”. Ale byłoby to bardzo wskazane.

4.4. Definiowanie i implementowanie interfejsów

Pewne rozwiązanie tego problemu uzyskano, wprowadzając w Javie (jako element składni języka) pojęcie interfejsu, jakby „biedniejszej” klasy. Interfejs (deklarowany za pomocą słowa kluczowego interface) to: • zestaw publicznych metod abstrakcyjnych, • i/lub publicznych stałych statycznych.

Poczynając od wersji 8 Javy, w definicji interfejsu mogą się też znaleźć definicje (konkretne implementacje) publicznych metod statycznych oraz publicznych metod niestatycznych, poprzedzane słowem kluczowym default. W wersji 9 dodano możliwość definiowania w interfejsach metod prywatnych. Implementacja interfejsu w klasie to zdefiniowanie w tej klasie wszystkich abstrakcyjnych metod interfejsu. To, że klasa ma implementować interfejs X, oznaczamy słowem kluczowym implements. Na przykład interfejs określający abstrakcyjną funkcjonalność „wydającego głos” mógłby wyglądać tak: public interface Speakable { int QUIET = 0; // = size; } }); return files; } Kod 4.6. Metoda zwracająca ze wskazanego katalogu pliki o określonym rozszerzeniu i rozmiarze większym od podanego W Javie w wersji 8 wprowadzono wygodne pojęcie zmiennej efektywnie finalnej (effectively final). W uproszczeniu: jest to taka zmienna, co do której kompilator może stwierdzić, że nie zmienia swoich wartości (np. nie występuje z lewej strony przypisań). Dzięki temu osłabiono wymaganie dostępu do zmiennych lokalnych z anonimowych klas wewnętrznych: takie zmienne nie muszą już być deklarowane ze specyfikatorem final, ale muszą być efektywnie finalne. Zatem z kodu 4.6 można usunąć wszystkie (niewiele przecież wnoszące) słówka final. Gdybyśmy jednak w ciele metody getFiles zapisali: withExt = withExt.startsWith(".") ? withExt :"." + withExt; to i w wersji 8 kompilator zgłosiłby błąd „Variable withExt is required to be final or effectively final”. Dlaczego w Javie obowiązuje wymaganie niezmienności wartości zmiennych lokalnych, gdy są używane w lokalnych klasach wewnętrznych?

Zauważmy, że obiekt klasy wewnętrznej jest odrębnym bytem. Ma dostęp do pól klasy otaczającej (elementów obiektu, na którym się opiera), ale tylko dlatego, że zawiera referencję do tego obiektu. Przy tworzeniu obiektu klasy wewnętrznej ta referencja jest zapisywana w „jego środku”. Można się do tego obiektu zawsze odwołać, ponieważ istnieje (na stercie) niezależnie od obiektu klasy wewnętrznej i metody, dla której ewentualnie ta klasa jest lokalna. Natomiast zmienne lokalne kończą swój byt po zakończeniu działania metody (a obiekt wewnętrznej klasy lokalnej może dalej istnieć i powinien móc operować na wartościach zmiennych lokalnych). W Javie (na razie) wybrano proste rozwiązanie tego problemu: skopiowanie wartości zmiennych lokalnych „do środka” obiektu klasy wewnętrznej. Gdyby więc wartości zmiennych lokalnych, do których odwołuje się klasa wewnętrzna, mogły się zmieniać, to mogłaby powstać niespójność między kopią i oryginałem. Dlatego zmiany są zabronione i konieczny jest specyfikator final lub efektywna niezmienność (w Javie 8).

ROZDZIAŁ 5 Typy i metody sparametryzowane ( generics)

Pojęcie szablonu (template) z języka C++ zrobiło oszałamiającą karierę. W różnych innych językach są podobne konstrukcje. Również w Javie – od wersji 1.5 – mamy możliwość tworzenia czegoś na podobieństwo szablonów z C++. Mechanizm generics z Javy różni się jednak znacząco od szablonów znanych z innych języków. Czym są generic w Javie? Jak je definiować? Jakie mają ograniczenia? I jakie użyteczne zastosowania?

5.1. Definiowanie typów sparametryzowanych Typy surowe i czyszczenie typów Typ sparametryzowany to wyznaczany przez nazwę klasy lub interfejsu typ z dołączonym co najmniej jednym parametrem. Definicję typu sparametryzowanego wprowadzamy słowem kluczowym class lub interface, podając po nazwie klasy lub interfejsu parametry w nawiasach kątowych. Parametrów tych następnie używamy w ciele klasy (interfejsu) w miejscu „normalnych” typów. Elementy składni typu sparametryzowanego są następujące: class | interface ParametrTypuN> { //.... }

Nazwa < ParametrTypu1, ParametrTypu2, ...

Wprowadzenie generics (typów sparametryzowanych) w Javie 5 pozwoliło na pewne ułatwienia w pisaniu programów, polegające głównie na: • możliwości unikania konwersji zawężających; • wykrywaniu błędów w fazie kompilacji i unikaniu wyjątku ClassCastException w fazie wykonania. Szczególnie użyteczna w tym kontekście jest parametryzacja kolekcji i był to niewątpliwie główny powód wprowadzenia generics do Javy. Przykład sparametryzowanej klasy Para przedstawia kod 5.1. class Para { private S first; private T last;

public Para(S f, T l) { first = f; last = l; } public S getFirst() { return first; } public T getLast() { return last; } public void setFirst(S f) { first = f; } public void setLast(T l) { last = l; } public String toString() { return first + " " + last; } } Kod 5.1. Sparametryzowana klasa Para Możemy teraz tworzyć różne pary: Para p1 = new Para ("Jan", "Kowalski"); Para p2 = new Para ("Jan Kowalski", new Data("2005-01-01")); Para p = new Para("Ala",2); Tutaj , , oznaczają konkretne typy, które są podstawiane w miejsce parametrów w klasie Para (ale – jak zaraz zobaczymy – tylko chwilowo, w fazie kompilacji). Są nazywane argumentami typu. Para, Para Para nazywają się konkretnymi instancjami sparametryzowanej klasy Para. Przy tworzeniu w wyrażeniu new takich konkretnych instancji do wersji Java 7 musieliśmy po obu stronach przypisania podawać typy, np.: Para p = new Para(„Jan”, „Kowalski”); W Javie 7 wprowadzono diamond operator (puste nawiasy kątowe ), dzięki czemu możemy pisać krócej: Para p1 = new Para ("Jan", "Kowalski"); Potrzebne w new argumenty typu są przez kompilator określane na podstawie typów argumentów konstruktora, a jeśli ich nie ma – argumentów typu podanych z lewej strony przypisania.

W Javie 8 takie konkludowanie typów przy użyciu operatora znacznie poszerzono, m.in. umożliwiając ich określanie przez kompilator nawet wtedy, gdy wyrażenie new nie ma ani lewej strony, ani argumentów konstruktora, np.: ArrayList metoda(ArrayList list) { list.add("a"); return list; } // ... System.out.println(metoda(new ArrayList())); // w Javie 7 błąd kompilacji, w Javie 8 wyprowadzi [a] Trzeba też pamiętać, że argumentami typu nie mogą być typy proste (char, int, double itd.). Oczywiście mogą je zastąpić klasy opakowujące typy proste (np. Integer, Double). Zastosowanie sparametryzowanej klasy Para pozwala uniknąć pisania konwersji zawężających, bo kompilator wie, jakie są argumenty typu, i „pod spodem” dopisuje odpowiednie rzutowania: Para pg = new Para("Ala", 3); //autoboxing System.out.println(pg.getFirst() + " " + pg.getLast()); String nam = pg.getFirst(); // bez konwersji! int m = pg.getLast(); // bez konwersji! pg.setFirst(name + " Kowalska"); pg.setLast(m+1); // bez konwersji, autoboxing System.out.println(pg.getFirst() + " " + pg.getLast()); Wynik działania powyższego kodu będzie następujący: Ala 3 Ala Kowalska 4 Przy tym błędy są wykrywane w fazie kompilacji: pg.setLast("kot"); GenTest1.java:77: setLast(java.lang.Integer) in Para cannot be applied to (java.lang.String) pg.setLast("kot"); ^ 1 error Zwróćmy uwagę, że funkcjonalność sparametryzowanej klasy Para jest niezbyt wielka: niezależnie od tego, jakie są argumenty typu, w środku klasy wobec zmiennych typu S i T możemy używać tylko metod klasy Object. Powodem jest to, że po kompilacji generics, w fazie wykonania, nie są dostępne – poza szczególnymi przypadkiem typów statycznych, znanych w fazie kompilacji – informacje o użytych przy tworzeniu obiektów klas

sparametryzowanych konkretnych argumentach typu (np. String i Integer). Po kompilacji klasy Para okaże się, że: • Jest tylko jedna klasa Para dla wszystkich instancji klasy sparametryzowanej Para; typ wyznaczany przez tę klasę nazywa się typem surowym (raw type). • Z definicji klasy Para zniknęły wszystkie parametry typu i zostały zastąpione przez Object; ten mechanizm nazywa się czyszczeniem typów (type erasure). • Ponieważ jest tylko jedna klasa Para, ewentualne zmienne reprezentowane przez pola statyczne są wspólne dla wszystkich instancji typu sparametryzowanego.

5.2. Ograniczenia parametrów typu Jednym ze sposobów zwiększania funkcjonalności generics Javy jest użycie (jawnych) ograniczeń parametrów typu. Dzięki temu w klasach i metodach sparametryzowanych możemy korzystać z metod specyficznych dla typów podanych w ograniczeniach. Ograniczenie parametru typu określa zestaw typów, które mogą być używane jako argumenty typu (i podstawiane w szablonie w miejscu parametrów typu), a w konsekwencji zestaw metod, które mogą być wołane na rzecz zmiennych oznaczanych parametrami typu. Ograniczenia parametru typu wprowadzamy za pomocą składni: ParametrTypu extends Typ1 & Typ2 & Typ3

& ... & TypN

gdzie: • Typ1 – nazwa klasy lub interfejsu, • Typ2-TypN – nazwy interfejsów.

Uwagi 1. Typy Typ1-TypN mogą być sparametryzowane. 2. Typy ograniczające nie mogą się powtarzać, w tym nie mogą występować powtórzenia dla typów sparametryzowanych TP TP (ze względu na czyszczenie typów). W przypadku ograniczanych parametrów typu type erasure daje typ pierwszego ograniczenia. Na przykład, w fazie wykonania, w kontekście class A , T staje się Appendable. Dla przykładu możemy zdefiniować klasę NaszeZwierze z parametrem typu oznaczającym dowolny typ zwierzęcia dającego głos i mogącego się poruszać (zob. kod 5.2).

public class NaszeZwierze { T z; public NaszeZwierze(T zwierz) { z = zwierz; } public void speak() { System.out.println( z.getTyp()+" "+z.getName() + " mówi " + z.getVoice(Speakable.LOUD) ); } T get() { return z; } public void startSpeakAndStop() { z.start(); speak(); z.stop(); } } Kod 5.2. Zastosowanie ograniczeń typu w klasach sparametryzowanych Jak widać, w tej klasie możemy wobec zmiennej typu T używać wszystkich metod klasy Zwierz oraz metod z interfejsów Speakable i Moveable. Przykładowe użycie: NaszeZwierze p = new NaszeZwierze(new Pies("kuba")); p.startSpeakAndStop(); wyświetli na konsoli: Pies kuba biegnie Pies kuba mówi HAU... HAU... HAU... Pies kuba stanął Gdyby w klasie Pies zdefiniowano metodę merda(), to wobec wyniku p.get() można by zastosować – bez rzutowania – metodę merda(), bo argumentem typu jest Pies, który merda ogonem. Co więcej, kompilator będzie bronił nas przed użyciem niewłaściwego argumentu typu, np.: NaszeZwierze ryba; spowoduje błąd w kompilacji:

Bound mismatch: The type Ryba is not a valid substitute for the bounded parameter of the type NaszeZwierze

5.3. Restrykcje Ze względu na sposób kompilacji (w fazie wykonania mamy jedną klasę raw type oraz zachodzi wspomniane już czyszczenie typów) w definicjach klas i metod sparametryzowanych nie w pełni możemy traktować parametry typu jak typy zwykłe. Możemy: • podawać je jako typy pól i zmiennych lokalnych; • podawać je jako typy parametrów i wyników metod; • dokonywać jawnych konwersji na typy przez nie oznaczane (np. (T) object); • wywoływać na rzecz zmiennych oznaczanych typami sparametryzowanymi metody klasy Object (i ewentualne właściwe dla klas i interfejsów, które stanowią górne ograniczenia danego parametru typu). Nie możemy: • tworzyć obiektów typów wyznaczanych przez parametry (new T() jest niedozwolone, bo na poziomie definicji generics nie wiadomo, czym konkretnie jest T); • używać operatora instanceOf (z powodu jw.); • używać ich w statycznych kontekstach (bo statyczny kontekst jest jeden dla wszystkich instancji typu sparametryzowanego); • używać ich w literałach klasowych; • wywoływać metod z konkretnych klas i interfejsów, które nie są zaznaczone jako górne ograniczenia parametru typu (w najprostszym przypadku tą górną granicą jest Object – wtedy możemy używać tylko metod klasy Object). Również użycie typów sparametryzowanych przy tworzeniu ich konkretnych instancji i przy odwołaniach do tych instancji obarczone jest restrykcjami. Nie wolno używać typów sparametryzowanych np.: • przy tworzeniu tablic (podając je jako typ elementu tablicy); • w obsłudze wyjątków (bo jest to mechanizm fazy wykonania); • w literałach klasowych (bo oznaczają typy fazy wykonania). Dlaczego nie możemy tworzyć tablic elementów typu sparametryzowanego? Wynika to z istoty pojęcia tablicy oraz ze sposobu kompilacji generics. Tablica jest zestawem elementów tego samego typu (albo jego podtypu). Informacja o typie elementów tablicy jest przechowywana i JVM korzysta z niej w fazie wykonania, aby zapewnić, że do tablicy nie zostanie wstawiony element niewłaściwego typu (wtedy generowany jest wyjątek

ArrayStoreException). Gdyby dopuścić tablice elementów typów sparametryzowanych, kontrakt ten zostałby zerwany (bo w fazie wykonania nic nie wiadomo o konkretnych instancjach typów sparametryzowanych, zatem nie można zapewnić odpowiedniej dynamicznej kontroli typów). Para[] pArr = new Para[5]; (1) niedozwolone Object[] objArr = p;

//

objArr[0] = new Para(„A”, „B”); // zadziała, jeśli dopuścimy (1) Wówczas błąd pojawiłby się (jako ClassCastException) później, np. gdy sięgniemy po pierwszy element tablicy pArr i zapytamy go o drugi składnik pary (Integer n = pArr[0].getLast()). Skąd błąd? Bo do tego odwołania kompilator dopisałby konwersję na Integer, a faktycznie mamy String. Byłoby to zaprzeczeniem głównej idei wprowadzenia generics w Javie. To ograniczenie najlepiej jest obejść, stosując kolekcje (listy) konkretnych instancji typu sparametryzowanego.

5.4. Metody sparametryzowane Parametryzacji mogą podlegać nie tylko klasy czy interfejsy, ale również metody. Definicja metody sparametryzowanej ma postać: specyfikatorDostępu [static] typWyniku nazwa(lista parametrów) { // ... }

Argumenty typów (podstawiane w fazie kompilacji w miejsce parametrów, choćby po to, by zapewnić zgodność typów oraz automatyczne konwersje zawężające) są określane na podstawie faktycznych typów użytych przy wywołaniu metody. Proces wyznaczania aktualnych argumentów typów nazywa się konkludowaniem typów (type inferring). Poniższy fragment to przykład sparametryzowanej metody zwracającej ostatni element przekazanej tablicy dowolnych obiektów: static T last(T[] elts) { return elts[elts.length-1]; } Użycie tej metody nie wymaga podawania argumentu typu, będzie on konkludowany z kontekstu, np.:

Integer n = last(new Integer[] { 1, 4, 7 }); // konludowanie typu System.out.println(n + 1); String s = last(new String[] {"a", "b", "ccc"}); // konludowanie typu System.out.println(s + s.length()); wyświetli: 8 ccc3

5.5. Uniwersalne argumenty typu Najbardziej skomplikowaną i chyba niezbyt udaną częścią koncepcji generics w Javie są uniwersalne argumenty typu (wildcards). Niestety czasem są konieczne. Rozważmy listę: ArrayList list1 = new ArrayList(); Czy ArrayList jest nadtypem typu ArrayList? Gdyby tak było, to moglibyśmy napisać: ArrayList list2 = list1; // hipotetyczna konwersja rozszerzająca a wtedy kompilator nie mógłby protestować przeciwko czemuś takiemu: list2.add(new Object()); To jednak doprowadziłoby do katastrofy: Integer n = list1.get(0); // próba przypisania Object na Integer - ClassCastException I wobec tego konstrukcja list2 = list1 jest zabroniona w fazie kompilacji (incompatible types), co oznacza, że w Javie między typami sparametryzowanymi za pomocą konkretnych parametrów nie zachodzą żadne relacje w rodzaju dziedziczenia (typ-nadtyp itp.). A jednak takie relacje są czasem potrzebne. Jeśli ArrayList i ArrayList nie są podtypami ArrayList, to jak utworzyć metodę wyświetlającą zawartość dowolnej listy typu ArrayList? Do tego służą uniwersalne argumenty typu (type wildcards) – oznaczenie „?”. Są trzy typy takich argumentów:

• ograniczony z góry – oznacza nieznany, dowolny typ. Notacja ta wprowadza do Javy wariancję typów sparametryzowanych. Mówi się, że typ sparametryzowany C jest: • kowariantny względem parametru T, jeśli dla dowolnych typów A i B, takich że B jest podtypem A, typ sparametryzowany C jest podtypem C (kowariancja, bo kierunek dziedziczenia typów sparametryzowanych zgodny z kierunkiem dziedziczenia parametrów typu); • kontrawariantny względem parametru T, jeżeli dla dowolnych typów A i B, takich, że B jest podtypem A, typ sparametryzowany C jest podtypem typu sparametryzowanego C (kontra – bo kierunek dziedziczenia jest przeciwny). Kowariancję uzyskujemy za pomocą symbolu , który oznacza wszystkie typy. Faktycznie ArrayList oznacza wszystkie możliwe listy ArrayList z dowolnym parametrem typu T, czyli ArrayList jest nadtypem ArrayList