JE_10: Přepisujte clone s rozvahou

Rozhraní Cloneable bylo zamýšleno jako smíšené rozhraní (rada 16) pro objekty, které inzerují, že umož�ují klonování. Bohužel však tento účel nenapl�uje. Jeho hlavní nedostatek spočívá v tom, že mu chybí nějaká metoda clone a metoda clone třídy Object je chráněná. Nelze tedy, pokud se neuchýlíte k reflexi (rada 35), zavolat metodu clone nějakého objektu jen proto, že implementuje Cloneable. Dokonce i reflexní volání může selhat, protože není zaručeno, že má daný objekt nějakou přístupnou metodu clone. Bez ohledu na tento a další nedostatky se ale tento nástroj používá tak široce, že se vyplatí porozumět mu. Tato rada vám vysvětlí, jak implementovat metodu clone s dobrým chováním, popíše, kdy je tak vhodné učinit, a krátce se zmíní také o dalších alternativách.

Tak co vlastně Cloneable dělá, když neobsahuje žádné metody? Určuje chování chráněné implementace clone třídy Object: pokud nějaká třída implementuje Cloneable, metoda clone třídy Object vrací kopii daného objektu podle jednotlivých atributů; jinak vyvolává výjimku CloneNotSupportedException. To je velmi netypické využívání rozhraní, které byste si neměli brát za vzor. Implementování nějakého rozhraní obvykle říká něco o tom, co může daná třída vykonat pro své klienty. V případě Cloneable se však modifikuje chování chráněné metody v nadtřídě.

Aby mělo implementování rozhraní Cloneable v určité třídě nějaký efekt, musí ono samotné i všechny jeho nadtřídy napl�ovat poměrně složitý, nevynutitelný a z velké části také nedokumentovaný protokol. Výsledný mechanismus je mimojazykový: vytváří nějaký objekt bez volání konstruktoru.

Obecný kontrakt metody clone je slabý. Zde jej máme zkopírovaný ze specifikace java.lang.Object:

S tímto konstruktorem souvisí několik problémů. Předpoklad, že "se nevoaljí žádné konstruktory", je příliš silný. Dobře vychovaná metoda clone může volat konstruktory při vytváření objektů interních pro vytvořeného klona. Je-li třída finální, může clone dokonce vrátit objekt vytvořený nějakým konstruktorem.

Předpoklad, že vztah x.clone().getClass() by měl být obecně identický se vztahem x.getClass(), je však příliš slabý. V praxi programátoři předpokládají, že když rozšíří nějakou třídu a zavolají super.clone z takové podtřídy, vrácený objekt bude instancí dané nadtřídy. Jediným způsobem, jak může nadtřída zajistit tuto funkčnost, je vrátit objekt získaný voláním super.clone. Pokud metoda clone vrací objekt vytvořený konstruktorem, bude mít nesprávnou třídu. Když tedy překryjete metodu clone v nefinální třídě, měli byste vracet objekt získaný voláním super.clone zavolá metodu clone třídy Object, čímž se vytvoří instanci správné třídy. Tento mechanismus se vzdáleně podobá automatickému řetězení konstruktorů, jenom není vynucovaný.

Rozhraní Cloneable alespo� ve verzi 1.3 nestanovuje zodpovědnost, kterou na sebe přebírá, když toto rozhraní implementuje. Specifikace neříká nic jiného, než jen jakým způsobem ovliv�uje implementace tohoto rozhraní chování implementace clone třídy Object. V praxi se od třídy implementující Cloneable očekává, že poskytne řádně fungující veřejnou metodu clone. To však obecně není možné, pokud se správně reagující implementaci clone, ať už veřejnou nebo chráněnou, neposkytují všechny nadtřídy dané třídy.

Předpokládejme, že chcete implementovat Cloneable v nějaké třídě, jejíž nadtřídy poskytují dobré metody clone. Objekt získaný pomocí super.clone() se může a nemusí přibližovat tomu, co nakonec vrátíte, což závisí na podstatě dané třídy. Tento objekt bude z hlediska každé nadtřídy plně funkčním klonem originálního objektu. Atributy deklarované ve vaší třídě (pokud nějaké existují) budou mít hodnoty identické s hodnotami klonovaného objektu. Pokud každý atribut obsahuje nějakou primitivní hodnotu nebo odkaz na neměnitelný objekt, pak může být vrácený objekt přesně tím, co potřebujete. Pak není zapotřebí další zpracování. To je příklad třeba třídy PhoneNumber z rady 8. Zde stačí jen poskytnout veřejný přístup k chráněné metodě clone třídy Object:

	public Object clone() {
		try {
			return super.clone();
		} catch (CloneNotSupportedException e) {
			throw new Error("Chyba"); // Nemůže nastat
		}
	}

