JE_06: Vyhýbejte se finalizátorům

Finalizátory jsou nepředvítdatelné, často nebezpečné a obvykle zbytečné. jejich použití může způsobovat nestejnorodé chování, chabou výkonnost a problémy s přenositelností. Finalizátory mejí několik platných použití, jimiž se budeme zabývat dále v této radě, celkově byste se však jejich používání měli vyhýbat.

Programátoři v jazyce C++ si musejí dávat pozor a nepovažovat finalizátory za analogické destruktorům C++. V jazyce C++ představují destruktory obvyklý způsob uvolnění prostředků přiřazených nějakému objektu a jsou nezbytným protějškem konstruktorů. V programovacím jazyku Java uvol�uje prostor přiřazený nějakému objektu, který již není dosažitelný, nástroj na čištění paměti a není tedy zapotřebí žádná zvláštní aktivita ze strany programátora. Destruktory C++ se rovněž používají k uvolnění dalších nepaměťových prostředků. K tomuto účelu se v programovacím jazyku Java obvykle používá příkazový blok try - finally.

Neexistuje žádná záruka, že se finalizátory vykonají okamžitě [JLS, 12.6]. Mezi okamžikem, kdy se stane nějaký objekt nedosažitelným, a okamžikem, kdy se vykoná jeho finalizátor, může uplynout libovolně dlouhá doba. To znamená, že ve finalizátoru se nikdy nesmí vykonávat nic časově kritického. Je například zásadní chybou spoléhat se na zavřením otevřených souborů jsou limitovaným prostředkem. Pokud zůstane otevřených mnoho souborů proto, že JVM vykonává finalizátory jen liknavě, může program selhat, jelikož již nedokáže otevírat další soubory.

Rychlost vykonávání finalizátorů je především funkcí algoritmu uvol�ování paměti, který se mezi implementacemi JVM liší. Podobně se může lišit i chování programu závisejícího na rychlosti vykonávání finalizátorů. je dobře možné, že takový program poběží perfektně na JVM, kde jste jej testovali, a pak těžce selže na JVM, kterému dává přednost váš nejdůležitější klient.

Liknavé finalizování není jen teoretický problém. Poskytnutí finalizátoru určité třídě může ve výjimečných situacích samovolně pozdržet vyždádání jejích instancí. Jeden můj kolega nedávno ladil dlouho běžící aplikaci GUI, která záhadně krachovala s chybou OutOfMemoryError. Analýza ukázala, že v okamžiku zhroucení měla aplikace ve své frontě finalizátorů tisíce grafických objektů, které čekaly na finalizaci a vyžádání. Bohužel však vlákno finalizátoru běželo s nižší prioritou než jiné vlákno v aplikaci, takže se objekty nefinalizovaly takovou rychlostí, jakou se řadily do fronty. JLS nijak nezaručuje, které vlákno vykoná finalizátory, takže neexistuje žádný přenositelný způsob zabránění tomuto problému, než se používání finalizátorů vyhýbat.

Nejenže JLS nezaručuje rychlé vykonání finalizátorů, dokonce ani nezaručuje, že se vůbec vykonají. Je dobře možné a dokonce i pravděpodobné, že program skončí, aniž se vykonaly finalizátory určitých již nedosažitelných objektů. Z toho vyplývá, že se nikdy nesmíte spoléhat na to, že finalizátor bude aktualizovat nějaký kritický trvalý stav. Budete-li se například spoléhat na to, že nějaký finalizátor uvolní uzamčení určitého sdíleného prostředku, například databáze, velmi dobře tak můžete kompletně zablokovat celý svůj distribuovaný systém.

Nenechte se zlákat metodami System.gc a System.runFinalization. Mohou sice zvýšit šance na vykonání finalizátorů, ale ani ony to nezaručí. Jedinými metodami, které o sobě tvrdí že zaručí finalizaci, jsou System.runFinalizationOnExit a její zlobivé dvojče runtime.runFinalizationOnExit. Tyto metody však mají zásadní chyby a jejich používání se nedoporučuje.

Pokud ještě nejste přesvědčeni, že je zapotřebí vyhýbat se finalizátorům, pak zvažte ještě jednu věc: pokud dojde k vyvolání nezachycené výjimky během finalizace, pak se tato výjimka ignoruje a finalizace daného objektu se ukončí [JLS, 12.6]. Nezachycené výjimky mohou zanechat objekty v poškozeném stavu. Pokud se jiné vlákno pokusí použít takový poškozený objekt, důsledkem může být libovolné a přesně nepostižitelné chování. Nezachycená výjimka obvykle ukončí dané vlákno a vytsikne výpis zásobníku, nikoli však když se vyskytne ve finalizátoru - v tom případě se dokonce ani nevytiskne varování.

Tak co byste tedy měli učinit místo vytvoření finalizátoru určité třídy, jejíž objekty zahrnují prostředky vyžadující ukončení, například soubory nebo vlákna? Prostě poskytněte explicitní ukončovací metodu a požadujte po klientech dané třídy volání této metody u každé instance, která již není zapotřebí. Je důležité zmínit se ještě o tom, že daná instance musí sledovat, zda již byla ukončena: explicitní ukončovací metoda musí zaznamenat do nějakého soukromého atributu, že daný objekt již není platný, a ostatní metody musí tento atribut prověřovat a vyvolat výjimku IllegalStateException, pokud dojde k jejich zavolání po ukončení daného objektu.

