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:

Nevýhody:

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:

  1. Neposkytujte metody, které mění stav objektu (tzv. mutátory).
  2. 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.
  3. 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.
  4. 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.
  5. 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 readObject vytvář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í.

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:

Čí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:

Příklad třídy, která potřebuje generika

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:

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ří.