JE_13 - Dávejte přednost neměnitelnosti

Neměnitelná třída je třída, jejíž instance nelze změnit. Všechny informace obsažené v každé instanci se poskytnou při jejím vytváření a jsou pak pevně dány po celou dobu života daného objektu. Knihovny platformy Java obsahují mnoho neměnitelných tříd, mezi něž patří String a primitivní obalové třídy BigInteger s BigDecimal. Existuje pro to mnoho dobrých důvodů: Neměnitelné třídy se navrhují, implementují a používají snáze, než třídy měnitelné. Jsou také méně náchylné k chybám a jsou i zabezpečenější. Chcete-li učinit nějakou třídu neměnitelnou, dodržte následujících pět pravidel:

  1. Neposkytujte žádné metody měnící daný objekt. (tzv. mutátory)
  2. Zajistěte, aby nebylo možné překrýt žádné metody. Zabráníte tak mechtěnému nebo dokonce i úmyslnému narušení neměnitelného chování dané třídy v jejích podtřídách. Překrývání metod se obvykle zabra�uje tak, že se třída učiní finální, existují však i jiné alternativy, které si popíšeme později.
  3. Uči�te všechny atributy finálními. To jasně vyjadřuje váš úmysl způsobem, který je systémem vynucen. Může být rovněž zapotřebí zajistit správné chování, pokud se předává odkyz na nově vytvořenou instanci z jednoho vlákna do jiného bez synchronizace, v závislosti na výsledcích stále probíhající snahy o přepracování paměťového modelu.
  4. Uči�te všechny atributy soukromými. Tím zabráníte klientům v přímé změně atributů. Třebaže je technicky možné, aby měly neměnitelné třídy veřejné finální atributy obsahující primitivní hodnoty nebo odkazy na neměnitelné objekty, nedpoporučuje se to, jelikož se zabra�uje ve změně interní reprezentace v novějších verzích (rada 12).
  5. Zajistěte exkluzivní přístup ke všem měnitelným komponentám. Má-li vaše třída nějaké atributy, které se odakzují na měnitelné objekty, pak zajistěte, aby nemohli klienti dané třídy získat odkazy na tyto objekty. Nikdy neinicializujte takový atribut odkazem na objekt poskytnutým klientem ani nevracejte odkaz na objekt z přístupové metody. Vytvářejte defenzivní kopie (rada 24) v metodách konstruktorů, přístupových metodách a v metodě readObject (rada 56).

Mnoho z ukázkových tříd v předchozích radách je neměnitelných. jednou takovou třídou je PhoneNumber v radě 8, která má přístupové metody pro všechny atributy, ale žádné odpovídající mutátory. Zde máme trochu složitější příklad:

public final class Complex {
	private final float re;
	private final float im;

	public Complex(float re, float im) {
		this.re = re;
		this.im = im;
	}

	//Přístupové metody bez odpovídajících mutátorů
	public float realPart() {
		return re;
	}

	public float imaginaryPart() {
		return im;
	}

	public Complex add(Complex c) {
		return new Complex(re + c.re, im + c.im);
	}

	public Complex subtract(Complex c) {
		return new Complex(re - c.re, im - c.im);
	}

	public Complex multiply(Complex c) {
		return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
	}

	public Complex divide(Complex c) {
		float tmp = c.re * c.re + c.im * c.im;
		return new Complex((re * c.re + im * c.im) / tmp, (im * c.re - re * c.im) / tmp);
	}

	public boolean equals(Object o) {
		if (o == this)
			return true;
		if (!(o instanceof Complex))
			  return false;
		Complex c = (Complex)o;
		return (Float.floatToIntBits(re) == Float.floatToIntBits(c.re)) &&
		       (Float.floatToIntBits(im) == Float.floatToIntBits(c.im));
	}

	public int hashCode() {
		int result = 17 + Float.floatToIntbits(re);
		result = 37 * result + Float.floatToIntBits(im);
		return result;
	}

	public String toString() {
		return "("+ re +" + "+ im +"i)";
	}
}