Pokud však váš objekt obsahuje atributy odkazující se na měnitelné objekty, může být použití takové metody clone katastrofální. Zvažme například třídu Stack z rady 5:

public class Stack {
	private Object[] elements;
	private int size = 0;

	public Stack(int initialCapacity) {
		this.elements = new Object[initialCapacity];
	}

	public void push(Object e) {
		ensureCapacity();
		elements[size++] = e;
	}

	public Object pop() {
		if (size == 0)
			throw new EmptyStackException();
		Object result = elements[--size];
		elements[size] = null; // Odstranění zastaralých odkazů
	}

	// Zajištění prostoru alespo� pro jeden další prvek
	private void ensureCapacity() {
		if (elements.length == size) {
			object oldElements[] = elements;
			elements = new Object[2 * elements.length + 1];
			System.arrayCopy(oldElements, 0, elements, 0, size);
		}
	}
}

Předpokládejme, že chcete učinit tuto třídu kolonovatelnou. Pokud její metoda clone vrátí prostě super.clone(), výsledná instance Stack bude mít správnou hodnotu ve svém atributu size, ale její atribut elements se bude odkazovat na toéž pole jako originální instance Stack. Úprava originálu zničí invarianty (neměnné podmínky) v klonu a naopak. Rychle zjistíte, že váš program poskytuje nesmyslné výsledky nebo vyvolává výjimku NullPointerException.

K této situaci by nikdy nemohlo dojít v důsledku volání jediného konstruktoru ve třídě Stack. Metoda clone vlastně funguje jakodalší konstruktor; musíte zajistit, že nijak nepoškodí originální objekt a že řádně vytvoří invarianty v klonu. Aby metoda clone třídy Stack řádně fungovala, musí zkopírovat interní záležitosti zásobníku. Toho lze nejsnáze dosáhnout rekurzivním voláním clone na atributu elements:

	public Object clone() throws CloneNotSupportedException {
		Stack result = (Stack) super.clone();
		result.elements = (Object[]) elements.clone();
		return result;
	}

Uvědomte si, že toto řešení nebude fungovat, pokud by byl atribut elements finální, protože metoda clone by nemohla přiřadit tomuto atributu novou hodnotu. To je zásadní problém: Architektura clone je nekomaptibilní s normálnímpoužíváním finálních atributů odkazujících se na měnitelné objekty s výjimkou případů, kdy mohou být dané měnitelné objekty bezpečně sdíleny objektem a jeho klonem. Aby se třída stala klonovatelnou, může být zapotřebí odstranit modifikátory final z některých atributů.

Ne vždy dostačuje volat metodu clonerekurzivně. Předpokládejem například, že píšete metodu clone prohešovací tabulku, jejíž vnitřní součásti se skládají z pole sektorů, z nich každý se odkazuje na první zadání v propojeném seznamu dvojic klíčů a hodnot, nebo je null, pokud je daný sektor prázdný. Z důvodu výkonnosti implementuje tato třída svůj vlastní jednoduchý propojený seznam a nepoužívá tedy interně java.util.LinkedList:

publi class HashTable implements Cloneable {
	private Entry[] buckets = ...;

	private static clas Entry {
		Object key;
		Object value;
		Entry next;

		Entry(Object key, Object value, Entry next) {
			this.key = key;
			this.value = value;
			this.next = next;
		}
	}

	. . .

}

Představme si, že budete pole sektorů klonovat rekurzivně, jako v případě třídy Stack:

	// Chyba - výsledekm je sdílený interní stav!
	public Object clone() throws CloneNotSupportedException {
		HashTable result =  (HashTable) super.clone();
		result.buckets = (Entry[]) buckets.clone();
		return result;
	}

