Osvědčené postupy z knihy „Effective Java (3rd Edition)“
Úryvky z knihy „Effective Java (3rd Edition) “ od Joshuy Blocha
1. Vracejte prázdné kolekce nebo pole, ne null
Pokud metoda vrací Set, nepište pro prázdný výsledek return null; – místo toho použijte return Set.of();.
2. Zvažte statické tovární metody místo konstruktorů
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
Nejsou vždy lepší, ale v některých případech nabízejí řadu výhod:
- Jednou z výhod statických továrních metod je, že na rozdíl od konstruktorů mají jména.
- Nemusejí při každém zavolání vytvářet nový objekt.
- Mohou vracet objekt libovolného podtypu svého návratového typu.
- Třída vráceného objektu se může od volání k volání měnit v závislosti na vstupních parametrech.
- Třída vráceného objektu nemusí existovat v době, kdy se píše třída obsahující tuto metodu.
Nevýhody:
- Hlavním omezením tříd, které nabízejí pouze statické tovární metody, je, že od tříd bez veřejných nebo chráněných konstruktorů nelze dědit.
- Druhou slabinou statických továrních metod je, že je programátoři těžko hledají.
3. Při mnoha parametrech konstruktoru zvažte builder
Statické továrny i konstruktory mají společné omezení: špatně se škálují při velkém počtu volitelných parametrů. Některé projekty, na kterých jsem pracoval, to postihlo opravdu tvrdě.
4. Vlastnost singletonu vynuťte privátním konstruktorem nebo výčtovým typem
Cokoli, co nepotřebuje více instancí, by se mělo instancovat jen jednou – ať už pomocí @ApplicationScoped, nebo jiného vzoru singleton.
5. Nemožnost vytvořit instanci vynuťte privátním konstruktorem
Občas budete chtít napsat třídu, která je jen seskupením statických metod a statických polí. Takové třídy si vysloužily špatnou pověst, protože je někteří lidé zneužívají, aby se vyhnuli uvažování v objektech, ale mají i svá oprávněná využití.
private MyStaticClass() {
// private constructor to prevent instantiation
}
6. Vyhýbejte se vytváření zbytečných objektů
String s = new String("bikini"); // DON'T DO THIS!
Nebo:
// 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;
}
Řešení: Tento program dává správnou odpověď, ale je mnohem pomalejší, než by měl být, a to kvůli překlepu v jediném znaku. Proměnná sum je deklarovaná jako Long místo long, což znamená, že program vytvoří zhruba 2^31 zbytečných instancí Long (přibližně jednu pokaždé, když se long i přičte k Long sum).
7. Vyhýbejte se finalizérům a cleanerům
Finalizéry jsou nepředvídatelné, často nebezpečné a obecně zbytečné. Jejich použití může způsobit nevyzpytatelné chování, slabý výkon a problémy s přenositelností.
8. Dávejte přednost try-with-resources před try-finally
// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
9. Zvažte implementaci Comparable
Implementací Comparable umožníte své třídě spolupracovat se všemi těmi mnoha generickými algoritmy a implementacemi kolekcí, které na tomto rozhraní závisejí.
10. Minimalizujte přístupnost tříd a členů
Nejdůležitějším faktorem, který odlišuje dobře navrženou komponentu od špatně navržené, je míra, do jaké komponenta skrývá svá vnitřní data a další detaily implementace před ostatními komponentami. Dobře navržená komponenta skrývá všechny detaily implementace a čistě odděluje své API od implementace. Komponenty pak komunikují jen prostřednictvím svých API a o vnitřním fungování ostatních nic nevědí. Tento koncept, známý jako skrývání informací neboli zapouzdření, je základním principem návrhu softwaru.
11. Ve veřejných třídách používejte přístupové metody, ne veřejná pole
Občas můžete podlehnout pokušení napsat degenerované třídy, které neslouží k ničemu jinému než k seskupení instančních polí:
// Degenerate classes like this should not be public!
class Point {
public double x;
public double y;
}
Protože se k datovým polím takových tříd přistupuje přímo, nenabízejí tyto třídy výhody zapouzdření. Nemůžete změnit reprezentaci, aniž byste změnili API, nemůžete vynucovat invarianty a nemůžete při přístupu k poli provést žádnou doplňkovou akci. Zarytí zastánci objektového programování považují takové třídy za zavrženíhodné a mají za to, že by je vždy měly nahradit třídy s privátními poli a veřejnými přístupovými metodami (gettery) a u měnitelných tříd i mutátory (settery):
// 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; }
}
Pokud jde o veřejné třídy, mají zarytí zastánci rozhodně pravdu: je-li třída přístupná mimo svůj balíček, poskytněte přístupové metody, abyste si zachovali flexibilitu změnit vnitřní reprezentaci třídy. Pokud veřejná třída zpřístupní svá datová pole, je veškerá naděje na změnu reprezentace ztracena, protože klientský kód může být rozšířen široko daleko.
Pokud je však třída package-private nebo jde o privátní vnořenou třídu, není na zpřístupnění jejích datových polí nic špatného – za předpokladu, že dostatečně dobře popisují abstrakci, kterou třída poskytuje.
12. Minimalizujte měnitelnost
Neměnná třída je prostě třída, jejíž instance nelze měnit. Veškeré informace obsažené v každé instanci jsou pevně dané po celou dobu životnosti objektu, takže nikdy nelze pozorovat žádnou změnu. Knihovny platformy Java obsahují mnoho neměnných tříd, mimo jiné String, obalové třídy primitivních typů a také BigInteger a BigDecimal. Má to řadu dobrých důvodů: neměnné třídy se snáze navrhují, implementují a používají než třídy měnitelné. Jsou méně náchylné k chybám a bezpečnější. Chcete-li třídu učinit neměnnou, dodržujte těchto pět pravidel:
- Neposkytujte metody, které mění stav objektu (tzv. mutátory).
-
Zajistěte, aby třídu nebylo možné rozšířit. Tím zabráníte tomu, aby nedbalé nebo záškodnické podtřídy narušily neměnné chování třídy tím, že se budou chovat, jako by se stav
objektu změnil. Dědění se obvykle zabraňuje například tak, že se třída označí jako
final. - Všechna pole označte jako final. Jasně tím vyjádříte svůj záměr způsobem, který vynucuje samotný systém. Je to také nutné pro správné chování v případě, že se odkaz na nově vytvořenou instanci předá z jednoho vlákna do jiného bez synchronizace, jak stanoví paměťový model.
- Všechna pole udělejte privátní. Tím zabráníte klientům získat přístup k měnitelným objektům, na které pole odkazují, a přímo tyto objekty měnit. I když technicky je přípustné, aby neměnné třídy měly veřejná final pole s primitivními hodnotami nebo odkazy na neměnné objekty, nedoporučuje se to, protože to znemožňuje změnit vnitřní reprezentaci v pozdější verzi.
-
Zajistěte výhradní přístup ke všem měnitelným komponentám. Pokud má vaše třída pole odkazující na měnitelné objekty, zajistěte, aby klienti třídy nemohli získat odkazy na
tyto objekty. Nikdy takové pole neinicializujte odkazem na objekt dodaný klientem a nevracejte ho z přístupové metody. V konstruktorech, přístupových metodách a
metodách
readObjectvytvářejte obranné kopie.
13. Dávejte přednost kompozici před dědičností
Dědičnost je mocný způsob, jak dosáhnout opětovného použití kódu, ale ne vždy je pro danou práci tím nejlepším nástrojem. Při nevhodném použití vede ke křehkému softwaru. Dědičnost je bezpečné používat v rámci jednoho balíčku, kde implementace podtřídy i nadtřídy mají pod kontrolou titíž programátoři. Bezpečné je také používat dědičnost při rozšiřování tříd, které byly pro rozšiřování výslovně navrženy a zdokumentovány. Dědit od běžných konkrétních tříd přes hranice balíčků je však nebezpečné.
Na rozdíl od volání metod porušuje dědičnost zapouzdření. Možná si myslíte, že rozšířit třídu je bezpečné, pokud jen přidáváte nové metody a zdržíte se přepisování existujících metod. I když je takové rozšíření mnohem bezpečnější, není bez rizika. Pokud nadtřída v některé příští verzi získá novou metodu a vy máte tu smůlu, že jste podtřídě dali metodu se stejnou signaturou a jiným návratovým typem, vaše podtřída se už nezkompiluje. Pokud jste podtřídě dali metodu se stejnou signaturou i návratovým typem jako nová metoda nadtřídy, pak ji teď přepisujete, a vztahují se na vás tedy dříve popsané problémy. Navíc je pochybné, že vaše metoda splní kontrakt nové metody nadtřídy, protože ten kontrakt ještě nebyl napsán, když jste psali metodu podtřídy.
Naštěstí existuje způsob, jak se všem popsaným problémům vyhnout. Místo rozšiřování existující třídy dejte nové třídě privátní pole, které odkazuje na instanci existující třídy. Tento návrh se nazývá kompozice, protože existující třída se stává komponentou nové třídy. Každá instanční metoda v nové třídě volá odpovídající metodu na obsažené instanci existující třídy a vrací výsledky. Tomu se říká přeposílání (forwarding) a metody v nové třídě se nazývají přeposílací metody. Výsledná třída bude pevná jako skála, bez závislostí na detailech implementace existující třídy. Ani přidání nových metod do existující třídy nebude mít na novou třídu žádný vliv.
14. Dávejte přednost rozhraním před abstraktními třídami
Java má dva mechanismy, jak definovat typ, který umožňuje více implementací: rozhraní a abstraktní třídy. Od zavedení výchozích metod rozhraní v Javě 8 umožňují oba mechanismy poskytnout implementace některých instančních metod.
Hlavní rozdíl je v tom, že aby třída implementovala typ definovaný abstraktní třídou, musí být podtřídou této abstraktní třídy. Protože Java povoluje jen jednoduchou dědičnost, toto omezení abstraktních tříd výrazně omezuje jejich použití jako definic typů. Rozhraní může implementovat každá třída, která definuje všechny požadované metody a dodržuje obecný kontrakt, bez ohledu na to, kde se třída v hierarchii tříd nachází. Existující třídy lze snadno dodatečně upravit tak, aby implementovaly nové rozhraní.
- Rozhraní jsou ideální pro definování mixinů.
- Rozhraní umožňují budovat nehierarchické systémy typů.
- Rozhraní umožňují bezpečná a účinná rozšíření funkcionality pomocí idiomu obalové třídy. Pokud k definici typů použijete abstraktní třídy, nenecháte programátorovi, který chce přidat funkcionalitu, jinou možnost než dědičnost.
15. Generika: nepoužívejte surové typy (raw types)
Chybu neobjevíte dřív než za běhu, dlouho poté, co nastala, a v kódu, který může být daleko od kódu obsahujícího chybu. Jakmile uvidíte ClassCastException, musíte prohledat celou kódovou základnu a hledat volání metody, které vložilo minci do sbírky známek. Kompilátor vám nepomůže.
16. Generika: odstraňte nekontrolovaná varování
Když programujete s generiky, uvidíte spoustu varování kompilátoru:
- unchecked varování u přetypování
- unchecked varování u volání metod
- unchecked varování u parametrizovaných vararg typů
- unchecked varování u konverzí.
Čím víc zkušeností s generiky získáte, tím méně varování budete dostávat, ale nečekejte, že se nově napsaný kód zkompiluje bez varování. Mnoho nekontrolovaných varování lze snadno odstranit. Předpokládejme například, že omylem napíšete tuto deklaraci:
Set<Lark> exaltation = new HashSet();
Změňte ji na:
Set<Lark> exaltation = new HashSet<>();
Některá varování se budou odstraňovat mnohem obtížněji. Když narazíte na varování, která vyžadují trochu přemýšlení, vytrvejte! Odstraňte každé nekontrolované varování, které můžete.
Pokud odstraníte všechna varování, máte jistotu, že je váš kód typově bezpečný, a to je velmi dobrá věc. Znamená to, že za běhu nedostanete ClassCastException, a zvyšuje to vaši důvěru,
že se program bude chovat tak, jak jste zamýšleli. Pokud varování odstranit nemůžete, ale dokážete prokázat, že kód, který ho vyvolal, je typově bezpečný, pak (a jedině pak) varování potlačte
anotací @SuppressWarnings("unchecked"). Pokud varování potlačíte, aniž byste nejdřív prokázali, že je kód typově bezpečný, dáváte si falešný pocit
bezpečí.
Kód se možná zkompiluje bez jediného varování, ale za běhu může přesto vyhodit ClassCastException.
Pokaždé, když použijete anotaci @SuppressWarnings("unchecked"), přidejte komentář, proč je to bezpečné.
17. Generika: kdy dávají smysl
Au – třída, která generika nutně potřebuje:
Něco takového by se mělo zachytit nejpozději při merge requestu!
18. Generika: jak to nedělat
Promiňte, slova jsou zbytečná. Tohle prosím NIKDY nemergujte:
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> {
// ...
}
Pochopit pouhou deklaraci třídy by nikdy nemělo trvat 5 minut.
19. Dávejte přednost seznamům před poli
Pole se od generických typů liší ve dvou důležitých ohledech. Zaprvé jsou pole kovariantní. Toto děsivě znějící slovo jednoduše znamená, že pokud je Sub podtypem Super, je
typ pole Sub[] podtypem typu pole Super[]. Generika jsou naproti tomu invariantní: pro libovolné dva různé typy Type1 a Type2
není List<Type1> ani podtypem, ani nadtypem List<Type2>.
20. Dávejte přednost lambdám před anonymními třídami
// 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());
}
});
Místo toho udělejte tohle:
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
21. Minimalizujte rozsah platnosti lokálních proměnných
Nejúčinnější technikou, jak minimalizovat rozsah platnosti lokální proměnné, je deklarovat ji tam, kde se poprvé použije...
22. Dávejte přednost cyklům for-each před tradičními cykly for
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
... // Do something with e
}
23. Znejte a používejte knihovny
Předpokládejme, že chcete generovat náhodná celá čísla mezi nulou a nějakou horní mezí. Při tomto běžném úkolu by mnoho programátorů napsalo malou metodu, která vypadá zhruba takto:
// Common but deeply flawed!
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
Tato metoda může vypadat dobře, ale má tři vady. První je, že pokud je n malou mocninou dvojky, posloupnost náhodných čísel se po poměrně krátké periodě zopakuje. Druhou vadou je, že pokud n není mocninou dvojky, budou se některá čísla v průměru vracet častěji než jiná. Pokud je n velké, může být tento efekt dost výrazný.
Třetí vadou metody random je, že může ve vzácných případech katastrofálně selhat a vrátit číslo mimo zadaný rozsah. Je to proto, že
metoda se snaží převést hodnotu vrácenou z rnd.nextInt() na nezáporný int voláním Math.abs. Pokud nextInt() vrátí
Integer.MIN_VALUE, pak i Math.abs vrátí Integer.MIN_VALUE a operátor zbytku (%) vrátí záporné číslo, za předpokladu, že
n není mocninou dvojky. To téměř jistě způsobí selhání programu, a toto selhání může být obtížné reprodukovat.
24. Vyhněte se float a double, pokud potřebujete přesné výsledky
Pro peněžní částky používejte BigDecimal. float a double způsobují zaokrouhlovací chyby.
Předpokládejme například, že máte v kapse 1,03 dolaru a utratíte 42 centů. Kolik peněz vám zbude? Tady je naivní fragment programu, který se snaží na tuto otázku odpovědět:
System.out.println(1.03 - 0.42);
Bohužel vypíše 0.6100000000000001. Nejde o ojedinělý případ. Předpokládejme, že máte v kapse dolar a koupíte devět podložek po deseti centech. Kolik
vám vrátí?
System.out.println(1.00 - 9 * 0.10);
Podle tohoto fragmentu programu dostanete $0.09999999999999998.
Možná si myslíte, že problém vyřeší pouhé zaokrouhlení výsledků před výpisem, ale bohužel to vždy nefunguje. Předpokládejme například, že máte v kapse dolar a vidíte polici s řadou lahodných bonbonů za 10, 20, 30 centů a tak dále až po dolar. Koupíte si od každého jeden, počínaje tím za 10 centů, dokud si nemůžete dovolit další bonbon na polici.
25. Dávejte přednost primitivním typům před zabalenými primitivy
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
26. Odkazujte na objekty přes jejich rozhraní
Dobrý příklad:
Set<Son> sonSet = new LinkedHashSet<>();
Špatný příklad:
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
27. Dávejte přednost rozhraním před reflexí
Jinak:
- Přijdete o všechny výhody kontroly typů při kompilaci, včetně kontroly výjimek. Pokud se program pokusí reflexí zavolat neexistující nebo nepřístupnou metodu, selže za běhu, pokud jste neučinili zvláštní opatření.
- Kód potřebný pro reflexivní přístup je těžkopádný a rozvláčný. Je zdlouhavé ho psát a obtížné ho číst.
- Trpí výkon. Reflexivní volání metody je mnohem pomalejší než běžné volání metody.
28. Dodržujte obecně přijímané konvence pojmenování
Platforma Java má zavedenou sadu konvencí pojmenování, z nichž mnohé obsahuje The Java Language Specification [JLS, 6.1].
29. Výjimky používejte jen pro výjimečné situace
// Horrible abuse of exceptions. Don't ever do this!
try {
int i = 0;
while (true) {
range[i++].climb();
}
} catch (ArrayIndexOutOfBoundsException e) {
// catch it
}
Co tento kód dělá? Na první pohled to vůbec není zřejmé, a to je samo o sobě dostatečný důvod ho nepoužívat. Ukáže se, že jde o příšerně nepromyšlený idiom pro procházení prvků
pole. Nekonečný cyklus skončí vyhozením, zachycením a ignorováním ArrayIndexOutOfBoundsException, když
se pokusí přistoupit k prvnímu prvku mimo hranice pole. Má být ekvivalentem standardního idiomu pro procházení pole, který okamžitě pozná
každý programátor v Javě:
for (Mountain m : range) {
m.climb();
}
Výjimky, jak už jejich název napovídá, se mají používat jen pro výjimečné situace; nikdy by se neměly používat pro běžné řízení toku programu.
30. Kontrolované výjimky používejte pro situace, ze kterých se lze zotavit, a běhové výjimky pro programátorské chyby
Kontrolované výjimky používejte pro situace, u kterých lze rozumně očekávat, že se z nich volající zotaví. Tím, že uživatele postaví před kontrolovanou výjimku, mu návrhář API ukládá povinnost se z dané situace zotavit. Uživatel může tuto povinnost obejít tím, že výjimku zachytí a ignoruje, ale obvykle je to špatný nápad.
Běhové výjimky používejte k signalizaci programátorských chyb. Naprostá většina běhových výjimek signalizuje porušení předpokladů. Porušení předpokladu je prostě situace, kdy
klient API nedodrží kontrakt stanovený specifikací API. Například kontrakt pro přístup k poli stanoví, že index pole musí být mezi nulou a délkou pole
minus jedna včetně. ArrayIndexOutOfBoundsException signalizuje, že byl tento předpoklad porušen.
31. Vyhýbejte se zbytečnému používání kontrolovaných výjimek
Shrnuto: při střídmém použití mohou kontrolované výjimky zvýšit spolehlivost programů; při nadměrném používání dělají z práce s API utrpení.
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. Upřednostňujte standardní výjimky
Vlastností, která odlišuje zkušené programátory od méně zkušených, je, že zkušení usilují o vysokou míru opětovného použití kódu a obvykle jí také dosahují.
Výjimky nejsou výjimkou z pravidla, že opětovné použití kódu je dobrá věc. Knihovny Javy poskytují sadu výjimek, která pokrývá většinu potřeb většiny API při vyhazování výjimek. Opětovné použití standardních výjimek má několik výhod. Hlavní z nich je, že se vaše API snáze učí a používá, protože odpovídá zavedeným konvencím, které programátoři už znají. Těsně za ní je to, že programy používající vaše API se lépe čtou, protože nejsou zahlcené neznámými výjimkami. A konečně (a nejméně důležité): méně tříd výjimek znamená menší paměťovou náročnost a méně času stráveného načítáním tříd.
Nejčastěji opakovaně používaným typem výjimky je IllegalArgumentException. Obecně je to výjimka, kterou je vhodné vyhodit, když volající předá argument s nevhodnou hodnotou. Například
by to byla správná výjimka, pokud by volající předal záporné číslo v parametru určujícím, kolikrát se má nějaká akce opakovat. Další často opakovaně používanou
výjimkou je IllegalStateException. Obecně je to výjimka, kterou je vhodné vyhodit, pokud je volání nepřípustné kvůli stavu přijímajícího objektu. Například by to byla
správná výjimka, pokud by se volající pokusil použít nějaký objekt dřív, než byl řádně inicializován.
Dá se říct, že každé chybné volání metody se dá převést na nepřípustný argument nebo stav, ale pro určité druhy nepřípustných argumentů a stavů se standardně používají jiné výjimky. Pokud volající
předá null v parametru, pro který jsou hodnoty null zakázané, konvence velí vyhodit NullPointerException, nikoli IllegalArgumentException.
Podobně pokud volající předá hodnotu mimo rozsah v parametru představujícím index do posloupnosti, měla by se vyhodit IndexOutOfBoundsException, nikoli IllegalArgumentException.
Nepoužívejte přímo Exception, RuntimeException, Throwable ani Error. Zacházejte s těmito třídami, jako by byly abstraktní. RuntimeException však lze použít, pokud chcete
převést kontrolovanou výjimku na běhovou a opravdu víte, co děláte.
33. Neignorujte výjimky
// Empty catch block ignores exception - Highly suspect!
try {
...
} catch (SomeException e) {
}
Prázdný blok catch maří samotný smysl výjimek, kterým je donutit vás ošetřit výjimečné situace. Ignorovat výjimku je jako ignorovat požární hlásič – a ještě ho vypnout, aby nikdo jiný neměl šanci zjistit, jestli opravdu hoří.