Typickým příkadem explicitní ukončovací metody je metoda close v InputSttream a OutputStream. Dalším příkladem je metoda cancel v java.util.Timer, která vykonává nezbytnou změnu stavu způsobující, že se vlákno přiřazené instanci Timer samo pěkně ukončí. Mezi příklady z java.awt patří Graphics.dispose a Window.dispose. Tyto metody jsou často přehlíženy, což má pochopitelně neříznivý vliv na výkonnost. Související metodou je Image.flush, která ruší přiřazení všech prostředků souvisejících s nějakou instancí Image, ale ponechává ji ve stavu, kdy ji lze dále použít opakovaným alokováním prostředků podle potřeby.

Explicitní ukončovací metody se často používají v kombinaci s konstrukcí try - finally, čímž se zajišťuje rychlé ukončení. Volání explicitní ukončovací metody uvnitř klauzule finally zajišťuje její vykonání dokonce i v případě, kdy je při používání daného objektu vyvolána výjimka:

	// Blok try - finally zajišťuje vykonání ukončovací metody
	Foo foo = new Foo(...);
	
	try {
		// Uči�te co je zapotřebí
	} finally {
		foo.terminate(); // Explicitní ukončovací metoda
	}

Jsou tedy vlastně finalizátory k něčemu dobré? Ve skutečnosti mají dvě platná využití. Jedním je jejich funkce "bezpečnostní sítě" pro případ, kdy vlastník objektu zapomene zavolat explicitní ukončovací metodu, kteoru jste vytvořili podle rady v předchozím odstavci. Třebaže není zaručeno, že se finalizátory zavolají rychle. Je lepší uvolnit kritický prostředek později než nikdy v těch (doufejme zřídkavých) případech, kdy klient nenaplní svou část dohody a nezavolá explicitní ukončovací metodu. Tři třídy zmíněné jako příklady vzoru explicitní ukončovací metody (InputStream, OutputStream a Timer) mají také finalizátory, které slouží jako bezpečnsotní sítě pro případ, kdy nejsou zavolány jejich ukončovací metody.

Druhé legitimní využití finalizátorů souvisí s objekty s nativními kolegy. Nativní kolega je určitý nativní objekt, do nějž se deleguje normální objekt prostřednictvím nativních metod. Protože nativní kolega není normální objekt, nástoj uvol�ování paměti o něm nic neví a nemůže jej odstranit, jakmile odstraní jeho normálního kolegu. Finalizátory představují vhodný nástroj pro tuto činnost za předpokladu, že nativní kolega nobsahuje žádné kritické prostředky. Pokud nativní kolega obsahuje prostředky, které je rychle zapotřebí ukončit, pak by měla mít daná třída explicitní ukončovací metodu, jak bylo řečeno výše. Ukončovací metoda by měla učinit vše potřebné k uvolnění daného kritického prostředku. Ukončovací metoda může být nativní metoda nebo může takovou metodu volat.

Je důležité uvědomit si, že nedochází automaticky k "řetězení finalizátorů". Má-li nějaká třída (jiná než Object) finalizátory a nějaká podtřída je překryje, pak musejí finalizátory podtřídy volat finalizátory nadtřídy manuálně. Měli byste ukončit podtřídu v bloku try a zavolat finalizátory nadtřídy v odpovídajícím bloku finally. Tím zajistíte vykonání finalizátorů nadtřídy i v případě, kdy finalizace podřídy vyvolá výjimku a naopak:

	// Manuální řetězení finalizátorů
	protected void finalize() throws Throwable {
		try {
			// Finalizování stavu podtřídy
		} fianlly {
			super.finalize();
		}
	}

Pokud implementátor podtřídy překryje finalizátory nadtřídy, ale zapomene zavolat finalizátory nadtřídy manuálně (nebo to neučiní schválně), finalizátor nadtřídy se nikdy nezavolá. Je možné bránit se před takovou nedbalou nebo zákeřnou podtřídou za cenu vytvoření dodatečného objektu pro každý objekt, který se má ukončit. Finalizátory pak neumisťujte do třídy vyžadující finalizaci, ale umístěte je do anonymní třídy (rada 18), jejíž jediným smyslem je finalizovat svou obklopující instanci. Pro každou instanci obklopující třídy se vytvoří jedna instance anonymní třídy označovaná za strážce finalizátoru. Obklopující instance si uloží jediný odkaz na svého strážce finalizátoru do soukromého atributu instancí, takže strážce finalizátoru bude možné fianlizovat v témže okamžiku jako obklopující instanci. Jakmile je strážce finalizován, vykoná finalizační činnost vyžadovanou obklopující instanci, stejně jako by byly její finalizátory metodou v obklopující třídě:

// Princip strážce finalizátoru
public class Foo {
	// Jediným smyslem tohoto objektu je finalzovat vnější objekt Foo
	private final Object finalzeGuardian = new Object() {
		protected void finalize() throws Throwable {
			// Finalizace vnějšího objektu Foo
			. . .
		}
	};
	. . .
}

Všimněte si, že veřejná třída Foo nemá žádné finalizátory (kromě jednoho triviálního, který dědí od Object), takže nezáleží na tom, zda finalizátor podtřídy volá super.finalize() nebo ne. Tuto techniku byste měli zvážit u každé nefinální veřejné třídy, která má finalizátor.

Z toho všeho vyplývá, že byste neměli používat finalizátory jinak, než jako bezpečnostní síť nebo k ukončení nekritických nativních prostředků. V těch vyjímečných případech, kdy nějaký finalizátor používáte, nezapome�te zavolat super.finalze(). Konečně také platí, že potřebujete-li přiřadit finalizátor veřejné nefinální třídě, pak zvažte použití strážce finalizátoru. Tím zajistíte vykonání finalizátoru i v případě, kdy finalizátor podtřídy nezavolá super.finalize().