JE_24 - Vytvářejte defenzivní kopie, když jsou zapotřebí

Jedním z důvodů, proč se s programovacím jazykem Java tak dobře pracuje, je jeho zabezpečenost. Ta znamená, že je bez nativních metod imunní vůči přetečení zásobníků, přetečení polí, divokým ukazatelům a dalším chybám paměti, které zamořují jiné nezabezpečené jazyky, jkými jsou C a C++. V zabezpečeném jazyku je možné psát třídy a najisto vědět, že jejich invarianty (neměnné podmínky) zůstanou zachovány bez ohledu na to, co se stane v jakékoli jiné součásti systému. To není možné u jazyků, které zacházejí s veškerou pamětí jako s jedním gigantickým polem.

Ani v zabezpečeném jazyku nejste izolováni od dalších tříd samočinně - musíte se trochu snažit. Musíte programovat defenzivně s předpokladem, že klienti vaší třídy se budou ze všech sil snažit zničit její invarianty. K tomu může opravdu dojít, pokud se někdo pokusí narušit zabezpečení vašeho systému, spíše se však vaše třída bude muset vypořádat s neočekávaným chováním, které bude důsledkem neúmyslných chyb programátora využívajícího vaše API. V každém případě však stojí za to věnovat čas vytváření tříd, které budou vzhledem ke svým nevychovaným klientům robustní.

Třebaže jiná třída nemůže změnit interní stav nějakého objektu, aniž by jí v tom daný objekt určitým způsobem nepomohl, je neuvěřitelně jednoduché nabídnout takovou pomoc, byť jste to neměli v úmyslu. Podívejme se například na následující třídu, která by měla představovat neměnitelnou časovou periodu:

// Chybná třídy "neměnitelné" časové periody
public final class Period {

	private final Date start;
	private final Date end;

	
	/**
	 * @param start začátek periody
	 * @param end	konec periody - nesmí být před start
	 * @throws IllegalArgumentException pokud je start za end
	 * @throws NullPointerException pokud je start nebo end null
	 */
	
	public Period(Date start, Date end) {
		if (start.compareTo(end) > 0)
			throw new IllegalArgumentException(start +" po "+ end);
		this.start = start;
		this.end = end;
	}

	public Date start() {
		return start;
	}

	public Date end() {
		return end;
	}

	. . . // Zbytek vynechán
}

Na první pohled se zdá, že je tato třída neměnitelná a že si vynucuje invariant, že počátek periody se nenachází až za jejím koncem. Je však velmi snadné narušit tento invariant s využitím skutečnosti, že třída Date je měnitelná:

//Útok na vnitřek instance Period
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Mění vntiřek p!

Chcete-li ochránit vnitřek nějaké instance Period od podobného útoku, pak je důležité vytvořit defenzivní kopii každého měnitelného parametru daného konstruktoru a používat tyto kopie jako komponenty instance Period namísto originálů:

// Opravený konstruktor - vytváří defenzivní kopie parametrů
public Period(Date start, Date end) {
	this.start = new Date(start.getTime());
	this.end = new Date(end.getTime());

	if (this.start.compareTo(this.end) > 0)
		throw new IllegalArgumentException(strat +" po "+ end);
}

Jakmile použijete druhý konstruktor, pak nebude mít výše uvedený útok na instanci Period žádný vliv. Všimněte si, že je zapotřebí vytvářet defenzivní kopie před kontrolou platnosti parametrů (rada 23); navíc je nutné vykonávat kontrolu platnosti kopií a nikoli originálů. Třebaže se vám to může zdát nepřirozené, je to nutné. Chrání to totiž třídu před změnami parametrů jiným vláknem během "okna zranitelnosti" v době mezi kontrolu parametrů a okamžikem jejich zkopírování.

Rovněž si všimněte, že jsme k vytváření defenzivních kopií nepoužívali metodu clone třídy Date. Protože třída Date je nefinální, není u metody clone zaručeno vrácení objektu, jehož třídou bude java.util.Date; mohla by vrátit instanci nějaké nedůvěryhodné podtřídy speciálně navržené pro takovouto nepravost. Taková podtřída by si mohla například zaznamenat odkaz na každou instanci v nějakém soukromém statickém seznamu v okamžiku jeho vytváření a umožnit útočníkovi přístup k tomuto seznamu. Útočník by pak měl plnou kontrolu nad všemi instancemi. Chcete-li zabránit tomuto typu útoku, nepoužívejte metodu clone k vytváření defenzivní kopie parametru, z jehož typu mohou nedůvěryhodné strany vytvářet podtřídy.

