Dobre praktyki z „Effective Java (3rd Edition)”
Fragmenty książki „Effective Java (3rd Edition) ” Joshuy Blocha
1. Zwracaj puste kolekcje lub tablice, a nie null
Jeśli metoda zwraca Set, przy pustym wyniku nie rób return null; – zamiast tego użyj return Set.of();.
2. Rozważ statyczne metody fabrykujące zamiast konstruktorów
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
Nie zawsze są lepsze, ale w niektórych przypadkach dają wiele korzyści:
- Jedną z zalet statycznych metod fabrykujących jest to, że w przeciwieństwie do konstruktorów mają nazwy.
- Nie muszą tworzyć nowego obiektu przy każdym wywołaniu.
- Mogą zwrócić obiekt dowolnego podtypu swojego typu zwracanego.
- Klasa zwracanego obiektu może się zmieniać od wywołania do wywołania w zależności od parametrów wejściowych.
- Klasa zwracanego obiektu nie musi istnieć w chwili pisania klasy zawierającej tę metodę.
Wady:
- Główne ograniczenie udostępniania wyłącznie statycznych metod fabrykujących polega na tym, że po klasach bez publicznych lub chronionych konstruktorów nie da się dziedziczyć.
- Drugą słabością statycznych metod fabrykujących jest to, że programistom trudno je znaleźć.
3. Rozważ budowniczego, gdy konstruktor ma wiele parametrów
Statyczne fabryki i konstruktory mają wspólne ograniczenie: słabo się skalują przy dużej liczbie parametrów opcjonalnych. W kilku projektach, przy których pracowałem, dało się to mocno we znaki.
4. Wymuszaj właściwość singletonu prywatnym konstruktorem lub typem wyliczeniowym
Wszystko, co nie potrzebuje wielu instancji, powinno być tworzone tylko raz – czy to przez @ApplicationScoped, czy przez inny wzorzec singletonu.
5. Wymuszaj brak możliwości tworzenia instancji prywatnym konstruktorem
Czasem zechcesz napisać klasę, która jest jedynie zbiorem statycznych metod i statycznych pól. Takie klasy mają złą reputację, bo niektórzy nadużywają ich, by uniknąć myślenia w kategoriach obiektów, ale mają też uzasadnione zastosowania.
private MyStaticClass() {
// private constructor to prevent instantiation
}
6. Unikaj tworzenia niepotrzebnych obiektów
String s = new String("bikini"); // DON'T DO THIS!
Albo:
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
Rozwiązanie: Ten program daje poprawny wynik, ale jest dużo wolniejszy, niż powinien, z powodu literówki w jednym znaku. Zmienna sum jest zadeklarowana jako Long zamiast long, co oznacza, że program tworzy około 2^31 niepotrzebnych instancji Long (mniej więcej jedną za każdym razem, gdy long i jest dodawane do Long sum).
7. Unikaj finalizatorów i cleanerów
Finalizatory są nieprzewidywalne, często niebezpieczne i zazwyczaj niepotrzebne. Ich użycie może powodować nieregularne zachowanie, słabą wydajność i problemy z przenośnością.
8. Preferuj try-with-resources zamiast try-finally
// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
9. Rozważ implementację Comparable
Implementując Comparable, pozwalasz swojej klasie współpracować z wieloma generycznymi algorytmami i implementacjami kolekcji, które opierają się na tym interfejsie.
10. Ograniczaj dostępność klas i składowych do minimum
Najważniejszym czynnikiem odróżniającym dobrze zaprojektowany komponent od źle zaprojektowanego jest to, w jakim stopniu komponent ukrywa swoje wewnętrzne dane i inne szczegóły implementacji przed innymi komponentami. Dobrze zaprojektowany komponent ukrywa wszystkie szczegóły implementacji, wyraźnie oddzielając swoje API od implementacji. Komponenty komunikują się wtedy wyłącznie przez swoje API i nic nie wiedzą o swoim wewnętrznym działaniu. Ta koncepcja, znana jako ukrywanie informacji albo hermetyzacja, jest podstawową zasadą projektowania oprogramowania.
11. W klasach publicznych używaj metod dostępowych, a nie pól publicznych
Czasem może cię kusić, by napisać zdegenerowane klasy, które służą wyłącznie do grupowania pól instancji:
// Degenerate classes like this should not be public!
class Point {
public double x;
public double y;
}
Ponieważ do pól danych takich klas sięga się bezpośrednio, klasy te nie dają korzyści płynących z hermetyzacji. Nie możesz zmienić reprezentacji bez zmiany API, nie możesz wymuszać niezmienników i nie możesz wykonać dodatkowej akcji przy dostępie do pola. Zatwardziali programiści obiektowi uważają takie klasy za herezję, którą zawsze należy zastąpić klasami z prywatnymi polami i publicznymi metodami dostępowymi (getterami), a w przypadku klas mutowalnych – metodami modyfikującymi (setterami):
// Encapsulation of data by accessor methods and mutators
class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
Z pewnością zatwardziali mają rację w odniesieniu do klas publicznych: jeśli klasa jest dostępna poza swoim pakietem, udostępnij metody dostępowe, by zachować swobodę zmiany wewnętrznej reprezentacji klasy. Jeśli klasa publiczna wystawia swoje pola danych, wszelka nadzieja na zmianę jej reprezentacji przepada, bo kod klientów może być rozsiany po całym świecie.
Jeśli jednak klasa jest pakietowa albo jest prywatną klasą zagnieżdżoną, wystawianie jej pól danych nie jest samo w sobie niczym złym – zakładając, że dobrze opisują one abstrakcję, którą klasa zapewnia.
12. Ograniczaj mutowalność
Klasa niemutowalna to po prostu klasa, której instancji nie można modyfikować. Wszystkie informacje zawarte w każdej instancji są stałe przez cały czas życia obiektu, więc nie da się zaobserwować żadnych zmian. Biblioteki platformy Java zawierają wiele klas niemutowalnych, m.in. String, klasy opakowujące typy proste oraz BigInteger i BigDecimal. Jest ku temu wiele dobrych powodów: klasy niemutowalne łatwiej zaprojektować, zaimplementować i używać niż mutowalne. Są mniej podatne na błędy i bezpieczniejsze. Aby klasa była niemutowalna, przestrzegaj tych pięciu zasad:
- Nie udostępniaj metod, które modyfikują stan obiektu (tzw. mutatorów).
-
Zadbaj o to, by klasy nie dało się rozszerzyć. Zapobiega to sytuacji, w której nieostrożne lub złośliwe podklasy naruszają niemutowalność klasy, zachowując się tak,
jakby stan obiektu się zmienił. Dziedziczeniu zapobiega się zwykle np. przez oznaczenie klasy jako
final. - Oznacz wszystkie pola jako final. W ten sposób jasno wyrażasz swój zamiar, a system go egzekwuje. Jest to też konieczne do zapewnienia poprawnego działania, gdy referencja do nowo utworzonej instancji jest przekazywana z jednego wątku do drugiego bez synchronizacji, jak opisuje to model pamięci.
- Oznacz wszystkie pola jako prywatne. Uniemożliwia to klientom uzyskanie dostępu do mutowalnych obiektów, do których odwołują się pola, i bezpośrednią modyfikację tych obiektów. Choć technicznie klasy niemutowalne mogą mieć publiczne pola final zawierające wartości proste lub referencje do obiektów niemutowalnych, nie jest to zalecane, bo uniemożliwia zmianę wewnętrznej reprezentacji w późniejszej wersji.
-
Zapewnij wyłączny dostęp do wszelkich mutowalnych komponentów. Jeśli klasa ma pola odwołujące się do obiektów mutowalnych, zadbaj o to, by jej klienci nie mogli uzyskać referencji do
tych obiektów. Nigdy nie inicjalizuj takiego pola referencją do obiektu przekazaną przez klienta ani nie zwracaj tego pola z metody dostępowej. Twórz kopie defensywne w konstruktorach, metodach dostępowych i
metodach
readObject.
13. Przedkładaj kompozycję nad dziedziczenie
Dziedziczenie to potężny sposób na ponowne wykorzystanie kodu, ale nie zawsze jest najlepszym narzędziem do danego zadania. Użyte niewłaściwie prowadzi do kruchego oprogramowania. Dziedziczenie jest bezpieczne w obrębie pakietu, gdzie implementacje podklasy i nadklasy są pod kontrolą tych samych programistów. Bezpieczne jest też przy rozszerzaniu klas zaprojektowanych i udokumentowanych specjalnie z myślą o rozszerzaniu. Natomiast dziedziczenie po zwykłych konkretnych klasach ponad granicami pakietów jest niebezpieczne.
W przeciwieństwie do wywołania metody dziedziczenie narusza hermetyzację. Możesz pomyśleć, że rozszerzanie klasy jest bezpieczne, jeśli tylko dodajesz nowe metody i nie nadpisujesz istniejących. Choć takie rozszerzenie jest znacznie bezpieczniejsze, nie jest wolne od ryzyka. Jeśli nadklasa w kolejnej wersji dostanie nową metodę, a ty masz pecha i nadałeś podklasie metodę o tej samej sygnaturze i innym typie zwracanym, twoja podklasa przestanie się kompilować. Jeśli nadałeś podklasie metodę o tej samej sygnaturze i typie zwracanym co nowa metoda nadklasy, to teraz ją nadpisujesz, więc dotyczą cię opisane wcześniej problemy. Co więcej, wątpliwe jest, by twoja metoda spełniała kontrakt nowej metody nadklasy, bo ten kontrakt nie był jeszcze napisany, gdy pisałeś metodę podklasy.
Na szczęście istnieje sposób, by uniknąć wszystkich opisanych problemów. Zamiast rozszerzać istniejącą klasę, daj nowej klasie prywatne pole, które odwołuje się do instancji istniejącej klasy. Taki projekt nazywa się kompozycją, bo istniejąca klasa staje się komponentem nowej. Każda metoda instancji w nowej klasie wywołuje odpowiadającą jej metodę na zawartej instancji istniejącej klasy i zwraca wyniki. Nazywa się to przekazywaniem (forwarding), a metody w nowej klasie – metodami przekazującymi. Powstała klasa będzie solidna jak skała, bez zależności od szczegółów implementacji istniejącej klasy. Nawet dodanie nowych metod do istniejącej klasy nie będzie miało wpływu na nową klasę.
14. Preferuj interfejsy zamiast klas abstrakcyjnych
Java ma dwa mechanizmy definiowania typu, który dopuszcza wiele implementacji: interfejsy i klasy abstrakcyjne. Od wprowadzenia metod domyślnych w interfejsach w Javie 8 oba mechanizmy pozwalają dostarczać implementacje niektórych metod instancji.
Główna różnica polega na tym, że aby zaimplementować typ zdefiniowany przez klasę abstrakcyjną, klasa musi być podklasą tej klasy abstrakcyjnej. Ponieważ Java dopuszcza tylko dziedziczenie pojedyncze, to ograniczenie klas abstrakcyjnych mocno zawęża ich zastosowanie jako definicji typów. Każda klasa, która definiuje wszystkie wymagane metody i przestrzega ogólnego kontraktu, może zaimplementować interfejs – bez względu na to, gdzie znajduje się w hierarchii klas. Istniejące klasy można łatwo dostosować do implementacji nowego interfejsu.
- Interfejsy idealnie nadają się do definiowania domieszek (mixinów).
- Interfejsy pozwalają budować niehierarchiczne struktury typów.
- Interfejsy umożliwiają bezpieczne i skuteczne rozszerzanie funkcjonalności za pomocą idiomu klasy opakowującej. Jeśli do definiowania typów używasz klas abstrakcyjnych, programiście, który chce dodać funkcjonalność, nie zostawiasz innej możliwości niż dziedziczenie.
15. Typy generyczne: nie używaj typów surowych
Błąd odkrywasz dopiero w czasie wykonania, długo po tym, jak wystąpił, i w kodzie, który może być daleko od kodu zawierającego błąd. Gdy zobaczysz ClassCastException, musisz przeszukać cały kod w poszukiwaniu wywołania metody, które wrzuciło monetę do kolekcji znaczków. Kompilator ci nie pomoże.
16. Typy generyczne: eliminuj ostrzeżenia unchecked
Gdy programujesz z użyciem typów generycznych, zobaczysz wiele ostrzeżeń kompilatora:
- ostrzeżenia o niesprawdzonym rzutowaniu (unchecked cast)
- ostrzeżenia o niesprawdzonym wywołaniu metody (unchecked method invocation)
- ostrzeżenia o niesprawdzonym sparametryzowanym typie vararg (unchecked parameterized vararg type)
- ostrzeżenia o niesprawdzonej konwersji (unchecked conversion).
Im więcej doświadczenia zdobędziesz z typami generycznymi, tym mniej ostrzeżeń dostaniesz, ale nie oczekuj, że świeżo napisany kod skompiluje się bez zastrzeżeń. Wiele ostrzeżeń unchecked łatwo usunąć. Załóżmy na przykład, że przypadkiem napiszesz taką deklarację:
Set<Lark> exaltation = new HashSet();
Zmień ją na:
Set<Lark> exaltation = new HashSet<>();
Niektóre ostrzeżenia będzie dużo trudniej usunąć. Gdy dostajesz ostrzeżenia, które wymagają zastanowienia, nie poddawaj się! Usuń każde ostrzeżenie unchecked, które tylko możesz.
Jeśli usuniesz wszystkie ostrzeżenia, masz pewność, że twój kod jest bezpieczny typowo, a to bardzo dobra rzecz. Oznacza to, że nie dostaniesz ClassCastException w czasie wykonania, i zwiększa twoją pewność,
że program będzie działał tak, jak zamierzałeś. Jeśli nie możesz usunąć ostrzeżenia, ale potrafisz udowodnić, że kod, który je wywołał, jest bezpieczny typowo, wtedy (i tylko wtedy) wycisz
ostrzeżenie adnotacją @SuppressWarnings("unchecked"). Jeśli wyciszasz ostrzeżenia, nie udowodniwszy najpierw, że kod jest bezpieczny typowo, dajesz sobie fałszywe poczucie
bezpieczeństwa.
Kod może się skompilować bez żadnych ostrzeżeń, a mimo to rzucić ClassCastException w czasie wykonania.
Za każdym razem, gdy używasz adnotacji @SuppressWarnings("unchecked"), dodaj komentarz wyjaśniający, dlaczego jest to bezpieczne.
17. Typy generyczne: kiedy mają sens
Auć – klasa, która aż prosi się o typy generyczne:
Coś takiego powinno zostać wyłapane najpóźniej przy merge requeście!
18. Typy generyczne: jak tego nie robić
Wybacz, bez komentarza. NIGDY tego nie merguj:
public abstract class JpaLearningCardBaseContentService<LC extends LearningCardBaseContent<L, LC, AC>, J extends JpaLearningCardBaseContent<L, LC, AC>, L extends
LearningCardBase<LC, A>, A extends AnswerOptionBase<L, AC>, AC extends AnswerOptionBaseContent<LC, AC, A>, AS extends AnswerOptionBaseService<A, AC, L, LC>, ACS extends
AnswerOptionBaseContentService<AC, A, L, LC>, LS extends LearningCardBaseService<L>>
extends JpaLightPersistenceService<LC, J, Long> implements LearningCardBaseContentService<LC, L, A, AC> {
// ...
}
Zrozumienie samej deklaracji klasy nie powinno nigdy zajmować 5 minut.
19. Preferuj listy zamiast tablic
Tablice różnią się od typów generycznych w dwóch ważnych aspektach. Po pierwsze, tablice są kowariantne. To groźnie brzmiące słowo oznacza po prostu, że jeśli Sub jest podtypem Super, to
typ tablicowy Sub[] jest podtypem typu tablicowego Super[]. Typy generyczne są natomiast inwariantne: dla dowolnych dwóch różnych typów Type1 i Type2
List<Type1> nie jest ani podtypem, ani nadtypem List<Type2>.
20. Preferuj lambdy zamiast klas anonimowych
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
Zamiast tego zrób tak:
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
21. Ograniczaj zasięg zmiennych lokalnych
Najskuteczniejszą techniką ograniczania zasięgu zmiennej lokalnej jest deklarowanie jej tam, gdzie zostaje po raz pierwszy użyta...
22. Preferuj pętle for-each zamiast tradycyjnych pętli for
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
... // Do something with e
}
23. Poznaj biblioteki i z nich korzystaj
Załóżmy, że chcesz generować losowe liczby całkowite od zera do pewnej górnej granicy. W obliczu tego typowego zadania wielu programistów napisałoby małą metodę, która wygląda mniej więcej tak:
// Common but deeply flawed!
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
Ta metoda może wyglądać dobrze, ale ma trzy wady. Pierwsza: jeśli n jest małą potęgą dwójki, ciąg liczb losowych zacznie się powtarzać po dość krótkim okresie. Druga wada: jeśli n nie jest potęgą dwójki, niektóre liczby będą średnio zwracane częściej niż inne. Jeśli n jest duże, efekt ten może być bardzo wyraźny.
Trzecia wada metody random polega na tym, że w rzadkich przypadkach może zawieść katastrofalnie, zwracając liczbę spoza zadanego zakresu. Dzieje się tak dlatego, że
metoda próbuje odwzorować wartość zwróconą przez rnd.nextInt() na nieujemny int, wywołując Math.abs. Jeśli nextInt() zwróci
Integer.MIN_VALUE, to Math.abs również zwróci Integer.MIN_VALUE, a operator reszty z dzielenia (%) zwróci liczbę ujemną, zakładając, że
n nie jest potęgą dwójki. To niemal na pewno spowoduje awarię programu, a błąd może być trudny do odtworzenia.
24. Unikaj float i double, jeśli potrzebujesz dokładnych wyników
Do kwot pieniężnych używaj BigDecimal. float i double powodują błędy zaokrągleń.
Załóżmy na przykład, że masz w kieszeni 1,03 $ i wydajesz 42 centy. Ile pieniędzy ci zostało? Oto naiwny fragment programu, który próbuje odpowiedzieć na to pytanie:
System.out.println(1.03 - 0.42);
Niestety wypisuje on 0.6100000000000001. To nie jest odosobniony przypadek. Załóżmy, że masz w kieszeni dolara i kupujesz dziewięć podkładek po dziesięć centów. Ile
reszty dostaniesz?
System.out.println(1.00 - 9 * 0.10);
Według tego fragmentu programu dostajesz $0.09999999999999998.
Może ci się wydawać, że problem da się rozwiązać po prostu przez zaokrąglanie wyników przed wypisaniem, ale niestety nie zawsze to działa. Załóżmy na przykład, że masz w kieszeni dolara i widzisz półkę z rzędem pysznych cukierków po 10, 20, 30 centów i tak dalej, aż do dolara. Kupujesz po jednym cukierku każdego rodzaju, zaczynając od tego za 10 centów, aż nie będzie cię stać na kolejny cukierek z półki.
25. Preferuj typy proste zamiast ich opakowań
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
26. Odwołuj się do obiektów przez ich interfejsy
Dobry przykład:
Set<Son> sonSet = new LinkedHashSet<>();
Zły przykład:
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
27. Preferuj interfejsy zamiast refleksji
W przeciwnym razie:
- Tracisz wszystkie korzyści sprawdzania typów w czasie kompilacji, łącznie ze sprawdzaniem wyjątków. Jeśli program próbuje refleksyjnie wywołać nieistniejącą lub niedostępną metodę, zakończy się to błędem w czasie wykonania, chyba że podjąłeś specjalne środki ostrożności.
- Kod potrzebny do dostępu refleksyjnego jest niezgrabny i rozwlekły. Pisze się go żmudnie i czyta z trudem.
- Cierpi na tym wydajność. Refleksyjne wywołanie metody jest znacznie wolniejsze niż zwykłe.
28. Przestrzegaj powszechnie przyjętych konwencji nazewniczych
Platforma Java ma dobrze ugruntowany zestaw konwencji nazewniczych, z których wiele zawiera The Java Language Specification [JLS, 6.1].
29. Używaj wyjątków tylko w wyjątkowych sytuacjach
// Horrible abuse of exceptions. Don't ever do this!
try {
int i = 0;
while (true) {
range[i++].climb();
}
} catch (ArrayIndexOutOfBoundsException e) {
// catch it
}
Co robi ten kod? Na pierwszy rzut oka wcale nie jest to oczywiste – i to wystarczający powód, by go nie używać. Okazuje się, że to fatalnie przemyślany idiom przechodzenia po elementach
tablicy. Nieskończona pętla kończy się przez rzucenie, przechwycenie i zignorowanie ArrayIndexOutOfBoundsException, gdy
następuje próba dostępu do pierwszego elementu poza granicami tablicy. Ma to być odpowiednik standardowego idiomu przechodzenia po tablicy, który natychmiast rozpozna
każdy programista Javy:
for (Mountain m : range) {
m.climb();
}
Wyjątki, jak sama nazwa wskazuje, należy stosować tylko w wyjątkowych sytuacjach; nigdy nie powinny służyć do zwykłego sterowania przepływem.
30. Używaj wyjątków kontrolowanych dla sytuacji, z których można się podnieść, a wyjątków czasu wykonania dla błędów programistycznych
Używaj wyjątków kontrolowanych (checked) w sytuacjach, z których można rozsądnie oczekiwać, że wywołujący się podniesie. Konfrontując użytkownika z wyjątkiem kontrolowanym, projektant API nakłada na niego obowiązek obsłużenia tej sytuacji. Użytkownik może zlekceważyć ten obowiązek, przechwytując wyjątek i ignorując go, ale zwykle to zły pomysł.
Używaj wyjątków czasu wykonania (runtime) do sygnalizowania błędów programistycznych. Ogromna większość wyjątków czasu wykonania wskazuje na naruszenie warunków wstępnych. Naruszenie
warunku wstępnego to po prostu niedotrzymanie przez klienta API kontraktu określonego w specyfikacji API. Na przykład kontrakt dostępu do tablicy określa, że indeks musi się mieścić w przedziale
od zera do długości tablicy minus jeden włącznie. ArrayIndexOutOfBoundsException sygnalizuje, że ten warunek wstępny został naruszony.
31. Unikaj niepotrzebnego używania wyjątków kontrolowanych
Podsumowując: stosowane oszczędnie, wyjątki kontrolowane mogą zwiększyć niezawodność programów; nadużywane sprawiają, że korzystanie z API staje się udręką.
Pair<Boolean, LearningCardGroup> answerLearningCard(@NotNull LearningCardGroup learningCardGroup,
LearningCardAssignment learningCardAssignment,
@NotNull Collection<AnswerOption> answers)
throws InvalidTrainingStateException, PersistenceException, LearningCardGroupAlreadyCompletedException,
ConsecutiveCorrectCountExceededException,
PreviousLearningCardGroupUncompletedException, LearningCardGroupCooldownException,
LearningCardGroupHasUncompletedLearningCardsException, InvalidParticipantException, TrainingNotSignedException; // but why ???
// this is a mistake seen a lot with junior devs
// exceptions are exceptionally slow and should be avoided for mere validation
// the performance gets even worse if a stacktrace is printed, since logging and printing is one of the slowest possible actions
32. Korzystaj ze standardowych wyjątków
Cechą, która odróżnia ekspertów od mniej doświadczonych programistów, jest to, że eksperci dążą do wysokiego stopnia ponownego wykorzystania kodu – i zwykle go osiągają.
Wyjątki nie są wyjątkiem od reguły, że ponowne wykorzystanie kodu to dobra rzecz. Biblioteki Javy udostępniają zestaw wyjątków, który pokrywa większość potrzeb większości API w zakresie rzucania wyjątków. Ponowne używanie standardowych wyjątków ma kilka zalet. Najważniejsza z nich to, że API staje się łatwiejsze do nauczenia i używania, bo odpowiada utartym konwencjom, które programiści już znają. Niewiele mniej ważne jest to, że programy korzystające z twojego API są czytelniejsze, bo nie są zaśmiecone nieznanymi wyjątkami. Wreszcie (i najmniej ważne): mniej klas wyjątków oznacza mniejsze zużycie pamięci i krótszy czas ładowania klas.
Najczęściej ponownie używanym typem wyjątku jest IllegalArgumentException. To zazwyczaj wyjątek, który należy rzucić, gdy wywołujący przekazuje argument o niewłaściwej wartości. Na
przykład należałoby go rzucić, gdyby wywołujący przekazał liczbę ujemną w parametrze określającym, ile razy ma zostać powtórzona jakaś czynność. Innym często używanym
wyjątkiem jest IllegalStateException. To zazwyczaj wyjątek, który należy rzucić, gdy wywołanie jest niedozwolone ze względu na stan obiektu, na którym je wykonano. Na przykład należałoby
go rzucić, gdyby wywołujący próbował użyć jakiegoś obiektu, zanim ten został poprawnie zainicjalizowany.
Można argumentować, że każde błędne wywołanie metody sprowadza się do niedozwolonego argumentu lub stanu, ale dla pewnych rodzajów niedozwolonych argumentów i stanów standardowo używa się innych wyjątków. Jeśli wywołujący
przekazuje null w parametrze, w którym wartości null są zabronione, konwencja nakazuje rzucić NullPointerException, a nie IllegalArgumentException.
Podobnie jeśli wywołujący przekaże wartość spoza zakresu w parametrze będącym indeksem w sekwencji, należy rzucić IndexOutOfBoundsException, a nie IllegalArgumentException.
Nie używaj bezpośrednio Exception, RuntimeException, Throwable ani Error. Traktuj te klasy tak, jakby były abstrakcyjne. Wyjątkiem jest RuntimeException, którego można użyć,
jeśli chcesz zamienić wyjątek kontrolowany na wyjątek czasu wykonania i naprawdę wiesz, co robisz.
33. Nie ignoruj wyjątków
// Empty catch block ignores exception - Highly suspect!
try {
...
} catch (SomeException e) {
}
Pusty blok catch niweczy sens wyjątków, którym jest zmuszenie cię do obsługi wyjątkowych sytuacji. Ignorowanie wyjątku przypomina ignorowanie alarmu przeciwpożarowego – i to jeszcze wyłączenie go, żeby nikt inny nie miał szansy sprawdzić, czy naprawdę się pali.