Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00228 006981 18968526 na godz. na dobę w sumie
Java. Efektywne programowanie. Wydanie III - książka
Java. Efektywne programowanie. Wydanie III - książka
Autor: Liczba stron: 408
Wydawca: Helion Język publikacji: polski
ISBN: 978-83-283-4576-8 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> programowanie >> java - programowanie
Porównaj ceny (książka, ebook (-35%), audiobook).

Poznaj najlepsze praktyki programowania z użyciem platformy Java

Język Java jest konsekwentnie udoskonalany i unowocześniany dzięki zaangażowaniu wielu ludzi. Nowoczesny język Java staje się coraz bardziej wieloparadygmatowy, co oznacza, że stosowanie najlepszych praktyk w coraz większym stopniu determinuje jakość kodu. Obecnie napisanie kodu, który prawidłowo działa i może być łatwo zrozumiany przez innych programistów, nie wystarczy - należy zbudować program w taki sposób, aby można było go łatwo modyfikować. Jako że Java stała się obszerną i złożoną platformą, konieczne stało się uaktualnienie najlepszych praktyk.

Ta książka jest kolejnym, trzecim wydaniem klasycznego podręcznika programowania w Javie. Poszczególne rozdziały zostały gruntownie przejrzane, zaktualizowane i wzbogacone o sporo ważnych treści. Znalazło się tu wiele wartościowych porad dotyczących organizowania kodu w taki sposób, aby stał się przejrzysty, co ułatwi przyszłe modyfikacje i usprawnienia. Poza takimi zagadnieniami, jak programowanie zorientowane obiektowo czy korzystanie z różnych typów, obszernie omówiono stosowanie lambd i strumieni, zasady obsługi wyjątków, korzystania ze współbieżności i serializacji. Książka składa się z dziewięćdziesięciu tematów pogrupowanych w dwanaście rozdziałów. Taki układ pozwala na szybkie odnalezienie potrzebnego rozwiązania.

W książce między innymi:

Java: jakość kodu, efektywność działania i przyjemność programowania.


Dr Joshua Bloch wykłada na Uniwersytecie Carnegie Mellon. Wcześniej był głównym architektem Javy w firmie Google, wyróżniającym się inżynierem w firmie Sun Microsystems i starszym projektantem systemów w Transarc. Kierował projektowaniem i implementacją wielu funkcjonalności platformy Java, w tym rozszerzenia języka w JDK 5.0 oraz Collection Framework. Jego książki są uważane za lekturę obowiązkową każdego, kto chce pisać dobry i wydajny kod w Javie.

Znajdź podobne książki Ostatnio czytane w tej kategorii

Darmowy fragment publikacji:

Tytuł oryginału: Effective Java (3rd Edition) Tłumaczenie: Rafał Jońca Projekt okładki: Studio Gravite / Olsztyn; Obarek, Pokoński, Pazdrijowski, Zaprucki ISBN: 978-83-283-4576-8 Authorized translation from the English language edition, entitled: EFFECTIVE JAVA, Third Edition; ISBN 0134685997; by Joshua Bloch; published by Pearson Education, Inc, publishing as Addison-Wesley Professional. Copyright © 2018 by Pearson Education, Inc. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc. Polish language edition published by HELION S.A. Copyright © 2018. Portions copyright © 2001-2008 Oracle and/or its affiliates. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji. Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Wydawnictwo HELION ul. Kościuszki 1c, 44-100 GLIWICE tel. 32 231 22 19, 32 230 98 63 e-mail: helion@helion.pl WWW: http://helion.pl (księgarnia internetowa, katalog książek) Drogi Czytelniku! Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres http://helion.pl/user/opinie/javep3 Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję. Printed in Poland. • Kup książkę • Poleć książkę • Oceń książkę • Księgarnia internetowa • Lubię to! » Nasza społeczność Spis tre(cid:258)ci S(cid:239)owo wst(cid:218)pne .......................................................................... 9 Przedmowa ............................................................................. 11 Podzi(cid:218)kowania ........................................................................ 15 Rozdzia(cid:239) 1. Wprowadzenie .............................................................. 19 Rozdzia(cid:239) 2. Tworzenie i usuwanie obiektów ..................................... 23 Temat 1. Tworzenie statycznych metod fabrycznych zamiast konstruktorów ..................................................... 23 Temat 2. Zastosowanie budowniczego do obsługi wielu parametrów konstruktora ........................................ 28 Temat 3. Wymuszanie właściwości singleton za pomocą prywatnego konstruktora lub typu enum ..... 36 Temat 4. Wykorzystanie konstruktora prywatnego w celu uniemożliwienia utworzenia obiektu .................... 38 Temat 5. Stosuj wstrzykiwanie zależności zamiast odwoływania się do zasobów na sztywno ........... 39 Temat 6. Unikanie powielania obiektów ......................................... 41 Temat 7. Usuwanie niepotrzebnych referencji do obiektów .......... 45 Temat 8. Unikanie finalizatorów i oczyszczaczy ............................. 48 Temat 9. Preferuj konstrukcję try z zasobami zamiast try-finally .............................................................. 54 Rozdzia(cid:239) 3. Metody wspólne dla wszystkich obiektów ...................... 57 Temat 10. Zachowanie założeń w trakcie przedefiniowywania metody equals ................................. 58 Temat 11. Przedefiniowywanie metody hashCode wraz z equals .... 70 Temat 12. Przedefiniowywanie metody toString .............................. 75 Poleć książkęKup książkę 6 SPIS TREŚCI Temat 13. Rozsądne przedefiniowywanie metody clone .................. 78 Temat 14. Implementacja interfejsu Comparable ............................ 86 Rozdzia(cid:239) 4. Klasy i interfejsy ........................................................... 93 Temat 15. Ograniczanie dostępności klas i ich składników ............. 93 Temat 16. Stosowanie metod akcesorów zamiast pól publicznych w klasach publicznych ....................................................... 98 Temat 17. Zapewnianie niezmienności obiektu .............................. 100 Temat 18. Zastępowanie dziedziczenia kompozycją ....................... 107 Temat 19. Projektowanie i dokumentowanie klas przeznaczonych do dziedziczenia ................................... 113 Temat 20. Stosowanie interfejsów zamiast klas abstrakcyjnych .... 119 Temat 21. Projektowanie interfejsów na długie lata ....................... 124 Temat 22. Wykorzystanie interfejsów jedynie do definiowania typów .................................................... 127 Temat 23. Zastępowanie oznaczania klas hierarchią ...................... 129 Temat 24. Zalety stosowania statycznych klas składowych ............ 132 Temat 25. Ograniczenie pliku źródłowego do pojedynczej klasy głównego poziomu .......................................................... 135 Rozdzia(cid:239) 5. Typy ogólne ............................................................... 139 Temat 26. Nie korzystaj z typów surowych ..................................... 139 Temat 27. Eliminowanie ostrzeżeń o braku kontroli ...................... 144 Temat 28. Korzystanie z list zamiast tablic ....................................... 147 Temat 29. Stosowanie typów ogólnych ............................................ 151 Temat 30. Stosowanie metod ogólnych ........................................... 156 Temat 31. Zastosowanie związanych szablonów do zwiększania elastyczności API ................................... 159 Temat 32. Ostrożne łączenie typów ogólnych i parametrów varargs ....................................................... 166 Temat 33. Wykorzystanie heterogenicznych kontenerów bezpiecznych dla typów ................................................... 171 Rozdzia(cid:239) 6. Typy wyliczeniowe i adnotacje .................................... 177 Temat 34. Użycie typów wyliczeniowych zamiast stałych int ......... 177 Temat 35. Użycie pól instancyjnych zamiast kolejności ................. 188 Temat 36. Użycie EnumSet zamiast pól bitowych .......................... 189 Temat 37. Użycie EnumMap zamiast indeksowania kolejnością ... 191 Temat 38. Emulowanie rozszerzalnych typów wyliczeniowych za pomocą interfejsów ..................................................... 196 Temat 39. Korzystanie z adnotacji zamiast wzorców nazw ............ 200 Temat 40. Spójne użycie adnotacji Override ................................... 207 Temat 41. Użycie interfejsów znacznikowych do definiowania typów .................................................... 210 Poleć książkęKup książkę SPIS TREŚCI 7 Rozdzia(cid:239) 7. Lambdy i strumienie ................................................... 213 Temat 42. Stosuj lambdy zamiast klas anonimowych ..................... 213 Temat 43. Wybieraj referencje do metod zamiast lambd ............... 217 Temat 44. Korzystaj ze standardowych interfejsów funkcyjnych ... 219 Temat 45. Rozważnie korzystaj ze strumieni .................................. 223 Temat 46. Stosuj w strumieniach funkcje bez efektów ubocznych .................................................... 231 Temat 47. Zwracaj kolekcje zamiast strumieni ............................... 236 Temat 48. Ostrożnie korzystaj ze strumieni zrównoleglonych ...... 242 Rozdzia(cid:239) 8. Metody ...................................................................... 247 Temat 49. Sprawdzanie poprawności parametrów ......................... 247 Temat 50. Defensywne kopiowanie ................................................. 250 Temat 51. Projektowanie sygnatur metod ....................................... 255 Temat 52. Rozsądne korzystanie z przeciążania ............................. 257 Temat 53. Rozsądne korzystanie z metod varargs .......................... 263 Temat 54. Zwracanie pustych tablic lub kolekcji zamiast wartości null ....................................................... 265 Temat 55. Rozsądne zwracanie obiektów opcjonalnych ................. 267 Temat 56. Tworzenie komentarzy dokumentujących dla udostępnianych elementów API ............................... 272 Rozdzia(cid:239) 9. Programowanie .......................................................... 281 Temat 57. Ograniczanie zasięgu zmiennych lokalnych .................. 281 Temat 58. Stosowanie pętli for-each zamiast tradycyjnych pętli for ........................................ 284 Temat 59. Poznanie i wykorzystywanie bibliotek ........................... 287 Temat 60. Unikanie typów float i double, gdy potrzebne są dokładne wyniki ................................. 290 Temat 61. Stosowanie typów prostych zamiast opakowanych typów prostych ........................... 292 Temat 62. Unikanie typu String, gdy istnieją bardziej odpowiednie typy .............................................. 296 Temat 63. Problemy z wydajnością przy łączeniu ciągów znaków .......................................... 298 Temat 64. Odwoływanie się do obiektów poprzez interfejsy ......... 299 Temat 65. Stosowanie interfejsów zamiast refleksyjności .............. 301 Temat 66. Rozważne wykorzystywanie metod natywnych ............. 304 Temat 67. Unikanie przesadnej optymalizacji ................................ 306 Temat 68. Wykorzystanie ogólnie przyjętych konwencji nazewnictwa ................................ 309 Poleć książkęKup książkę 8 SPIS TREŚCI Rozdzia(cid:239) 10. Wyj(cid:200)tki ...................................................................... 313 Temat 69. Wykorzystanie wyjątków w sytuacjach nadzwyczajnych ......................................... 313 Temat 70. Stosowanie wyjątków przechwytywanych i wyjątków czasu wykonania ........................................... 316 Temat 71. Unikanie niepotrzebnych wyjątków przechwytywanych .......................................... 318 Temat 72. Wykorzystanie wyjątków standardowych ...................... 320 Temat 73. Zgłaszanie wyjątków właściwych dla abstrakcji ............. 323 Temat 74. Dokumentowanie wyjątków zgłaszanych przez metodę .................................................................... 325 Temat 75. Udostępnianie danych o błędzie ..................................... 326 Temat 76. Zachowanie atomowości w przypadku błędu ................ 328 Temat 77. Nie ignoruj wyjątków ...................................................... 330 Rozdzia(cid:239) 11. Wspó(cid:239)bie(cid:285)no(cid:258)(cid:202) ........................................................... 333 Temat 78. Synchronizacja dostępu do wspólnych modyfikowalnych danych ....................... 333 Temat 79. Unikanie nadmiarowej synchronizacji .......................... 338 Temat 80. Stosowanie wykonawców, zadań i strumieni zamiast wątków ................................................................ 344 Temat 81. Stosowanie narzędzi współbieżności zamiast wait i notify ........................................................ 346 Temat 82. Dokumentowanie bezpieczeństwa dla wątków .............. 352 Temat 83. Rozsądne korzystanie z późnej inicjalizacji ................... 355 Temat 84. Nie polegaj na harmonogramie wątków ........................ 358 Rozdzia(cid:239) 12. Serializacja ................................................................ 361 Temat 85. Stosuj rozwiązania alternatywne wobec serializacji Javy ..................................................... 361 Temat 86. Rozsądne implementowanie interfejsu Serializable ...... 365 Temat 87. Wykorzystanie własnej postaci serializowanej .............. 368 Temat 88. Defensywne tworzenie metody readObject ................... 375 Temat 89. Stosowanie typów wyliczeniowych zamiast readResolve do kontroli obiektów .................... 381 Temat 90. Użycie pośrednika serializacji zamiast serializowanych obiektów ................................. 385 Dodatek A Tematy odpowiadaj(cid:200)ce drugiemu wydaniu .................. 389 Dodatek B Zasoby ....................................................................... 393 Skorowidz ....................................................................................... 399 Poleć książkęKup książkę 6 Typy wyliczeniowe i adnotacje Java obsługuje dwie rodziny specjalnych typów referencyjnych — pewien ro- dzaj klas nazwany typem wyliczeniowym oraz pewien rodzaj interfejsu nazwany typem adnotacyjnym. W tym rozdziale przedstawimy najlepsze praktyki związane z wykorzystywaniem obu rodzin typów. Temat 34. U(cid:285)ycie typów wyliczeniowych zamiast sta(cid:239)ych int Typ wyliczeniowy to typ, którego prawidłowe wartości tworzy stały zbiór, na przykład pory roku, planety w systemie słonecznym lub rodzaje kart w talii. Zanim do języka został dodany typ wyliczeniowy, standardową praktyką reprezentowania takich typów było deklarowanie grupy zmiennych typu int, po jednej dla każdej wartości typu. // Wzorzec typu wyliczeniowego z u(cid:298)yciem int - bardzo niedoskona(cid:225)y! public static final int APPLE_FUJI = 0; public static final int APPLE_PIPPIN = 1; public static final int APPLE_GRANNY_SMITH = 2; public static final int ORANGE_NAVEL = 0; public static final int ORANGE_TEMPLE = 1; public static final int ORANGE_BLOOD = 2; Poleć książkęKup książkę 178 ROZDZIAŁ 6. TYPY WYLICZENIOWE I ADNOTACJE Technika ta, znana pod nazwą wzorca wyliczeniowego int, ma wiele wad. Nie zapewnia ona żadnej formy bezpieczeństwa typów i jest mało wygodna. Kompilator nie będzie protestował, jeżeli przekażemy jabłko do metody oczekującej poma- rańczy, porównamy jabłko z pomarańczą za pomocą operatora == lub co gorsza: // Smaczny sok jab(cid:225)kowy z domieszk(cid:261) cytrusów! int i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN; Jak można zauważyć, nazwa każdej ze stałych dla jabłka zaczyna się od APPLE_, a nazwa każdej stałej pomarańczy zaczyna się od ORANGE_. Dzieje się tak, ponieważ Java nie zapewnia przestrzeni nazw dla grup wartości wyliczeniowych typu int. Prefiksy zapobiegają powtórzeniu nazw, gdy dwie grupy wyliczeniowe mają tak samo nazwane stałe, co pozwala odróżnić od siebie ELEMENT_MERCURY i PLANET_MERCURY. Programy korzystające z wzorca wyliczeniowego int są niewygodne. Ponieważ stałe wyliczeniowe int są zmiennymi stałymi [JLS, 4.12.4], są kompilowane do programów klienckich, które je wykorzystują [JLS, 13.1]. Jeżeli wartość int związana ze stałą wyliczeniową zmieni się, klient musi być ponownie skompilowany. W prze- ciwnym razie będzie nadal działał, ale jego zachowanie będzie niezdefiniowane. Nie istnieje prosty sposób przetłumaczenia stałej wyliczeniowej na napis do wyświe- tlenia. Jeżeli drukujemy taką stałą lub wyświetlamy ją w debugerze, widzimy tylko liczbę, co nie jest zbyt pomocne. Nie istnieje niezawodny sposób na iterowanie po wszystkich wartościach int w typie wyliczeniowym, a nawet określenie wielkości grupy stałych int. Można również spotkać odmianę tego wzorca, w którym w miejsce stałych int są używane stałe String. Wariant ten, nazywany wzorcem wyliczeniowym String, jest nawet mniej polecany. Choć zapewnia on czytelne napisy dla stałych, może prowadzić do problemów z wydajnością, ponieważ korzysta z porównywania napisów. Co gorsza, może doprowadzić do tego, że niedoświadczeni użytkownicy na stałe wpiszą stałe znakowe do kodu klienta, zamiast korzystać z nazw pól. Jeżeli taki wpisany napis zawiera błąd typograficzny, wymknie się spod kontroli w cza- sie kompilacji i spowoduje błąd w czasie działania aplikacji. Na szczęście Java zapewnia alternatywę pozwalającą na ominięcie wad wzorców wyliczeniowych int i String, dającą wiele nowych korzyści. Typ ten jest zdefiniowany w podręczniku jako typ enum [JLS, 8.9]. W najprostszej postaci wygląda następująco: public enum Apple { FUJI, PIPPIN, GRANNY_SMITH } public enum Orange { NAVEL, TEMPLE, BLOOD } Na pierwszy rzut oka typ wyliczeniowy może wyglądać podobnie do typów znanych z innych języków, takich jak C, C++ i C#, ale podobieństwo jest mylące. Typ wy- liczeniowy w języku Java jest w pełni wartościową klasą, znacznie bardziej zaawan- sowaną niż odpowiedniki w innych językach, w których typ wyliczeniowy jest wła- ściwie zbiorem wartości int. Poleć książkęKup książkę TEMAT 34. UŻYCIE TYPÓW WYLICZENIOWYCH ZAMIAST STAŁYCH INT 179 Podstawowe założenie leżące u podstaw typu wyliczeniowego w języku Java jest proste — są to klasy eksportujące po jednym obiekcie dla każdej stałej wyliczenia z użyciem finalnego statycznego pola publicznego. Typy wyliczeniowe są w efekcie finalne, dzięki temu, że nie jest dostępny konstruktor. Ponieważ klienty nigdy nie tworzą obiektów typu wyliczeniowego ani po nich nie dziedziczą, nie mogą istnieć instancje inne niż zadeklarowane stałe wyliczeniowe. Inaczej mówiąc, typy wyliczeniowe są kontrolowanymi instancjami (temat 1.). Są one generali- zacją singletonów (temat 3.), które są w praktyce jednoelementowymi typami wyliczeniowymi. Typ wyliczeniowy zapewnia bezpieczeństwo typów w czasie kompilacji. Jeżeli zadeklarujemy, że parametr będzie typu Apple, to gwarantowane jest, że przekazana do parametru referencja obiektu różna od null będzie jedną z trzech prawidłowych wartości typu Apple. Próba przekazania wartości niewłaściwego typu spowoduje błąd kompilacji, tak samo jak próba przypisania wyrażenia jednego typu wylicze- niowego do zmiennej innego albo użycia operatora == do porównywania wartości różnych typów wyliczeniowych. Typy wyliczeniowe o identycznie nazwanych stałych mogą bez problemów współ- istnieć, ponieważ każdy typ posiada własną przestrzeń nazw. Można dodawać lub zmieniać kolejność stałych w typie wyliczeniowym bez konieczności ponow- nej kompilacji klientów, ponieważ pola eksportujące stałe zapewniają warstwę izolacyjną pomiędzy typem wyliczeniowym i jego klientami — stałe nie są wkompi- lowywane w klienty, tak jak miało to miejsce w przypadku wzorca typu wylicze- niowego int. Można również tłumaczyć wartości typu wyliczeniowego na czytelne napisy przez wywoływanie ich metody toString. Oprócz naprawiania niedociągnięć typu wyliczeniowego korzystającego z int nowe typy wyliczeniowe pozwalają dopisać dowolne metody i implementować dowolne interfejsy. Zapewniają wysokiej jakości implementacje wszystkich metod klasy Object (rozdział 3.), implementują Comparable (temat 14.) i Serializable (rozdział 12.), a ich serializowana postać jest zaprojektowana tak, aby przetrwać większość zmian typu wyliczeniowego. Do czego jest potrzebna możliwość dodawania do typu wyliczeniowego metod i pól? Przykładem może być możliwość skojarzenia danych ze stałymi. Nasze typy Apple i Orange mogą na przykład zawierać metodę zwracającą kolor owocu lub nawet jego zdjęcie. Można wzbogacać typ wyliczeniowy o dowolną metodę, która wydaje się nam odpowiednia. Typ wyliczeniowy może rozpocząć życie jako prosta kolekcja stałych wyliczeniowych i przeobrazić się w rozbudowaną abstrakcję. Dobrym przykładem takiego bogatego typu wyliczeniowego może być typ modelu- jący osiem planet naszego systemu słonecznego. Każda planeta ma masę i promień, a na podstawie tych dwóch parametrów można obliczyć grawitację na powierzchni. Poleć książkęKup książkę 180 ROZDZIAŁ 6. TYPY WYLICZENIOWE I ADNOTACJE To z kolei pozwala obliczyć ciężar obiektu na powierzchni planety na podstawie masy obiektu. Poniżej zamieszczony jest przykład takiego typu wyliczeniowego. Liczby w nawiasach po stałych wyliczeniowych są parametrami przekazywanymi do konstruktora. W tym przypadku jest to masa i promień planety: // Typ wyliczeniowy z danymi i operacjami public enum Planet { MERCURY(3.302e+23, 2.439e6), VENUS (4.869e+24, 6.052e6), EARTH (5.975e+24, 6.378e6), MARS (6.419e+23, 3.393e6), JUPITER(1.899e+27, 7.149e7), SATURN (5.685e+26, 6.027e7), URANUS (8.683e+25, 2.556e7), NEPTUNE(1.024e+26, 2.477e7); private final double mass; // w kilogramach private final double radius; // w metrach private final double surfaceGravity; // w m / s^2 // uniwersalna sta(cid:225)a grawitacyjna w m^3 / kg s^2 private static final double G = 6.67300E-11; // konstruktor Planet(double mass, double radius) { this.mass = mass; this.radius = radius; surfaceGravity = G * mass / (radius * radius); } public double mass() { return mass; } public double radius() { return radius; } public double surfaceGravity() { return surfaceGravity; } public double surfaceWeight(double mass) { return mass * surfaceGravity; // F = ma } } Jak widać, bardzo łatwo jest napisać bogaty typ wyliczeniowy, taki jak Planet. Aby skojarzyć dane ze stałymi wyliczenia, należy zadeklarować pola instancyjne i napisać konstruktor pobierający dane i zapisujący je w polach. Typy wyliczeniowe są z natury niezmienne, więc wszystkie pola powinny być oznaczone final (temat 17.). Mogą być one publiczne, ale znacznie lepiej zadeklarować je jako pry- watne i udostępnić publiczne akcesory (temat 16.). W przypadku typu Planet konstruktor oblicza i zapisuje grawitację na powierzchni, ale jest to tylko optymaliza- cja. Grawitacja może być wyliczana na podstawie masy i promienia przy każdym wywołaniu metody surfaceWeight, który pobiera masę obiektu i zwraca jego ciężar na planecie reprezentowanej przez stałą. Poleć książkęKup książkę TEMAT 34. UŻYCIE TYPÓW WYLICZENIOWYCH ZAMIAST STAŁYCH INT 181 Choć typ wyliczeniowy Planet jest prosty, daje on zaskakująco dużo możliwości. Poniżej znajduje się krótki program pobierający wagę obiektu na Ziemi (w dowolnej jednostce) i wyświetla elegancką tabelę wag obiektu na wszystkich ośmiu planetach (w tej samej jednostce): public class WeightTable { public static void main(String[] args) { double earthWeight = Double.parseDouble(args[0]); double mass = earthWeight / Planet.EARTH.surfaceGravity(); for (Planet p : Planet.values()) System.out.printf( Waga na s wynosi f n , p, p.surfaceWeight(mass)); } } Należy zwrócić uwagę, że Planet, podobnie jak inne typy wyliczeniowe, posiada metodę statyczną values, która zwraca tablicę wartości typu w kolejności ich zade- klarowania. Należy również zwrócić uwagę, że metoda toString zwraca zade- klarowaną nazwę każdej z wartości typu wyliczeniowego, co pozwala w łatwy sposób drukować ją za pomocą println i printf. Jeżeli nie jesteśmy usatysfakcjonowani reprezentacją tekstową stałej, można ją zmienić przez nadpisanie metody toString. Poniżej zamieszczony jest wynik działania naszego małego programu WeightTable (wyliczenie nie przysłania metody toString) z przekazaną w wierszu poleceń wartością 185: Waga na MERCURY wynosi 69.912739 Waga na VENUS wynosi 167.434436 Waga na EARTH wynosi 185.000000 Waga na MARS wynosi 70.226739 Waga na JUPITER wynosi 467.990696 Waga na SATURN wynosi 197.120111 Waga na URANUS wynosi 167.398264 Waga na NEPTUNE wynosi 210.208751 Aż do roku 2006, czyli dwa lata po dodaniu typu wyliczeniowego do Javy, Pluton był planetą. Warto w tym momencie zadać sobie pytanie, co się stanie, jeśli usu- niemy element z typu wyliczeniowego. Dowolny klient, który nie korzystał bezpo- średnio z usuniętego elementu, nadal będzie działał prawidłowo. Nasz przykła- dowy program WeightTable nadal działałby prawidłowo — po prostu wyświetlałby jeden wiersz mniej. Co stanie się z programem, który jawnie korzystał z usunię- tego elementu (w tym przypadku Planet.PLUTO)? Jeśli program będzie kompilowany ponownie, proces ten się nie powiedzie i zostanie wyświetlony komunikat o błędzie wskazujący na odniesienie do zdegradowanej planety. Jeśli nie dojdzie do ponownej kompilacji, program wyświetli podobny komunikat w trakcie działania. To najlepsze zachowanie, na jakie można liczyć, znacznie przyjemniejsze od tego, które dostar- czyłby wzorzec wyliczenia int. Poleć książkęKup książkę 182 ROZDZIAŁ 6. TYPY WYLICZENIOWE I ADNOTACJE Niektóre operacje związane ze stałymi wyliczanymi mogą być używane wyłącz- nie w klasie lub pakiecie, w którym jest zdefiniowany typ wyliczeniowy. Takie operacje najlepiej zaimplementować jako metody prywatne lub prywatne w ramach pakietu. Każda ze stałych posiada wtedy ukrytą kolekcję operacji, które pozwalają na odpowiednie reagowanie w klasie lub pakiecie w przypadku manipulacji na stałych. Tak samo jak w przypadku innych klas, o ile nie mamy ważnej potrzeby udostęp- niania metod klientom, należy zadeklarować je jako prywatne lub, jeżeli jest to wy- magane, prywatne w ramach pakietu (temat 15.). Jeżeli typ wyliczeniowy jest ogólnie przydatny, powinien być klasą najwyższego poziomu, natomiast jeżeli jest używany w klasie najwyższego poziomu, powinien być jej składnikiem (temat 24.). Na przykład typ wyliczeniowy java.math. (cid:180)RoundingMode reprezentuje tryb zaokrąglania ułamków dziesiętnych. Tryby zaokrąglania są wykorzystywane w klasie BigDecimal, ale zapewniają one przy- datną abstrakcję, która nie jest fundamentalnie związana z BigDecimal. Przez umieszczenie typu wyliczeniowego RoundingMode na najwyższym poziomie projek- tanci biblioteki zachęcają programistów potrzebujących trybu zaokrąglania do ponownego użycia tego typu, dzięki czemu uzyskuje się większą spójność API. Techniki zademonstrowane w przykładzie Planet są wystarczające dla większości typów wyliczeniowych, ale czasami potrzeba więcej. Z każdą stałą Planet są związane różne dane, ale czasami potrzebujemy skojarzyć całkowicie inne działanie z każdą ze stałych. Załóżmy, że chcemy napisać typ wyliczeniowy reprezentujący operacje na prostym kalkulatorze czterodziałaniowym i chcemy zapewnić metodę do wykonania operacji arytmetycznych reprezentowanych przez każdą ze stałych. Jednym ze sposobów realizacji jest wybór wyrażenia w zależności od wartości wyliczenia: // Typ wyliczeniowy z wyborem spo(cid:286)ród w(cid:225)asnych warto(cid:286)ci - niedoskona(cid:225)y public enum Operation { PLUS, MINUS, TIMES, DIVIDE; // wykonanie operacji arytmetycznej reprezentowanej przez sta(cid:225)(cid:261) public double apply(double x, double y) { switch(this) { case PLUS: return x + y; case MINUS: return x - y; case TIMES: return x * y; case DIVIDE: return x / y; } throw new AssertionError( Nieznana operacja: + this); } } Kod ten działa, ale nie jest zbyt ładny. Nie kompiluje się on bez instrukcji throw, ponieważ koniec metody jest technicznie osiągalny, nawet jeżeli nie zostanie nigdy osiągnięty [JLS, 14.21]. Co gorsza, kod ten jest delikatny. Jeżeli dodamy nową Poleć książkęKup książkę TEMAT 34. UŻYCIE TYPÓW WYLICZENIOWYCH ZAMIAST STAŁYCH INT 183 stałą wyliczeniową, a zapomnimy dodać odpowiedni przypadek w switch, typ nadal będzie się kompilował, ale wystąpi błąd w czasie działania przy próbie wyko- nania nowej operacji. Na szczęście istnieje lepszy sposób na kojarzenie różnych operacji z każdą stałą wyliczenia — można zadeklarować abstrakcyjną metodę apply w typie wyliczenio- wym i nadpisywać ją w konkretnej metodzie dla stałej umieszczonej w treści klasy specyficznej dla stałej. Takie metody są znane jako implementacje metod specy- ficznych dla stałych. // Typ wyliczeniowy z implementacj(cid:261) metod specyficznych dla sta(cid:225)ej public enum Operation { PLUS { public double apply(double x, double y){return x + y;} }, MINUS { public double apply(double x, double y){return x - y;} }, TIMES { public double apply(double x, double y){return x * y;} }, DIVIDE { public double apply(double x, double y){return x / y;} }; public abstract double apply(double x, double y); } Jeżeli dodamy nową stałą do drugiej wersji Operation, mało prawdopodobne jest, że zapomnimy napisać metody apply, ponieważ znajduje się ona zaraz po deklaracji każdej ze stałych. W mało prawdopodobnym przypadku, gdy jednak jej zapomnimy, kompilator przypomni nam o tym, ponieważ metody abstrakcyjne w typie wyliczeniowym muszą być przesłaniane konkretnymi metodami w każdej ze stałych. Implementacja metody specyficzna dla stałej może być łączona z danymi specyficz- nymi dla stałej. Poniżej znajduje się na przykład kolejna wersja Operation, w której nadpisujemy metodę toString w celu zwrócenia symbolu skojarzonego z operacją. // Typ wyliczeniowy z tre(cid:286)ci(cid:261) klasy specyficzn(cid:261) dla sta(cid:225)ej oraz z danymi public enum Operation { PLUS( + ) { public double apply(double x, double y) { return x + y; } }, MINUS( - ) { public double apply(double x, double y) { return x - y; } }, TIMES( * ) { public double apply(double x, double y) { return x * y; } }, DIVIDE( / ) { public double apply(double x, double y) { return x / y; } }; private final String symbol; Operation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } public abstract double apply(double x, double y); } Poleć książkęKup książkę 184 ROZDZIAŁ 6. TYPY WYLICZENIOWE I ADNOTACJE Na przykład przedstawiona powyżej implementacja toString pozwala łatwo wy- świetlać wyrażenia arytmetyczne w sposób pokazany przez ten mały program. public static void main(String[] args) { double x = Double.parseDouble(args[0]); double y = Double.parseDouble(args[1]); for (Operation op : Operation.values()) System.out.printf( f s f = f n , x, op, y, op.apply(x, y)); } Uruchomienie tego programu z wartościami 2 i 4 przekazanymi w wierszu poleceń daje nam następujący wynik: 2.000000 + 4.000000 = 6.000000 2.000000 - 4.000000 = -2.000000 2.000000 * 4.000000 = 8.000000 2.000000 / 4.000000 = 0.500000 Typy wyliczeniowe mają automatycznie generowaną metodę valueOf(String), która przekształca nazwę stałej na samą stałą. Jeżeli nadpiszemy metodę toString w typie wyliczeniowym, warto rozważyć również napisanie metody fromString w celu przekształcenia własnej reprezentacji na odpowiednią stałą. Poniższy kod (po odpowiedniej zmianie typu) realizuje to zadanie dla dowolnego typu wylicze- niowego, o ile każda stała będzie miała unikatową reprezentację znakową. // Implementacja metody fromString dla typu wyliczeniowego private static final Map String, Operation stringToEnum = Stream.of(values()).collect( toMap(Object::toString, e - e)); // zwraca Operation dla napisu lub null, je(cid:298)eli napis jest nieprawid(cid:225)owy public static Optional Operation fromString(String symbol) { return Optional.ofNullable(stringToEnum.get(symbol)); } Należy zwrócić uwagę, że stałe Operation są umieszczane w odwzorowaniu stringToEnum w bloku statycznym, który jest wykonywany po utworzeniu stałych. Przedstawiony kod używa strumienia (rozdział 7.) dla tablicy zwróconej przez metodę values(). Przed Javą 8 musielibyśmy utworzyć pusty obiekt HashMap, a na- stępnie przejść po każdej wartości z tablicy i wstawić ją do odwzorowania. Oczywi- ście jeśli chcesz, możesz nadal korzystać z takiego rozwiązania. Pamiętaj jednak, że próba umieszczenia każdej stałej w odwzorowaniu w jej konstruktorze nie zadziała. Spowoduje błąd kompilacji, co jest prawidłowym działaniem, ponie- waż nastąpiłoby wygenerowanie NullPointerException, jeżeli takie przypisanie byłoby legalne. Konstruktory typu wyliczeniowego nie mogą korzystać z pól statycznych typu z wyjątkiem zmiennych stałych (temat 34.). Takie ograniczenie jest niezbędne, ponieważ te pola statyczne nie są zainicjowane w momencie wykonywania Poleć książkęKup książkę TEMAT 34. UŻYCIE TYPÓW WYLICZENIOWYCH ZAMIAST STAŁYCH INT 185 konstruktorów. Specjalnym przypadkiem tego ograniczenia jest to, że stała wylicze- niowa nie może uzyskać dostępu do innej stałej wyliczenia z poziomu swojego konstruktora. Zwróć uwagę, że metoda fromString zwraca Optional String . Umożliwia to metodzie wskazanie, że tekst, który został do niej przekazany, nie reprezentuje żadnej poprawnej operacji, co zmusza klienta do uwzględnienia takiej możliwości (temat 55.). Wadą specyficznych dla stałych implementacji metod jest to, że powodują utrud- nienie współdzielenia kodu pomiędzy stałymi wyliczenia. Jako przykład weźmy typ wyliczeniowy reprezentujący dni tygodnia w pakiecie księgowym. Taki typ wyliczeniowy posiada metodę obliczającą wynagrodzenie pracownika za dany dzień na podstawie podstawowego wynagrodzenia (za godzinę) oraz liczby minut przepracowanych danego dnia. W pięciu dniach tygodnia każda minuta przekra- czająca zwykłą zmianę powoduje wygenerowanie płacy w nadgodzinach, a w dwóch dniach weekendu cały czas pracy jest traktowany jako nadliczbowy. Przy użyciu instrukcji switch można łatwo wykonać takie obliczenia przez zastosowanie wielu etykiet dla dwóch fragmentów kodu. // Typ wyliczeniowy wybieraj(cid:261)cy na podstawie w(cid:225)asnych warto(cid:286)ci w celu // wspó(cid:225)dzielenia kodu - w(cid:261)tpliwe enum PayrollDay { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY; private static final int MINS_PER_SHIFT = 8 * 60; double pay(double minutesWorked, double payRate) { double basePay = minutesWorked * payRate; double overtimePay; switch(this) { case SATURDAY: case SUNDAY: // weekend overtimePay = basePay / 2; break; default: // zwyk(cid:225)e dni tygodnia overtimePay = minutesWorked = MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2; } return basePay + overtimePay; } } Kod ten jest bez wątpienia spójny, ale z perspektywy jego utrzymania dosyć niebez- pieczny. Załóżmy, że dodajemy element do wyliczenia, być może specjalną wartość oznaczającą urlop, ale zapomnimy dodać odpowiedniego przypadku do instrukcji switch. Program nadal będzie się kompilował, ale metoda pay po cichu zapłaci pracownikowi taką samą kwotę jak za zwykły dzień tygodnia. Poleć książkęKup książkę 186 ROZDZIAŁ 6. TYPY WYLICZENIOWE I ADNOTACJE Aby bezpiecznie wykonać obliczenie przy użyciu implementacji specyficznej dla stałej, konieczne będzie powielenie obliczenia nadgodzin dla każdej stałej lub prze- niesienie obliczeń do dwóch metod pomocniczych (jednej dla weekendów i jednej dla dni roboczych) i wywoływanie odpowiedniej metody pomocniczej z każdej ze stałych. Oba podejścia powodują powstanie sporej ilości kodu narzędziowego, co znacznie zmniejsza czytelność i zwiększa możliwość popełnienia błędu. Kod narzędziowy może być zmniejszony przez zastąpienie metody abstrakcyjnej overtimePay w PayrollDay metodą konkretną, w której wykonamy obliczenie nadgodzin dla dni powszednich. W takim przypadku konieczne jest nadpisanie metody wyłącznie dla weekendów. Ma to jednak takie same wady jak instrukcja switch — jeżeli dodamy kolejny dzień bez nadpisywania metody overtimePay, odziedziczy ona obliczenia dla dni tygodnia bez żadnej informacji o tym fakcie. Potrzebujemy więc wymuszenia wyboru strategii płacy nadgodzin za każdym razem, gdy dodamy stałą wyliczenia. Na szczęście istnieje łatwy sposób osiągnię- cia tego efektu. Polega to na przeniesieniu obliczenia nadgodzin do prywatnego zagnieżdżonego typu wyliczeniowego i przekazanie obiektu tego typu wyliczenio- wego strategii do konstruktora typu wyliczeniowego PayrollDay. Typ wylicze- niowy PayrollDay deleguje obliczenie płacy w nadgodzinach do typu wylicze- niowego strategii, co eliminuje potrzebę zastosowania polecenia switch lub implementacji metod specyficznych dla stałych w PayrollDay. Choć ten wzorzec jest mniej zwięzły niż instrukcja switch, jest bezpieczniejszy i bardziej elastyczny: // Wzorzec typu wyliczeniowego strategii enum PayrollDay { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND); private final PayType payType; PayrollDay(PayType payType) { this.payType = payType; } PayrollDay() { this(PayType.WEEKDAY); } // domy(cid:286)lny int pay(int minutesWorked, int payRate) { return payType.pay(minutesWorked, payRate); } // typ wyliczeniowy strategii private enum PayType { WEEKDAY { int overtimePay(int minsWorked, int payRate) { return minsWorked = MINS_PER_SHIFT ? 0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2; } }, WEEKEND { int overtimePay(int minsWorked, int payRate) { Poleć książkęKup książkę TEMAT 34. UŻYCIE TYPÓW WYLICZENIOWYCH ZAMIAST STAŁYCH INT 187 return minsWorked * payRate / 2; } }; abstract int overtimePay(int mins, int payRate); private static final int MINS_PER_SHIFT = 8 * 60; int pay(int minsWorked, int payRate) { int basePay = minsWorked * payRate; return basePay + overtimePay(minsWorked, payRate); } } } Jeżeli instrukcje switch w typach wyliczeniowych nie są dobre do implemento- wania operacji specyficznych dla stałych w typach wyliczeniowych, to do czego się one nadają? Instrukcje wyboru w typach wyliczeniowych są dobre do wzboga- cania zewnętrznych typów wyliczeniowych o operacje specyficzne dla stałych. Załóżmy, że typ wyliczeniowy Operation jest poza naszą kontrolą i chcemy dodać metodę instancyjną zwracającą odwrotność każdej operacji. Można symulować ten efekt za pomocą następującej metody statycznej: // Wybór ze sta(cid:225)ej do symulowania brakuj(cid:261)cej metody public static Operation inverse(Operation op) { switch(op) { case PLUS: return Operation.MINUS; case MINUS: return Operation.PLUS; case TIMES: return Operation.DIVIDE; case DIVIDE: return Operation.TIMES; default: throw new AssertionError( Nieznana operacja: + op); } } Skorzystaj z tej techniki również dla typów wyliczeniowych, które są pod Twoją kontrolą, jeśli metoda po prostu nie należy do typu wyliczeniowego. Metoda może być potrzebna przy niektórych wyliczeniach, ale nie jest na tyle użyteczna, aby rozważyć dołączenie jej do typu wyliczeniowego. Typy wyliczeniowe są porównywalne pod względem wydajności ze stałymi int. Niewielki spadek wydajności w stosunku do stałych int jest związany z ładowaniem i inicjowaniem typu wyliczeniowego. Poza urządzeniami o ograniczonych zasobach, takich jak telefony komórkowe czy tostery, mało prawdopodobne jest, aby było to w praktyce zauważalne. Kiedy należy więc korzystać z typów wyliczeniowych? Za każdym razem, gdy potrzebujemy stałego zestawu stałych znanych w momencie kompilacji. Oczywi- ście, obejmuje to „naturalne typy wyliczeniowe”, takie jak planety, dni tygodnia Poleć książkęKup książkę 188 ROZDZIAŁ 6. TYPY WYLICZENIOWE I ADNOTACJE i figury szachowe. Oprócz tego mogą to być inne zbiory, których wszystkie możli- we wartości są znane w czasie kompilacji, takie jak pozycje menu, kody operacji i znaczniki wiersza poleceń. Nie jest konieczne, aby zbiór stałych w typie wylicze- niowym był stały przez cały czas. Mechanizm typów wyliczeniowych został zapro- jektowany tak, aby pozwolić na binarną zgodność ewolucji typów wyliczeniowych. Podsumujmy. Zalety typu wyliczeniowego w stosunku do stałych int są zachę- cające. Typ wyliczeniowy jest znacznie bardziej czytelny, bezpieczny i daje więk- sze możliwości. Wiele typów wyliczeniowych nie wymaga jawnego konstruktora ani składników, ale wiele innych korzysta z zalet skojarzenia danych z każdą stałą i zapewnienia metod, których działanie jest zależne od tych danych. Znacz- nie mniej typów wyliczeniowych korzysta z kojarzenia wielu operacji z jedną metodą. W tych względnie rzadkich przypadkach lepiej użyć metod specyficznych dla stałych, sterowanych własnymi wartościami. Jeżeli wiele stałych wyliczeniowych korzysta z tych samych operacji, warto rozważyć zastosowanie wzorca typu wy- liczeniowego strategii. Temat 35. U(cid:285)ycie pól instancyjnych zamiast kolejno(cid:258)ci Wiele typów wyliczeniowych jest naturalnie skojarzonych z wartościami int. Wszystkie takie typy mają metodę ordinal, która zwraca pozycję numeryczną każdej stałej wyliczenia w tym typie. Można ulec pokusie skorzystania z wartości int metody ordinal: // Nadu(cid:298)ycie kolejno(cid:286)ci do uzyskania skojarzonej warto(cid:286)ci - NIE RÓB TAK public enum Ensemble { SOLO, DUET, TRIO, QUARTET, QUINTET, SEXTET, SEPTET, OCTET, NONET, DECTET; public int numberOfMusicians() { return ordinal() + 1; } } Choć ten typ wyliczeniowy działa, jest on koszmarem utrzymania. Jeżeli kolejność stałych zostanie zmieniona, metoda numberOfMusicians zawiedzie. Jeżeli chcemy dodać drugą stałą wyliczenia skojarzoną z wartością int, która była już użyta, mamy sporego pecha. Załóżmy, że chcemy dodać stałą do podwójnego kwartetu, który podobnie jak oktet składa się z ośmiu muzyków, ale nie ma sposobu na wykonanie tej operacji. Dodatkowo nie można dodać stałej dla wartości int bez dodawania stałej dla kolejnych wartości int. Chcemy na przykład dodać stałą reprezentującą potrójny kwartet, który składa się z dwunastu muzyków. Nie istnieje standardowy termin Poleć książkęKup książkę TEMAT 36. UŻYCIE ENUMSET ZAMIAST PÓL BITOWYCH 189 dla zespołu składającego się z jedenastu muzyków, więc jesteśmy zmuszeni do dodania dodatkowej stałej dla nieużywanej wartości int (11). Jest to co najmniej nieładne. Jeżeli mamy wiele nieużywanych wartości int, jest to niepraktyczne. Na szczęście istnieje proste rozwiązanie tych problemów. Nigdy nie należy kojarzyć wartości skojarzonej z wartością wyliczaną na podstawie kolejności; należy zamiast tego przechować ją w polu instancyjnym: public enum Ensemble { SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8), NONET(9), DECTET(10), TRIPLE_QUARTET(12); private final int numberOfMusicians; Ensemble(int size) { this.numberOfMusicians = size; } public int numberOfMusicians() { return numberOfMusicians; } } Specyfikacja Enum zawiera następujące objaśnienie dla metody ordinal: „Większość programistów nie skorzysta z tej metody. Jest zaprojektowana do użycia przez ogólne struktury danych bazujące na typach wyliczeniowych, takie jak EnumSet i EnumMap”. Jeżeli nie piszemy tego typu struktur danych, najlepiej unikać metody ordinal. Temat 36. U(cid:285)ycie EnumSet zamiast pól bitowych Jeżeli element typu wyliczeniowego był używany przede wszystkim w zbiorach, zwykle wykorzystywany był wzorzec typu wyliczeniowego int (temat 34.), w którym każdej stałej przypisywana była inna potęga 2: // Bitowe sta(cid:225)e typu wyliczeniowego - PRZESTARZA(cid:224)E! public class Text { public static final int STYLE_BOLD = 1 0; // 1 public static final int STYLE_ITALIC = 1 1; // 2 public static final int STYLE_UNDERLINE = 1 2; // 4 public static final int STYLE_STRIKETHROUGH = 1 3; // 8 // parametr jest bitow(cid:261) sum(cid:261) zero lub wi(cid:266)cej sta(cid:225)ych STYLE_ public void applyStyles(int styles) { ... } } Ta reprezentacja pozwala użyć bitowej operacji OR do połączenia kilku stałych ze zbioru, co jest znane pod nazwą pola bitowego: text.applyStyles(STYLE_BOLD | STYLE_ITALIC); Poleć książkęKup książkę 190 ROZDZIAŁ 6. TYPY WYLICZENIOWE I ADNOTACJE Reprezentacja pola bitowego pozwala również na efektywne wykonywanie zbioru operacji, takich jak suma lub część wspólna, przy użyciu arytmetyki bitowej. Pola bitowe mają wszystkie wady stałych int, jak również swoje własne. Jest nawet trud- niej zinterpretować pole bitowe niż zwykłą stałą wyliczeniową int po jej wyświetle- niu w postaci liczby. Dodatkowo nie istnieje prosty sposób na iterowanie po wszystkich elementach reprezentowanych przez pole bitowe. Co więcej, trzeba przewidzieć maksymalną liczbę bitów, która kiedykolwiek będzie potrzebna, gdy pisze się API, i na tej podstawie dobrać typ pola (int lub long). Po wybraniu ty- pu nie można przekroczyć jego szerokości (32 lub 64 bity) bez zmiany API. Niektórzy programiści, korzystający z typów wyliczeniowych zamiast stałych int, nadal upierają się przy korzystaniu z pól bitowych, gdy muszą przekazywać zbiory stałych. Nie ma powodu, aby to robić, ponieważ istnieje lepsza alternaty- wa. W pakiecie java.util znajduje się klasa EnumSet, która pozwala efektywnie reprezentować zbiór wartości jednego typu wyliczeniowego. Klasa ta implementuje interfejs Set, dzięki czemu daje bogactwo, bezpieczeństwo typów i możliwości współpracy zapewniane przez wszystkie inne implementacje Set. Jednak wewnętrz- nie każdy EnumSet jest reprezentowany jako wektor bitowy. Jeżeli bazowy typ wyliczeniowy ma nie więcej niż sześćdziesiąt cztery elementy — czyli w większo- ści przypadków — cały EnumSet jest reprezentowany za pomocą jednej liczby long, więc wydajność jest porównywalna do pól bitowych. Operacje masowe, takie jak removeAll i retainAll, są implementowane za pomocą arytmetyki bitowej, tak samo jak wykonujemy to ręcznie w przypadku pól bitowych. Jesteśmy jednak izolowani od brzydoty i podatności na błędy ręcznych manipulacji na bitach — EnumSet wykonuje za nas całą ciężką pracę. Poniżej zamieszczony jest poprzedni przykład korzystający z typu wyliczeniowego zamiast pól bitowych. Jest krótszy, czytelniejszy i bezpieczniejszy: // EnumSet - nowoczesny nast(cid:266)pca pól bitowych public class Text { public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH } // mo(cid:298)e by(cid:252) przekazany dowolny Set, ale EnumSet jest najlepszy public void applyStyles(Set Style styles) { ... } } Poniżej mamy kod klienta, który przekazuje obiekt EnumSet do metody applyStyles. EnumSet zapewnia bogaty zestaw statycznych metod fabrycznych dla ułatwienia tworzenia zbioru i jedna z nich jest użyta w tym fragmencie kodu: text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC)); Należy zwrócić uwagę, że metoda applyStyles oczekuje Set Style , a nie EnumSet (cid:180) Style . Choć wydaje się prawdopodobne, że wszystkie klienty będą przeka- zywać do metody obiekt EnumSet, to jednak dobrą praktyką jest akceptowanie typu interfejsu zamiast typu implementacji (temat 64.). Pozostawia to możliwość przekazania przez klienta w niektórych przypadkach innych implementacji Set. Poleć książkęKup książkę TEMAT 37. UŻYCIE ENUMMAP ZAMIAST INDEKSOWANIA KOLEJNOŚCIĄ 191 Podsumujmy. Użycie typu wyliczeniowego w zbiorach nie jest powodem jego reprezentacji przy użyciu pól bitowych. Klasa EnumSet łączy w sobie spójność i wydajność pól bitowych z wieloma zaletami typów wyliczeniowych opisanych w temacie 34. Jedyną rzeczywistą wadą EnumSet jest to, że nadal w Java 9 niemożliwe jest utworzenie niezmiennego obiektu EnumSet, ale najprawdopodobniej zostanie to poprawione w kolejnej wersji. Na razie można opakować EnumSet za pomocą Collections.unmodifiableSet, ale wtedy zmniejszona zostanie zwięzłość i wydajność. Temat 37. U(cid:285)ycie EnumMap zamiast indeksowania kolejno(cid:258)ci(cid:200) Czasami można napotkać kod korzystający z indeksowania tablicy lub listy za po- mocą metody ordinal (temat 35.). Oto prosty przykład klasy reprezentującej roślinę: class Plant { enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL } final String name; final LifeCycle lifeCycle; Plant(String name, LifeCycle lifeCycle) { this.name = name; this.lifeCycle = lifeCycle; } @Override public String toString() { return name; } } Załóżmy teraz, że mamy tablicę reprezentującą rośliny w ogrodzie i chcemy wyświe- tlić te rośliny zorganizowane względem typu (roczne, wieloletnie lub dwuletnie). W tym celu utworzymy trzy zbiory, po jednym dla każdego cyklu życia i będziemy iterować po elementach ogrodu, umieszczając każdą roślinę w odpowiednim zbiorze. Niektórzy programiści umieściliby te zbiory w tablicy indeksowanej warto- ścią ordinal cyklu życia: // Zastosowanie ordinal() do indeksowania tablicy - NIE RÓB TAK! Set Plant [] plantsByLifeCycle = (Set Plant []) new Set[Plant.LifeCycle.values().length]; for (int i = 0; i plantsByLifeCycle.length; i++) plantsByLifeCycle[i] = new HashSet (); for (Plant p : garden) plantsByLifeCycle[p.lifeCycle.ordinal()].add(p); Poleć książkęKup książkę 192 ROZDZIAŁ 6. TYPY WYLICZENIOWE I ADNOTACJE // wy(cid:286)wietlenie wyników for (int i = 0; i plantsByLifeCycle.length; i++) { System.out.printf( s: s n , Plant.LifeCycle.values()[i], plantsByLifeCycle[i]); } Technika ta działa, ale powoduje wiele problemów. Ponieważ tablice nie są zgodne z typami ogólnymi (temat 28.), program wymaga niekontrolowanego rzutowania, więc nie skompiluje się bez problemu. Ponieważ tablica nie ma żadnych informacji na temat tego, co reprezentuje indeks, konieczne jest ręczne nadawanie etykiet w czasie wyświetlania. Jednak najpoważniejszym problemem jest to, że przy odwo- ływaniu się do tablicy poprzez indeks będący kolejnością w typie wyliczeniowym spoczywa na nas odpowiedzialność za użycie właściwej wartości int; liczby int nie zapewniają bezpieczeństwa typów, tak jak typ wyliczeniowy. Jeżeli użyjemy niewłaściwej wartości, program wykona niewłaściwą operację lub — jeżeli będziemy mieli szczęście — zgłosi wyjątek ArrayIndexOutOfBoundsException. Na szczęście istnieje znacznie lepszy sposób na osiągnięcie tego samego efektu. Tablica efektywnie służy jako odwzorowanie pomiędzy typem wyliczeniowym a wartością, więc można w takim przypadku zastosować Map. Dokładniej — dostępna jest bardzo szybka implementacja Map, zaprojektowana do wykorzystywania z klu- czami typu wyliczeniowego, o nazwie java.util.EnumMap. Poprzedni program korzystający z EnumMap wygląda następująco: // Zastosowanie EnumMap do skojarzenia typu wyliczeniowego z danymi Map Plant.LifeCycle, Set Plant plantsByLifeCycle = new EnumMap (Plant.LifeCycle.class); for (Plant.LifeCycle lc : Plant.LifeCycle.values()) plantsByLifeCycle.put(lc, new HashSet ()); for (Plant p : garden) plantsByLifeCycle.get(p.lifeCycle).add(p); System.out.println(plantsByLifeCycle); Program ten jest krótszy, czytelniejszy i bezpieczniejszy, a dodatkowo ma wydajność zbliżoną do pierwszej wersji. Nie ma tu niebezpiecznego rzutowania; nie ma potrzeby ręcznego wyświetlania etykiet, ponieważ klucze odwzorowania są typu wyliczeniowego, który może się przekształcić na czytelną postać, a dodatkowo nie ma możliwości popełnienia błędu przy obliczaniu indeksów tablicy. Obiekty EnumMap mają porównywalną wydajność z tablicami indeksowanymi za pomocą kolejności, ponieważ korzystają one wewnętrznie właśnie z tablic. Jednak ukrywają szczegóły implementacji przed programistą, łącząc bogactwo i bezpieczeństwo typów Map z wydajnością tablicy. Należy zwrócić uwagę, że konstruktor EnumMap oczekuje jako klucza obiektu Class — jest to token typu związanego, który zapew- nia dostęp do typu ogólnego w czasie działania (temat 33.). Przedstawiony powyżej program można skrócić jeszcze bardziej, wykorzystując stru- mienie (temat 45.) do zarządzania odwzorowaniem. Oto najprostszy kod bazujący na strumieniach, który w dużej mierze powiela działanie wcześniejszego przykładu: Poleć książkęKup książkę TEMAT 37. UŻYCIE ENUMMAP ZAMIAST INDEKSOWANIA KOLEJNOŚCIĄ 193 // Uproszczone podej(cid:286)cie wykorzystuj(cid:261)ce strumienie - jest w(cid:261)tpliwe, // czy doprowadzi do utworzenia EnumMap! System.out.println(Arrays.stream(garden) .collect(groupingBy(p - p.lifeCycle))); Problemem w tym kodzie jest to, że wybierze on własną implementację odwzo- rowania, czyli w praktyce inną klasę niż EnumMap, a tym samym nie zapewni wydaj- ności i zwięzłości pamięciowej rozwiązania stosującego EnumMap w sposób jawny. Aby rozwiązać ten problem, użyjmy trójparametrowej wersji Collectors.groupingBy, która pozwala kodowi wywołującemu wskazać implementację odwzorowania w parametrze mapFactory: // Wykorzystanie strumienia i EnumMap do powi(cid:261)zania danych z wyliczeniem System.out.println(Arrays.stream(garden) .collect(groupingBy(p - p.lifeCycle, () - new EnumMap (LifeCycle.class), toSet()))); Tego rodzaju optymalizacja nie jest warta dodatkowego nakładu pracy w tak pro- stym przykładzie jak prezentowany, ale może mieć duże znaczenie w programie wykorzystującym odwzorowania w sposób niezwykle intensywny. Zachowanie wersji bazującej na strumieniu różni się nieco od wersji z EnumMap. Wersja EnumMap zawsze tworzy zagnieżdżone odwzorowanie dla każdego cyklu życia rośliny, a wersja strumieniowa tworzy zagnieżdżone odwzorowanie tylko wtedy, gdy ogród zawiera jedną roślinę lub więcej roślin o danym cyklu życia. Jeśli więc ogród zawiera rośliny jednoroczne i wieloletnie, ale nie dwuletnie, rozmiar plantsByLifeCycle będzie w wersji EnumMap wynosił trzy, a w obu wersjach strumie- niowych dwa. Można się spotkać również z tablicami tablic, indeksowanymi (dwukrotnie!) kolej- nością w typie, reprezentującymi odwzorowanie pomiędzy dwoma wartościami wyliczeniowymi. Na przykład poniższy program korzysta z takich tablic do repre- zentowania przejść pomiędzy dwoma stanami (z ciekłego na stały to zamarzanie, z ciekłego do gazowego to parowanie i tak dalej). // Zastosowanie ordinal() do indeksowania tablicy tablic - NIE RÓB TAK! public enum Phase { SOLID, LIQUID, GAS; public enum Transition { MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT; // wiersze indeksowane kolejno(cid:286)ci(cid:261) (cid:296)ród(cid:225)ow(cid:261), kolumny docelow(cid:261) private static final Transition[][] TRANSITIONS = { { null, MELT, SUBLIME }, { FREEZE, null, BOIL }, { DEPOSIT, CONDENSE, null } }; Poleć książkęKup książkę 194 ROZDZIAŁ 6. TYPY WYLICZENIOWE I ADNOTACJE // zwraca przej(cid:286)cie fazowe z jednego stanu do drugiego public static Transition from(Phase from, Phase to) { return TRANSITIONS[from.ordinal()][to.ordinal()]; } } } Program działa i może nawet wydawać się elegancki, ale wrażenie to jest złudne. Podobnie jak w przypadku wcześniejszego przykładu z ogrodem zielnym, kompila- tor nie ma możliwości poznać relacji pomiędzy kolejnością w typie i indeksami tablicy. Jeżeli popełnimy błąd w tablicy przejść lub zapomnimy zaktualizować ją przy modyfikacji typu wyliczeniowego Phase lub Phase.Transition, program ulegnie awarii w czasie działania. Awaria może przyjąć postać wyjątku ArrayIndex (cid:180)OutOfBoundsException, NullPointerException lub (co gorsza) nieprawidłowego działania. Dodatkowo wielkość tablicy jest kwadratem tablicy stanów, nawet jeżeli liczba niepustych wpisów jest mała. I w tym przypadku lepiej zastosować EnumMap. Ponieważ każde przejście fazowe jest indeksowane parą stałych stanu, najlepiej reprezentować tę relację jako odwzo- rowanie z jednego typu wyliczeniowego (stan początkowy) na odwzorowanie z drugiego typu wyliczeniowego (stan końcowy) i na wynik (przejście fazowe). Dwa stany skupienia skojarzone z przejściem fazowym najlepiej modelować przez dołączenie danych do typu wyliczeniowego przejść fazowych, które są następnie wykorzystywane do inicjowania zagnieżdżonych obiektów EnumMap: // Zastosowanie zagnie(cid:298)d(cid:298)onych EnumMap do skojarzenia danych z par(cid:261) // warto(cid:286)ci wyliczeniowych public enum Phase { SOLID, LIQUID, GAS; public enum Transition { MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID), BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID), SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID); private final Phase from; private final Phase to; Transition(Phase from, Phase to) { this.from = from; this.to = to; } // inicjalizacja odwzorowania przej(cid:286)(cid:252) mi(cid:266)dzy stanami private static final Map Phase, Map Phase, Transition m = Stream.of(values()).collect(groupingBy(t - t.from, () - new EnumMap (Phase.class), toMap(t - t.to, t - t, (x, y) - y, () - new EnumMap (Phase.class)))); Poleć książkęKup książkę TEMAT 37. UŻYCIE ENUMMAP ZAMIAST INDEKSOWANIA KOLEJNOŚCIĄ 195 public static Transition from(Phase from, Phase to) { return m.get(from).get(to); } } } Kod inicjujący odwzorowanie przejść fazowych może wydawać się nieco skompli- kowany, ale nie jest taki zły. Typem odwzorowania jest Map Phase, Map Phase, Transition , co oznacza „odwzorowanie ze stanu (źródłowego) na odwzorowanie ze stanu (docelowego) na przejście”. To odwzorowanie odwzorowań jest inicjalizo- wane kaskadową wersją dwóch kolektorów. Pierwszy kolektor grupuje przejścia na podstawie stanu początkowego, a drugi tworzy EnumMap z odwzorowaniami ze stanu docelowego do przejścia. Funkcja łącząca w drugim kolektorze ((x, y) - y) nie jest używana; potrzebujemy jej tylko dlatego, że musimy wskazać fabrykę obiek- tów map w celu uzyskania EnumMap, a Collectors oferuje fabryki teleskopowe. Poprzednie wydanie książki stosowało jawną iterację w celu inicjalizacji odwzoro- wań. Tamten kod był dłuższy, ale łatwiejszy do zrozumienia. Teraz załóżmy, że chcemy dodać do systemu nowy stan — plazmę, czyli zjoni- zowany gaz. Z tym stanem są związane tylko dwa przejścia — jonizacja, która powoduje przejście gazu w plazmę, oraz dejonizacja, czyli przejście plazmy w gaz. Aby zaktualizować program bazujący na tablicach, należy dodać nową stałą do Phase oraz dwie do Phase.Transition i zamienić oryginalną dziewięcioelemen- tową tablicę tablic na nową, szesnastoelementową. Jeżeli dodamy zbyt dużo lub za mało elementów do tablicy albo umieścimy element w niewłaściwym miejscu, wszystko pójdzie źle — program się skompiluje, ale ulegnie awarii w czasie dzia- łania. Aby zaktualizować wersję bazującą na EnumMap, wystarczy dodać PLASMA do listy stanów oraz IONIZE(GAS, PLASMA) i DEIONIZE(PLASMA, GAS) do listy przejść fazowych. // Dodanie nowe fazy za pomoc(cid:261) zagnie(cid:298)d(cid:298)onej implementacji EnumMap public enum Phase { SOLID, LIQUID, GAS, PLASMA; public enum Transition { MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID), BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID), SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID), IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS); ... // pozosta(cid:225)y kod pozostaje bez zmian } } Program zajmie się resztą, a my niemal nie będziemy mieli możliwości popełnienia błędu. Wewnętrznie odwzorowanie odwzorowań jest implementowane jako tablica tablic, więc nie poświęcimy zbyt wiele przestrzeni ani narzutu czasowego w zamian za czytelność, bezpieczeństwo i łatwość utrzymania. Poleć książkęKup książkę 196 ROZDZIAŁ 6. TYPY WYLICZENIOWE I ADNOTACJE Warto w tym miejscu zastanowić się nad rozwiązaniem z wcześniejszych przy- kładów, w których używano null do wskazania braku zmiany stanu (czyli wartości from i to były identyczne). Nie jest to dobra praktyka, bo może doprowadzić do wystąpienia wyjątku NullPointerException w trakcie działania programu. Zapro- jektowanie czystego, eleganckiego rozwiązania dla analizowanego problemu jest zadziwiająco trudne. Wynikowe programy byłyby dosyć długie, co odciągałoby uwagę od głównego celu tego tematu. Podsumujmy. Korzystanie z wartości ordinal jako indeksów tablicy jest rzadko właściwe — zamiast nich należy używać EnumMap. Jeżeli reprezentowana relacja jest wielowymiarowa, należy skorzystać z EnumMap ..., EnumMap ... . Jest to specjalny przypadek ogólnej zasady, że programiści aplikacji powinni bardzo rzadko, o ile nie wcale, korzystać z Enum.ordinal (temat 35.). Temat 38. Emulowanie rozszerzalnych typów wyliczeniowych za pomoc(cid:200) interfejsów Typy wyliczeniowe są lepsze niemal we wszystkich aspektach od wzorca bezpiecz- nego dla typów typu wyliczeniowego opisywanego w pierwszym wydaniu tej książki [Bloch01]. Jednak jednym problemem może być rozszerzalność, która jest możliwa przy zastosowaniu wzorca, ale nie jest wspierana przez konstrukcje języka. Inaczej mówiąc, przy użyciu wzorca można mieć jeden typ wyliczeniowy będący rozszerzeniem innego, natomiast przy użyciu konstrukcji języka — nie. Nie jest to przypadek. W większości przypadków rozszerzalność typów wyliczenio- wych okazuje się bardzo złym pomysłem. Mylące jest, że elementy typu rozszerzo- nego są obiektami typu bazowego i że nie zachodzi relacja odwrotna. Nie istnieje dobry sposób wyliczenia wszystkich elementów typu bazowego i jego rozszerzeń. Na koniec — rozszerzalność może skomplikować wiele aspektów projektu i implementacji. Trzeba jednak pamiętać, że istnieje co najmniej jeden kuszący przypadek użycia rozszerzalnych typów wyliczeniowych — kody operacji. Kody operacji są typem wyliczeniowym, którego elementy reprezentują operacje w pewnej maszynie, którego przykładem jest typ Operation z tematu 34., reprezentujący funkcje prostego kalkulatora. Czasami pożądane jest umożliwienie użytkownikom API dodawanie własnych operacji, co jest rozszerzaniem zbioru operacji udostępnianych przez API. Na szczęście istnieje łatwy sposób osiągnięcia tego efektu z użyciem typów wyli- czeniowych. Podstawą tego pomysłu jest wykorzystanie możliwości implemen- towania przez typy wyliczeniowe dowolnych interfejsów przez zdefiniowanie Poleć książkęKup książkę TEMAT 38. EMULOWANIE ROZSZERZALNYCH TYPÓW WYLICZENIOWYCH ZA POMOCĄ INTERFEJSÓW 197 interfejsu dla typu kodu operacji oraz typu wyliczeniowego będącego standardową implementacją interfejsu. Poniżej zamieszczona jest rozszerzalna wersja typu Operation z tematu 34. // Emulowanie rozszerzalnego typu wyliczeniowego z u(cid:298)yciem interfejsu public interface Operation { double apply(double x, double y); } public enum BasicOperation implements Operation { PLUS( + ) { public double apply(double x, double y) { return x + y; } }, MINUS( - ) { public double apply(double x, double y) { return x - y; } }, TIMES( * ) { public double apply(double x, double y) { return x * y; } }, DIVIDE( / ) { public double apply(double x, double y) { return x / y; } }; private final String symbol; BasicOperation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } } Choć typ wyliczeniowy (BasicOperation) nie jest rozszerzalny, typ interfejsu (Operation) jest, a ten typ interfejsu jest używany do reprezentowania operacji w API. Możemy zdefiniować kolejny typ wyliczeniowy, który implementuje ten interfejs, i użyć obiektów tego nowe
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

Java. Efektywne programowanie. Wydanie III
Autor:

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: