JE_04: Vyhýbejte se vytváření duplicitních objektů

Často je vhodné opakovaně používat jediný objekt a nevytvářet neustále nový funkčně shodný objekt, kdykoli je zapotřebí. Opakované použití může být rychlejší i stylovější. Objekt lze vždy opakovaně použít, pokud je neměnitelný (rada 13).

Jako extrémní příklad nesprávného postupu zvažme tento příkaz:

	// TOTO NEDĚLEJTE!
	String s = new String("hloupé");

Tento příkaz vytváří při každém svémy vykonání novou instanci String, přitom ale vůbec není zapotřebí vytvářet žádný z těchto objektů. Argument konstruktoru String("hloupé") je sám instancí String a je funkčně identický se všemi objekty vytvořenými tímto konstruktorem. Pokud se podobný příkaz vyskytuje ve smyčce nebo nějaké často volané metodě, může docházet k vytváření milionů nepotřebných instancí String.

Lepší je tento prostý příkaz:

	String s = "To už není hloupé";

Tato verze používá jedinou instanci String a nevytváří tedy novou při každém svém vykonání. navíc je zaručeno opakované použití takového objektu jakýmkoli kódem běžícím v témže virtuálním stroji, který náhodou obsahuje stejný řetězcový literál [JLS, 3.10.5].

Často se můžete vyhnout vytváření duplicitních objektů používání statických továrních metod (rada 1) namísto konstruktorů v neměnitelných třídách, které poskytují oboje. Například statická tovární metoda Boolean.valueOf(String) je téměř vždy lepší než konstruktor Boolean(String). Takový konstruktor při každém svém volání vytváří nový objekt, zatímco statická tovární metoda to nemusí dělat nikdy.

Kromě opakovaného používání neměnitelných objektů můžete rovněž opakovaně používat měnitelné objekty, o nichž víte, že se nebudou měnit. Zde máme poněkud skrytější a mnohem obecnější příklad nevhdoného uspořádání, které zahrnuje měnitelné objekty, jež se však po prvním výpočtu hodnot nemění:

public class Person {
	private final Date birthDate;
	// Další atributy vynechány

	public Person(Date birthDate) {
		this.birthDate = birthDate;
	}

	// TOHLE NEDĚLEJTE
	public Boolean isBabyBoomer() {
		Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
		gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
		Date boomStart = gmtCal.getTime();
		gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
		Date boomEnd = gmtCal.getTime();
		return birthDate.compareTo(boomStart) >= 0 &&
			birthDate.compareTo(boomEnd) < 0;
	}
}

Metoda isbabyBoomer zybtečně po každém svém volání vytváří nové instance Calendar a TimeZone a dvě instance Date. Následující verze se vyhábá této nevýkonnsoti pomocí statického inicializátoru:

class Person {
	private final Date birthDate;

	public Person(Date birthDate) {
		this.birthDate = birthDate;
	}

	
	/**
	 * Počáteční a koncové datum velkého počtu narozených dětí
	 */
	private static final Date BOOM_START;
	private static final Date BOOM_END;

	static {
		Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
		gmtCal.set(1946, Calendar.JANUAR, 1, 0, 0, 0);
		BOOM_START = gmtCal.getTime();
		gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
		BOOM_END = gmtCal.getTime();
	}

	public Boolean isBabyBoomer() {
		return birthDate.compareTo(BOOM_START) >= 0 &&
			birthDate.compareTo(BOOM_END) < 0;
	}
}

Vylepšená verze třídy Person vytváří instance Calendar, TimeZone a Date pouze jednou při své inicializaci a nevytváří je při každém volání isBabyBoomer. Důsledkem je značné zvýšení výkonnosti, pokud je tato metoda volána často. Na mém počítači trvá milion volání první metody 36000 ms, zatímco vylepšená verze trvá pouze 370 ms a je tedy stokrát rychlejší. Nejenže došlo ke zvýšení efektivnosti, ale také je kód přehlednější. Ze změny boomStart a boomEnd z lokálních proměnných na konečné statické atributy je jasné, že s těmito daty se pracuje jako s konstantami, takže je kód pochopitelnější. Je samozřejmé, že úspory získané tímto typem optimalizace nebudou vždy tak dramatické, protože vytváření instancí Calendar je zvláště náročné.

Pokud nikdy nedojde k zavolání metody isBabyBoomer, pak vylepšená verze třídy Person zbytečně inicializuje atributy BOOM_START a BOOME_END. Tyto zbytečné inicializace je možné eliminovat tak, že se tyto atributy odloženě inicializují (rada 48) při prvním zavolání metody isBabyBoomer, nelze to však doporučit. Odložené inicicalizace často komplikují implementaci a většinou nemají ani zpozorovatelný výkonnostní efekt (rada 37).

Ve všech předchozích příkladech této rady je zřejmé, že příslušné objekty lze opakovaně používat, protože jsou neměnitelné. Existují však také další situace, kde to je méně zřejmé. Zvažme příklad adaptérů [Gamma95, str. 139] označovaných rovněž za pohledy. Adaptér je jeden objekt, který deleguje základní objekt a poskytuje alternativní rozhraní základního objektu. Protože adaptér nemá žádný jiný stav než svůj základní objekt, není zapotřebí vytvářet více než jednu instanci daného adaptéru k určitému objektu.

Například metoda keySet v rozhraní Map vrací pohled Set objektu Map, který se skládá ze všech klíčů v mapě. Z naivního hlediska by se mohlo zdát, že každé volání keySet bude muset vytvořit novou instanci Set, ve skutečnosti však může každé volání keySet v daném objektu Map vrátit stejnou instanci Set. Třebaže je vrácená instance Set obvykle měnitelná, všechny vrácené objekty jsou funkčně identické: když se změní jeden vrácený objekt, změní se i všechny ostatní, protože za nimi stojí stejná instance Map.

smysl této rady nesmíte chápat tak, že vytváření objektů je náročné a je zapotřebí se mu vyhýbat. Právě naopak je totiž vytváření a odstra�ování malých objektů, jejichž konstruktory vykonávají jen velmi málo explicitních činností, velmi rychlé. To platí zejména v moderních implementacích JVM. Vytváření dalších objektů zlepšujících jasnost, jednoduchost nebo sílu programu je obecně pozitivní.

Naopak také platí, že není vhodné vyhýbat se vytváření objektů tím, že si udržujete svou vlastní sadu objektů, pokud tedy objekty v této sadě nejsou extrémně vytěžované. Prototypovým příkladem objektu, který osporavedl�uje používání sady objektů, je databázové připojení. Náklady na vytvoření takového připojení jsou natolik vysoké, že má smysl tyto objekty opakovaně používat. Obecně však platí, že vytváření sad objektů komplikuje váš kód, zvyšuje nároky na paměť a má negativní dopad na výkonnost. Moderní implementace JVM mají vysoce optimalizované nástroje čištění paměti, které podobné sady nepříliš vytěžovaných objektů snadno překonají.

Opakem k této radě je rada 24 zabývající se defenzivním kopírováním. Aktuální rada říká: "Nevytvářejte nový objekt, když byste měli opakovaně použít již existující", zatímco rada 24 říká: "Nepoužívejte opakovaně již existující objekt, když byste měli vytvořit nový". Postih za opakované používání objektu v případě, kde je zapotřebí používat defenzivní kopírování, je ale mnohem vyšší, než postih za zbytečné vytváření duplicitního objektu. Chybné nepoužívání defenzivních kopií na místech, kde jsou zapotřebí, může vést ke vzniku zákeřných chyb a bezpečnsotních děr; nadbytečné vytváření objektů má vliv jen na styl a výkonnost.