Třebaže se tento náhradní konstruktor úspěšně ubrání předchozímu útoku, je možné změnit instanci Period, protože její přístupové metody nabízejí přístup k jejímu měnitelnému vnitřku:

// Druhý útok na vnitřek instance Period
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // Mění vnitřek p!

Chcete-li se bránit tomuto druhému útoku, pak jen upravte přístupové metody tak, aby vracely defnzivní kopie vnitřních atributů:

// Opravené přístupové metody - tvoří defenzivní kopie
// interních atributů
public Date start() {
	return (Date) start.clone();
}

public Date end() {
	return (Date) end.clone();
}

S novým konstruktorem a novými přístupovými metodami je Period skutečně neměnitelná. Bez ohledu na to, jak je programátor zákeřný nebo nekompetentní, skutečně neexistuje žádný způsob, kterým by bylo možné narušit invariant tak, aby byl počátek periody před jejím koncem. Je tomu tak proto, že žádná jiná třída než samotná Period nemůže získat přístup k některému z měnitelných atributů v nějaké instanci Period. Tyto atributy jsou skutečně zapouzdřené v daném objektu.

Všimněte si, že nové přístupové metody používají, na rozdíl od nového konstruktoru, k vytváření defenzivních kopií metodu clone. To je přijatelné (třebaže nikoli nutné), jelikož s jistotou víme, že třídou interních objektů Date třídy Period je java.util.Date a nikoli nějaká třeba nedůvěryhodná podtřída.

Defenzivní kopírování parametrů neslouží jen neměnitelným třídám, kdykoli npíšte nějakou metodu nebo konstruktor, který do interní struktury dat vkládá nějaký klinetem poskytovaný objekt, zamyslete se nad tím,zda je takový klientem poskytovaný objekt potencionálně měnitelný. Pokud ano, zvažte, zda může vaše třída tolerovat nějakou změnu v tomto objektu po jeho vložení do datové struktury. Je-li odpověď záporná, musíte daný objekt defenzivně kopírovat a do datové struktury vkládat tuto kopii namísto originálu. Zvažujete-li například použití klientem poskytovaného odkazu na objekt jako prvku v nějaké interní instanci Set nebo jako klíče v interní instanci Map, pak si musíte být vědomi toho, že invarianty dané množiny nebo mapy budou zničeny, pokud dojde ke změně daného objektu po jeho vložení.

Totéž platí pro defenzivní kopírování interních součástí před jejich vrácením klientům. Ať už vaše třída je nebo není měnitelná, měli byste si dvakrát rozmyslet vrácení odkazu na nějakou vnitřní součást, která je měnitelná. Je praděpodobné, že byste měli vracet defenzivní kopii. Rovněž je důležité pamatovat na to, že pole s nenulovou délkou jsou vždycky měnitelná. Proto byste měli vždy vytvořit defenzivní kopii interního pole, než je vrátíte klientovi. Alternativně můžete vrátit uživateli neměnitelný pohled na dané pole. Obě tyto techniky jsou ukázaány v radě 12.

Lze diskutovat o tom, že skutečným důsledkem celého tohoto pojednání je to, že byste měli, kdykoli je to možné, používat jako komponenty svých objektů neměnitelné objekty, abyste se nemuseli trápit s defenzivním kopírováním (rada 13). V případě našeho příkladu Period stoojí za to zdůraznit, že zkušení programátoři často používají jako interní reprezentaci času primitivní hodnotu long vrácenou Date.getTime() a nepoužívají odakz na objekt Date. Činí tak zejména kvůli tomu, že objekt Date je měnitelný.

Není vždy vhodné vytvářet defenzivní kopii nějakého měnitelného parametru před jeho integrací do objektu. Existují určité metody a konstruktory, jejichž volání indikuje explicitní přeřazení objektu odkazovaného daným parametrem. Při volání takové metody klient slibuje, že již nebude daný objekt měnit přímo. Metoda nebo konstruktor, který očekává převzetí nějakého klientem poskytnutého objektu, to musí jasně říci v dokumentaci.

Třídy obsahující metody nebo konstruktory, jejichž volání znamená přenesení řízení, se nemohou samy bránit proti zákeřným klientům. Takové třídy jsou přijatelné pouze za situace, kdy mezi třídou a jejím klientem existuje vzájemná důvěra, nebo když poškození iinvariantů třídy neuškodí nikomu jinému, než danému klientovi. Příkladem této druhé situace je vzor obalové třídy (rada 14). Podle podstaty obalové třídy by mohl klient zničit invarianty dané třídy přímým přístupem k nějakému objektu po jeho obalení, což však obvykle uškodí pouze danému klientovi.