Třebaže má metoda clone své vlastní pole sektorů, oskazuje se toto pole na tentýž propojený seznam jako originál, což může snadno způsobit neočekávané chování jak v klonu tak i v originálu. Chcete-li problém vyřešit, musíte po jednom zkopírpvat propojený seznam, který tvoří každý sektor. Zde je jeden obecný přístup:

public class HashTable implements Cloneable {
	private Entry[] buckets = ...;
	
	private static class Entry {
		Object key;
		Object value;
		Entry next;

		Entry(Object key, Object value, Entry next) {
			this.key = key;
			this.value = value;
			this.next = next;
		}

		
		// Rekurzivní kopírování propojeného seznamu začínajícího 
		// tímto zadáním
		Entry deepCopy() {
			return new Entry(key, value,
				next == null ? null : next.deepCopy());
		}
	}

	publi object clone() throws CloneNotSupportedException {
		HashTable result = (HashTable) super.clone();
		result.buckets = new Entry[buckets.length];
		for (int i = 0; i < buckets.length; i++) {
			if (buckets[i] != null)
				result.buckets[i] = (Entry)buckets[i].deepCopy();
		}
		return result;
	}

	. . .

}

Soukromá třída HashTable.Entry byla rozšířena o podporu určité metody "hloubkového kopírování". Metoda clone v HashTable alokuje nové pole buckets správné velikosti a iteruje originálním polem buckets, přičemž hloubkově kopíruje každý neprázdný sektor. Metoda hloubkového kopírování v Entry se sama rekurzivně volá a kopíruje tak celý propojený seznam uvozovaný danou položkou. Třebaže je tato technika líbivá a dobře funguje v případě, kdy nejsou sektory příliš dlouhé, nejdená se o dobrý způsob klonování propojeného seznamu, protože spotřebovává jeden rámec zásobníku pro každý prvek v seznamu. Je-li tento seznam dlouhý, může snadno dojít k přetečení zásobníku. Aby k tomu ndeošlo, můžet nahradit rekurzi v deepCopy iterací:

	// Iterační kopírování propojeného seznamu začínajícího tímto zadáním
	Entry deepCopy() {
		Entry result = new Entry(key, value, next);
		for (Entry p = result; p.next != null; p = p.next)
			p.next = new Entry(p.next.key, p.next.value, p.next.next);
		return result;
	}

Posledním přístupem ke klonování složitých objektů je zavolat super.clone, nastavit všechny atributy ve výsledném objektu na počáteční stav a pak volat metody vyšší úrovně a regenerovat stav objektu. V našem případě HashTable by se atribut buckets inicializoval na nové pole sektorů a pak by se pro každé přiřazení klíč - hodnota v klonované hešovací tabulce zavolala (neukázaná) metoda put(key, value). tento přístup obvykle nabízí jednoduchou a rozumně elegantní metodu clone, která však neběží tak rychle, jako jiná metoda přímo pracující s vnitřními součástmi objektu a jeho klonu.

Podobně jako konstruktor nesmí metoda clone volat žádné nefinální metody na vytvářeném klonu (rada 15). Zavolá-li clone překrytou metodu, vykoná se tato metoda ještě předtím, než mohla podtřída, v níž je definovaná, zafixovat svůj stav v klonu, což může snadno vést k poškození klonu a originálu. Proto musí být metoda put(key, value), popsaná v předchozím odstavci, buď finální nebo soukromá (je-li soukromá, pak se jedná pravděpodobně o "pomocnou metodu" nějaké nefinální veřejné metody).

Metoda clone třídy Object je vytvořena tak, aby vyvolávala výjimku CloneNotSupportedException, překrývání metody clone však může tut deklaraci vynechat. Metody clone finálních tříd by měly tuto deklaraci vynechat, protože s metodami, které nevyovlávají kontrolované výjimky, se pracuje lépe, než s metodami, které to činí (rada 41). Pokud nějaká rozšiřitelná třída, zejména určená k dědění (rada 15), překryje metodu clone, pak by měla překrývající metoda clone zahrnovat deklaraci vyvolání výjimky CloneNotSupportedException. To umožní podtřídám pohodlně zrušit klonovatelnost poskytnutím následující metody clone:

	// metoda clone zaručující, že instance nelze klonovat
	public final Object clone() throws CloneNotSupportedException {
		throw new CloneNotSupportedException();
	}