Tato třída představuje komplexní číslo (číslo s reálnou a imaginární částí). Kromě standardních metod třídy Object poskytuje přístupové metody pro reálnou a imaginární část a nabízí také čtyří základní aritmetické operace: sčítání, odečítání, násobení a dělení. Všimněte si, jak tyto aritmetické operátory vytvářejí a vracejí novou instanci Complex a nemění tedy tuto instanci. Takový vzor se označuje za funkční přístup, protože metody vracejí výsledek aplikování nějaké funkce na jejich operand, který nemění. Opakem je častější procedurální přístup, v němž metody aplikují nějakou proceduru na svůj operand a způsobují jeho změnu stavu.

Funkční přístup se může zdát nepřirozený, pokud jej dobře neznáte, umož�uje však dodržovat neměnitelnost, což má mnoho výhod. Neměnitelné objekty jsou jednoduché. Neměnitelný objekt se může nacházet přesně v jednom stavu, ve stavu, v jakém byl vytvořen. Pokud zajistíte, že všechny konstruktory napl�ují invarianty třídy, pak je zaručeno, že tyto invarianty zůstanou pravdivé navždy, přičemž nemusíte vy ani programátor, používající tuto třídu, dělat už nic jiného. Měnitelné objekty mohou mít na druhou stranu libovolně složité stavové prstory. Pokud dokumentace neposkytuje přesný popis stavových přechodů vykonávaných metodami mutátorů, může být obtížné nebo dokonce nemožné používat měnielnou třídu spolehlivě.

Neměnitelné objekty jsou svou podstatou zabezpečené z hlediska vláken - nevyžadují synchronizaci. Nelze je poškodit, když k nim přistupuje více vláken najednou. To je zdaleka nejjednodušší přístup k zajištění bezpečnosti vláken. Žádné vlákno nemůže nikdy na neměnitelném objektu vliv jiného vlákna. Proto lze neměnitelné objekty volně sdílet. Neměnitelné třídy by toho měly využívat a podněcovat klienty k opakovanému používání existujících instancí, kdykoli je to možné. Jednou z možností, jak toho snadno dosáhnout, je poskytovat veřejné statické finální konstanty pro často používané hodnoty. Například třída Complex by mohla poskytovat tyto konstanty:

public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE  = new Complex(1, 0);
public static final Complex I    = new Complex(0, 1);

Tento přístup lze posunout ještě o krok dále. Neměnitelná třída může poskytovat statické tovární metody, které ukládají do mezipaměti (cache) často požadované instance a tím se vyhýbá vytváření nových instancí, kdykoli je vyžadována nějaká již existující instance. Takové statické tovární metody mají třídy BigInteger a boolean. Používání takových statických továrních metod má za následek, že klienti sdílejí již existující instance a nevytvářejí si nové, což snižuje spotřebu paměti a náklady na její uvol�ování.

Důsledkem skutečnosti, že neměnitelné objekty lze volně sdílet, je to, že nikdy nemusíte vytvářet defenzivní kopie (rada 24). Vlastně nemusíte vytvářet vůbec žádné kopie, protože takové kopie by byly navždy ekvivalentní svým originálům. Proto nemusíte a ani byste neměli poskytovat metodu clone nebo kopírovací konstruktor (rada 10 v neměnitelné třídě. To ještě nebylo v začátcích platformy Java tak zřejmé, takže třída String má konstruktor kopírování, měli byste jej však používat jen výjimečně nebo se mu raději vyhýbat (rada 4).

Nejenže můžete sdílet neměnitelné objekty, ale můžete sdílet i jejich vnitřní záležitosti. Například třída BigInteger používá interně reprezentaci znaménka a velikosti. Znaménko je představováno typem int a velikost je reprezentována polem int. Metoda negate vytváří nový BigInteger stejné velikosti a opačného znaménka. Pole nemusí kopírovat; nově vytvořený BigInteger ukazuje na stejné interní pole, jako originál.

Neměnitelné objekty jsou výbornými stavebními bloky pro další objekty. A to jak pro měnitelné tak pro neměnitelné. Je mnohem jednodušší spravovat invarianty složitého objektu, pokud víte, že se jeho komponentové (složkové) objekty nezmění. Speciálním případem tothoto principu je skutečnost, že neměnitelné objekty jsou výbornými klíči map a prvky množin; jakmile jsou v mapě nebo množině, pak si nemusíte dělat starosti se změnou jejich hodnot, ktrá by zničila invarianty dané mapy nebo množiny.

Jedinou opravdovou nevýhodou neměnitelných tříd je to, že vyžadují samostatný objekt pro každou odlišnou hodnotu. vytváření takových objektů může být nákladné, zejména jsou-li velké. Předpokládejme například, že máte objekt BigInteger s milionem bitů a že chcete vytvořit komplement (doplněk) jeho nejnižšího bitu:

BigInteger moby = ...;
moby = moby.flipBit(0);

Metoda flipBit vytvoří novu instanci BigInteger, rovněž milión bitů dlouhou, která se bude lišit od té původní jediným bitem. Taková operace vyžaduje nějaký čas a prostor proporcionální k velikosti BigInteger. Porovnejte si to s java.util.BitSet. Podobně jako BigInteger reprezentuje BitSet libovolně dlouhou sekvenci bitů, ale narozdíl od BigInteger je třída BitSet měnitelná. Třída BitSet poskytuje metodu, která vám umož�uje změnit stav jediného bitu instance s milionem bitů v konstantním čase.

Problém s výkonností je ještě zásadnější, pokud vykonáváte operaci o několika krocích, která v každém kroku vytváří nový objekt, a nakonec se zbavíte všech objektů s výjimkou konečného výsledku. S tímto problémem se můžete vyrovnat dvěma způsoby. Jednak můžete odhadnout, které vícekroké operace budou obecně požadované a ty pak poskytnout jako primitiva. Je-li nějaká operace s více kroky poskytnuta jako primitivum, pak nemusí daná neměnitelná třída vytvářet samostatný objekt v každém kroku. Interně může být taková neměnitelná třída libovolně šikovná. BigInteger má například "doprovodnou" měnitelnou přátelskou třídu, kterou používá ke zrychlení vícekrokých operací, jako je například modulární umoc�ování. Z výše uvedených důvodů je mnohem těžší používat tuto měnitelnou doprovodnou třídu, naštěstí to ale nemusíte dělat. Implementátoři třídy BigInteger za vás všechnu tu složitou práci již vykonali.

Tento přístup funguje výborně, dokážete-li přesně předpovědět, které vícekroké operace budou chtít klienti ve vaší neměnitelné třídě vykonávat. Pokud to nedokážete, pak bude nejlepší poskytnout nějakou veřejnou měnitelnou doprovodnou třídu. Hlavním příkladem tohoto přístupu v knihovnách platformy Java je třída String, jejímž měnitelným doprovodem je StringBuffer. Lze rovněž diskutovat o tom, že BitSet za určitých podmínek hraje roli měnitelného doprovodu ke třídě BigInteger.

Protože nyní víte, jak vytvořit neměnitelnou třídu, a chápete výhody a nevýhody neměnitelnosti, popišme si několik alternativ návrhu. Vzpome�te si, že pro zaručení neměnitelnosti, nesmí třída umožnit překrytí žádné ze svých meotd. Kromě učinění třídy finální existují další dvě možnosti, jak toto zajistit. Jednou z nich je učinit finální každou z metod dané třídy, nikoli však třídu samotnou. Jedinou výhodou tohoto přístupu je to, že umož�uje programátorům rozšiřovat danou třídu přidáváním nových metod vybudovaných nad těmi staršími. Stejně efektivní je poskytnout nové metody jsko statické metody v oddělené pomocné třídě, z níž nelze vytvářet instance (rada 3), takže tento přístup se nedoporučuje.

Druhou alternativou k učinění neměnitelné třídy finální je učinit všechny její konstruktory soukromým nebo přátelskými a přidat veřejné statické tovární metody namísto veřejných konstruktorů (rada 1). Aby to bylo vše konkrétnější, podívejme se, jak bude s využitím tohoto přístupu vypadat třída Complex:

public class Complex {
	private final float re;
	private final float im;

	private Complex(float re, float im) {
		this. re = re;
		this. im = im;
	}

	public static Complex valueOf(float re, float im) {
		return new Complex(re, im);
	}

	... // Zbytek se nemění
}

Třebaže se tento přístup obecně nepoužívá, jedná se často o tu nejlepší ze všech tří alternativ. Je nejflexibilnější, protože umož�uje používání víc implementačních přátelských tříd. Pro klienty, kteří se nacházejí mimo její balíček, je taková neměnitelná třída vlastně finální, protože není možné rozšířit třídu, která pochází z jiného balíčku a které chybí nějaký veřejný nebo chráněný konstruktor. Kromě toho, že tento přístup zaručuje pružnost více implementačních tříd, umož�uje také vyladit výkonnost dané třídy v dalších verzích zlepšením schopností statických továrních metod ukládat si objekty do mezipaměti.

Statické tovární metody mají mnoho dalších výhod oproti konstruktorům, jak bylo popsáno v radě 1. Předpokládejme třeba, že hcete poskytnout nějaký prostředek pro vytváření komplexního čísla podle jeho polárních souřadnic. S využitím konstruktorů by to bylo velmi zmatené, protože přirozený konstruktor by měl stejný podpis, jaký jsme již použili: Complex(float, float). Se statickými továrnami je to jednoduché, stačí jen přidat druhou statikou tovární metodu, jejíž název jasně určuje její funkci:

public static Complex valueOfPolar(float r, float theta) {
	return new Complex((float)(r * Math.cos(theta)),
			   (float)(r * Math.sin(theta)));
}

V době, kdy byly vytvářeny třídy BigInteger a BigDecimal, ještě nebylo dobře známé, že neměnitelné třídy musejí být v zásadě finální, takže jejich metody lze překrývat. Bohužel z důvodů kompatibility nelze tento problém již napravit. Jestliže píšete třídu, jejíž zabezpečení závisí ne neměnitelnosti nějakého argumentu BigInteger nebo BigDecimal od pověřeného klienta, pak musíte kontrolovat, že je takový argument "skutečný" BigInteger nebo BigDecimal a ne jen nějaká instance určité pověřené podtřídy. Jedná-li se o druhou možnost, musíte jej defenzivně kopírovat s předpokladem, že může být měnitelný (rada 24):

public void foo(BigInteger b) {
	if (b.getClass() != BigInteger.class)
		b = new BigInteger(b.toByteArray());
	...
}

Výčet pravidel neměnitelných tříd na začátku této rady říká, že daný objekt nesmějí změnit žádné metody a že všechny atributy musejí být finální. Ve skutečnosti jsou tato pravidla trochu slinější, než je nutné, a lze je zmírnit v zájmu zvýšení výkonnosti. Platí totiž, že žádná metoda nesmí vytvářet externě viditelnou změnu ve stavu takového objektu. Mnoho neměnitelných tříd má však jeden nebo více nefinálních redundatních atributů, do nichž si ukládají výsledky náročných výpočtů v okamžiku, kdy jsou poprvé vyždaovány. Je-li stejný výpočet požadován opakovaně, pak se vrátí již uložená hodnota a omezí se tak náklady na opakovaný výpočet. Tento trik funguje právě proto, že je daný objekt neměnitelný; jeho neměnitelnost zaručuje, že výpočet, pokud by byl znovu proveden, poskytne stejný výsledek.

Například metoda hashCode třídy PhoneNumber (rada 8) vypočítává při prvním volání hešovací kód a ten si pak ukládá do paměti pro případ, kdyby byl ještě zapotřebí. Tato tchnika, která je klasickým příkladem odložené inicializace (rada 48), je rovněž využíván třídou Sring. Synchronizace není zapotřebí, protože není žádným problémem, je-li hešovací hodnota přepočítávána jednou nebo dvakrát. Zde máme obecný idiom vracející uloženou, odloženě inicializovanou funkci nějakého neměnitelného objektu:

//Uložená, odložen inicializovaná funkce neměnitelného objektu
private volatile Foo cachedFooVal = UNLIKELY_FOO_VALUE;

public Foo fool() {
	if (result == UNLIKELY_FOO_VALUE)
		result = cachedFooVal();
	return result;
}

//Soukromá pomocná funkce vypočítávající naši hodnotu foo
private Foo fooVal() { ... }

Musím se zmínit ještě o jedné nepříjemnosti týkající se serializovatelnosti. Pokud se rozhodnete, že bude vaše neměnitelná třída implementovat Serializable, přičemž obsahuje jeden nebo více atributů, jež se odkazují na měnitelné objekty, pak musíte poskytnout explicitní metodu readObject nebo readResolve i v případě, kdy je výchozí serializovaná forma přijatelná. Výchozí metoda readObject by umožnila útočníkovi vytvořit neměnitelnou instanci vaší jinak neměnitelné třídy. Tímto tématem se podrobně zabývá rada 56.

Celkově tedy platí, že byste měli odolat touze napsat metodu set pro každou metodu get. třídy by měly být neměnitelné, pokud neexistuje velmi dobrý důvod k tomu, učinit je měnitelnými. Neměnitlné třídy mají mnoho výhod a jejich jedinou neýhodou jsou možné potíže s výkonností za určitých podmínek. Malé hodnotové objekty, jakými jsou například PhneNumber a Complex, byste měli vždy činit neměnitelnými. V knihovnách platformy Java existuje několik tříd, mezi něž patří třídy java.util.Date a java.awt.Point, které by měly být neměnitelné, ale nejsou. Vážně byst se měli zamyslet také nad zajištěním neměnitelnosti větších hodnotových objektů, jako jsou String a BigInteger. Veřejnou měntelnou třídu byste měli pro svou neměnitelnou třídu zajistit, pouze jakmile si potvrdíte, že je zapotřebí k dosažení uspokojivé výkonnosti (rada 37).

Existují určité třídy, u nichž je neměnitelnost nepraktická. Sem patří "procesní třídy", jakými jsou Thread a TimerTask. Nelze-li učinit nějakou třídu neměnitelnou, pak byste měli její měnitelnost omezit v co největší možné míře. Snížení počtu stavů, v nichž může určitý objekt existovat, usnad�uje zvažování daného objektu a snižuje pravděpodobnost vzniku chyb. Konstruktory by měly vytvářet plně inicializované objekty se všemi jejih invarianty nastavenými(invarianty = neměnitelné podmínky). Neměly by předávat částečně zkonstruované instance dalším metodám. Neměli byste poskytovat veřejnou inicializační metodu oddělenou od konstruktoru, pokud pro to nemáte velmi dobrý důvod. Podobně byste neměli poskytovat "reinicializační" metodu, která umož�uje objekt znovu používat, jako by byl zkonstruován s jiným počátečním stavem. Reinicializační metoda obvykle nabízí jen malý výkonnostní příspěvek za cenu zvýšené složitosti.

Tyto principy příkladně napl�uje třída TimerTask. Je měnitelná, její stavový prostor je však úmyslně zachován jako malý. Vytvoříte instanci, naplánujete její vykonání a volitelně ji také zrušíte. Jakmile se taková úloha dokončí nebo je zrušena, nelze ji opakovaně naplánovat.

Třída Complex v této radě si zasluhuje ještě poslední poznámku. Tento příklad má pouze ilustrovat neměnitelnost. Nejdená se o úplnou a použitelnou implemntaci komplexních čísel. Pro komplexní násobení a dělení používá standardní vzorce, které se nesprávně zaokrouhlují, a poskytuje jen slabou sémantiku pro komplexní hodnoty NaN a nekonečna.