Není důležité přesně se řídit výše uvedenou radou, protože metoda clone podtřídy, která se nemá klonovat, může vždy vyvolat nekontrolovatelnou výjimku, například UsupportedOperationException, pokud není v metodě clone, kterou překrývá, deklarované vyvolání CloneNotSupportedException. Běžně se však vyžaduje, aby se v takové situaci správně projevila výjimka CloneNotSupportedException.

Abychom si vše zrekapitulovali: všechny třídy implementující Cloneable by měly překrývat metodu clone nějakou veřejnou metodou. Tato veřejná metoda musí nejprve volat super.clone a pak opravit všechny atributy, které to vyžadují. To obvykle znamená kopírování všch měnitelných objektů tvořících vnitřní "hloubkovou strukturu" klonovaného objektu a nahrazení odkazů na takové obejkty odkazy na jejich kopie. Třebaže lze tyto interní kopie obecně vytvářet rekurzivním voláním clone, nejedná se vždy o ten nejlepší přístup. Pokud daná třída obsahuje pouze primitivní atributy nebo odakzy na neměnitelné objekty, pak pravděpodobně nebude zapotřebí upravovat žádné atributy. Existují však výjimky z tohoto pravidla. například atribut, představující sériové číslo nebo jiný jednoznačný identifikátor, či atribut představující čas vytvoření objektu bude zapotřebí opravit, i když jsou primitivní nebo neměnné.

Je všechna tato složitost opravdu nezbytná? Jen výjimečně. Pokud rozšíříte třídu implementující Cloneable, pak nemůžete prakticky udělat nic jiného, než jen implementovat vychovanou metodu clone. V jiném případě bude asi lepší poskytnout nějaké alternativní prostředky pro kopírování objektu nebo tuto možnost vůbec nepodporovat. Podpora kopírování objektů nemá například velký význam u neměnitelných tříd, protože kopie budou prakticky nedolišitelné od originálu.

Vhodným přístupem ke kopírování objektů je poskytnout kopírovací konstruktor. Kopírovací konstruktor je prostě konstruktor, který přebírá jediný argument, jehož typem je třída obsahující daný konstruktor, například:

	public Yum(Yum yum)

Menší variantou je poskytnout statickou továrnu namísto konstruktoru:

	public static Yum newInstance(Yum yum)

Přístup využívající kopírovací konstruktor a jeho variant se statickou tovární metodou mají oproti Cloneable / clone mnoho výhod: nespoléhají se na riskantní mechanismus mimojazykového vytváření objektů; nevyžadují nevynutitelné plnění špatně zdokumentovaných konvencí; nekolidují se správným používáním finálních atributů; nevyžadují po klientovi zachytávání zbytečných kontrolovaných výjimek; poskytují klientovi statický typový objekt. Třebaže není možné umístit kopírovací konstruktor nebo statickou tovární metodu v rozhraní, Cloneable také nefunguje jako rozhraní, protože mu chybí veřejná metoda clone. Proto se nevzdávejte funkčnosti, když použijete kopírovací konstruktor namísto metody clone.

Navíc může kopírovací konstruktor (nebo statická tovární metoda) přebírat argument, jehož typem je příslušné rozhraní implementované danou třídou. Například všechny implementace obecných kolekcí konvenčně posyktují kopírovací konstruktor, jehož argument je typu Collection nebo Map. Kopírovací konstruktory, využívající rozhraní, umož�ují klientovi vybrat si implementaci kopírování a nenutí jej příijímat implementaci originálu. Předpokládejme například, že máte LinkedList l, který chete zkopírovat jako ArrayList. Metoda clone takovou funkčnost nenabízí, vše je však jednoduché s kopírovacím konstruktorem: new ArrayList(l).

Po vysvětlení všeh problémů s Cloneable můžeme klidně říci, že by jej neměla rozšiřovat další rozhraní a že by jej neměly implementovat třídy určené k dědění (rada 15). Kvůli mnoha nedostatkům někteří zkušení programátoři prostě metodu clone nikdy nepřekrývají ani nevolají, snad jen s výjimkou laciného kopírování polí. Pamatujte si, že pokud neposkytnete přinejmenším vychovanou chráněnou metodu clone ve třídě určené pro dědění, nebudou moci podtřídy implementovat Cloneable.