JE_21: Nahrazujte konstrukce enum třídami

Konstrukce enum jazyka C byla z programovacího jazyka Java vypuštěna. Tato konstrukce normálně definuje nějaký výčtový typ: typ, jehož platné hodnoty se skládají z pevné množiny konstant. Bohužel však ani konstrukce enum příliš dobře výčtové typy nedefinuje. Prostě jen definuje množinu pojmenovaných celočíselných konstant, vůbec se nestará o zabezpečení typů a jen malé starosti si dělá s pohodlností použití. Nejenže je v jazyce C možné toto:

typedef enum {FUJI, PIPPIN, GRANNY_SMITH} apple_t;
typedef enum {NAVEL, TEMPLE, BLOOD} orange_t;
orange_t myFavorite = PIPPIN; /* Míchání jablek a pomerančů */

ale tohle už je příšernost:

orange_t x = (FUJI - PIPPIN) / TEMPLE; /* Jablečná šťáva */

Konstrukce enum nevytváří pro generované konstanty obor názvů. Proto následující deklarace, která opakovaně používá jeden z názvů, koliduje s deklarací orange_t:

typedef enum {BLOOD, SWEAT, TEARS} fluid_t;

Typy definované konstrukcí enum jsou křehoučké. Přidání konstant do takového typu bez opakované kompilace jeho klientů způsobuje nepředvídatelné chování, pokud není zajištěna ochrana všech již existujících konstantních hodnot. Více součástí nemůže přidávat konstanty do takového typu nezávisle, protože jejich nové výčtové konstanty budou pravděpodobně kolidovat. Konstrukce enum neumož�uje žádným snadným způsobem překládat výčtové konstanty na tisknutelné řetězce nebo procházet konstantami v nějakém typu.

Bohužel nejčastěji používaný vzor vytváření výčtových typů v programovacím jazyce Java, který je uvedený dále, sdílí nedostatky konstrukce enum jazyka C:

// Vzor int konstrukce výčtu - problematický!!
public class PlayingCard {
	public static final int SUIT_CLUBS = 0;
	public static final int SUIT_DIAMONDS = 1;
	public static final int SUIT_HEARTS = 2;
	public static final int SUIT_SPADES = 3;
	. . .
}

Můžete se také setkat s variantou tohoto vzoru, kde jsou namísto konstant int používány konstanty String. Tuto variantu byste neměli nikdy používat. Třebaže jako své konstanty poskytuje tisknutelné řetězce, může to vést k problémům s výkonností, protože se spoléhá na porovnávání řetězců. Navíc to může vést k tomu, že naivní uživatelé napevno zakódují řetězcové konstanty do svého klientského kódu a nebudou používat příslušné názvy atributů. Bude-li taková napevno zadaná řetězcová konstanta obsahovat typografickou chybu, tato chyba unikne detekci během kompilování a výsledkem budou chyby za běhu.

Naštěstí však programovací jazyk Java nabízí alternativu, která řeší nedostatky obvyklých vzorů int a String a poskytuje řadu dodatečných výhod. Označuje se za vzor typově zabezpečeného výčtu. Bohužel však ještě není příliš známá. Základní myšlenka je prostá: Definovat třídu představující jediný element daného výčtového typu a neposkytovat žádné veřejné konstruktory. Poskytnout pouze veřejné finální atributy, jeden pro každou konstantu ve výčtovém typu. Zde je patrné, jak tento vzor vypadá v nejjednodušší formě:

// Vzor typově zabezpečeného výčtu
public class Suit {
	private final String name;

	private Suit(String name) {
		this.name = name;
	}

	public String toString() {
		return name;
	}

	public static final Suit CLUBS = new Suit("clubs");
	public static final Suit DIAMONDS = new Suit("diamonds");
	public static final Suit HEARTS = new Suit("hearts");
	public static final Suit SPADES = new Suit("spades");
}

Protože klienti nemají možnost vytvářet objekty této třídy nebo ji rozšiřovat, pak nebudou nikdy existovat objekty tohoto typu kromě těch exportovaných prostřednictvím veřejných statických finálních atributů. Třebaže není třída definovaná jako finální, nelze ji žádným způsobem rozšířit: Konstruktory podtříd musejí volat konstruktor nadtřídy a žádný takový konstruktor tu není přístupný.

Jak už název naznačuje, vzor typově zabezpečeného výčtu zajišťuje typové zabezpečení v době kompilace. Deklarujete-li nějakou metodu s paramterem typu Suit, pak máte zaručeno, že jakýkoli předaný nenullový odkaz na objekt představuje jednu ze čtyř platných barev karet. Jakýkoli pokus o předání objektu nesprávného typu bude zachycen během kompilace, stejně jako každý pokus o přiřazení výrazu jednoho výčtového typu proměnné jiného typu. Více typově zabezpečených tříd se shodně nazvanými výčtovými konstantami spolu žije v mítu, protože každá třída má svůj vlastní obor názvů.

Konstanty lze přidávat do třídy typově zabezpečeného výčtu, aniž by bylo nutné znovu zkompilovat její klienty, protože veřejné statické atributy odkazů na objekty obsahující výčtové konstanty zajišťují izolační vrstvu mezi klientem a výčtovou třídou. Konstanty samotné se do klientů nikde nezkompilují, jak se to stane v obvyklejším vzoru int a jeho variantě s typem String.

Protože jsou typově zabezpečené výčty úplnými třídami, můžete překrýt metodu String, jak bylo uvedeno již dříve, a umožnit tak překlad hodnot na tisknutelné řetězce. Chcete-li, můžete učinit ještě další krok a internacionalizovat typově zebezpečené výčty standardními prostředky. Názvy řetězců jsou používány pouze metodou toString; nepoužívají se v porovnáních rovnosti, jelikož implementace metody equals zděděná ze třídy Object vykonává porovnání shody odkazu.

Obecně můžete rozšířit třídu typově zabezpečeného výčtu libovolnou metodou, která se vám zdá vhodná. Například naše třída Suit by mohla težit z přidání metody vracející skutečnou barvu (červenou nebo černou) daného znaku na kartách nebo metody vracející obrázek představující daný znak. Třída může začít existovat jako jednoduchý typově zabezpečený výčet a časem se rozvinout v plně funkční abstrakci.

Protože lze do tříd typově zabezpečených výčtů přidávat libovolné metody, mohou takové třídy implementovat jakékoli rozhraní. Předpokládejme například, že chcete, aby třída Suit implementovala Comparable, aby mohli klienti při bridži řadit obdržené karty podle barev. Zde máme mírně upravený původní vzor, který to zajišťuje. K přiřazení ordinálního čísla každé instanci při jejím vytvoření se používá statická proměnná nextOrdinal. Tato pořadová čísla používá metoda compareTo k seřazení instancí:

// Ordinální výčet se zabezpečenými typy
public class Suit implements Comparable {
	private final String name;
	
	// Pořadí další vytvářené barvy
	private static int nextOrdinal = 0;

	// Přiřazení pořeadí této barvě
	private final int ordinal = nextOrdinal++;

	private Suit(String name) {
		this.name = name;
	}

	public String toString() {
		return name;
	}

	public int compareTo(Object o) {
		return ordinal - ((Suit)o).ordinal;
	}

	public static final Suit CLUBS = new Suit("clubs");
	public static final Suit DIAMONDS = new Suit("diamonds");
	public static final Suit HEARTS = new Suit("hearts");
	public static final Suit SPADES = new Suit("spades");
}

Protože konstanty typově zebezpečeného výčtu jsou objekty, můžete je vkládat do kolekcí. Představme si například, že chceme, aby třída Suit exportovala neměnitelný seznam barev karet ve standardním pořadí. Do třídy stačí přidat jen dvě následující deklarace atributů:

public static final Suit[] PRIVATE_VALUE =
	{CLUBS, DIAMONDS, HEARTS, SPADES};
public static final List VALUES = 
	Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

Narozdíl od nejjednpdušší formy vzoru typově zabezpečeného výčtu lze učinit třídy výše uvedené ordinální formy serializovatelnými (kapitola 10) jen s minimem námahy. Stačí do deklarace třídy přidat implements Serializable. Musíte rovně ž poskytnout metodu readResolve (rada 57):

private Object readResolve() throws ObjectStreamException {
	return PRIVATE_VALUES[ordinal]; // Přizpůsobit předpisům

Tato metoda, která je volána automaticky serializačním systémem, zabra�uje vzniku duplicitních konstant vznikajících jako výsledek deserializace. Tímje naplněna záruka, že každou výčtovou konstantu představuje jen jediný objekt - není tedy zapotřebí překrývat Object.equals. Bez této záruky by Object.equals hlásila chybně nesoulad, kdyby byla vystavena dvěma rovným, ale odlišným výčtovým konstantám. Všimněte si, že se metoda readResolve odkazuje na pole PRIVATE_VALUES, takže toto pole musíte deklarovat, i když se rozhodnete neexportovat VALUES. Všimněte si také, že metoda readResolve nepužívá atribut name, takže by mohl a měl být tranzientní (přechodný).

Výsledná třída je trochu křehčí; konstruktory jakýchkoli nových hodnot se musejí objevit za konstruktory již existujících hodnot, aby se po deserializaci dříve serializovaných instancí nezměnila jejich hodnota. Je tomu tak proto, že serializovaná forma (rada 55) nějaké výčtové konstanty je tvořena výhradně jejím pořadím. Pokud se změní výčtová konstanta vztažená k nějakému pořadí, pak serialzovaná konstanta sdaným pořadím převezme při své deserializaci tuto novou hodnotu.

Může existovat určité chování, přiřazené každé konstantě, které se používá pouze v rámci balíčku obsahujícího danou výčtovou třídu se zabezpečenými typy. Taková chování se nejlépe implementují jako metody dané přátelské třídy. Každá výčtová konstanta pak s sebou nese skrytou kolekci chování, jež umož�ují balíčku obsahujícímu daný výčtový typ reagovat příslušným způsobem, když je mu tato konstanta představena.

Má-li typově zabezpečená výčtová třída metody, jejichž chování se výrazně liší mezi jednotlivými konstantami třídy, pak byste pro každou konstantu měli požít samostatnou soukromou třídu nebo anonymní vnitřní třídu. Každá konstanta pak bude moci mít svou vlastní implementaci každé takové metody a automaticky volat tu správnou implementaci. Alternativou je strukturovat každou takovou metodu jako větev s více cestami, která se chová odlišně podle toho, s jakou konstantou je volána. Tato alternativa je ale ošklivá, náchylná k chybám a bude pravděpodobně nabízet výkonnost nižší, než poskytuje automatické rozdělování metod virtuálního počítače.

Obě techniky popsané v předchozích odstavcích jsou ilustrovány v typově zabezpečené výčtové třídě uvedené dále. Tato třída, nazývaná Operation, představuje nějakou operaci vykonávanou kalkulátorem se čtyřmi základními funkcemi. Mimo balíček, v němž je tato třída definovaná , můžete s konstantami Operation volat jen metody třídy Object (toString, hashCOde, equals atd.). V balíčku však můžete vykonávat aritmetické operace představované danými konstantami, Lze předpokládat, že balíček bude exportovat nějaký objekt kalkulátoru vyšší úrovně, který bude exportovat jednu nebo více metod přebírajících jako parametr konstantu Operation.

Všimněte si, že Operation samotná je abstraktní třída obsahující jen jednu abstraktní přátelskou metodu nazvanou eval, jež vykonává příslušnou aritmetickou operaci. Pro každou konstantu je definována anonymní vnitřní třída, takže si každá konstanta může definovat svou vlastní verzi metody eval:

// Typově zebezpečený výčet s chováními přiřazenými konstantám
public abstract class Operation {
	private final String name;

	Operation(String name) {
		this.name = name;
	}

	public String toString() {
		return this.name;
	}

	// Vykonat aritmetickou operaci představovano touto konstantou
	abstract double eval(double x, double y);

	public static final Operation PLUS = new Operation("+") {
		double eval(double x, double y) {
			return x + y;
	};

	public static final Operation MINUS = new Operation("-") {
		double eval(double x, double y) {
			return x - y;
	};

	public static final Operation TIMES = new Operation("*") {
		double eval(double x, double y) {
			return x * y;
	};

	public static final Operation DIVIDED_BY = new Operation("/") {
		double eval(double x, double y) {
			return x / y;
	};
}

Obecně lze výkonnost typově zebezpečených výčtů srovnat s výkonností výčtových konstant int. Dvě samostatné instance typově zabezpečené třídy nemoho nikdy představovat stejnou hodnotu, takže se ke zjišťování logické rovnosti používají porovnání identity odkazu, která jsou rychlá. KLienti typově zabezpečené třídy mohou používat operátor == namísto metody equals; je zaručené, že výsledky budou stejné a operátor == může být dokonce rychlejší.

Je-li nějaká typově zabezpečená výčtová třída obecně užitečná, pak by se mělo jednat o třídu nejvyšší úrovně; pokud je její použití svázáno s určitou třídou nejvyšší úrovně, pak by měla být statickou člensko třídou dané třídy nejvyšší úrovně (rada 18). Například třída java.math.BigDecimal obsahuje kolekci výčtových konstant int představujících režimy zaokrouhlování zlomkových částí. Tyto režimy zaokrouhlování jsou užitečnou abstrakcí, která se v zásadě omezuje jen na třídu BigDecimal; lepší by bylo implementovat je jako samostatně stojící třídu java.math.RoundingMode. Každý programátor pracující s režimy zaokrouhlování by pak měl snahu opakovaně používat tyo režimy zaokrouhlování, což by vedlo ke zvýšení konzistentnosti různých rozhraní API.

Základní vzor typově zabezpečeného výčtu, který byl ukázán v příkladu obou výše uvedených implementací Suit, je pevný; Uživatelé nemohou přidávat do tohoto výčtového typu nové prvky, protože jeho třída nemá žádné konstruktory přístupné uživatelům. Třída je tak vlastně finální, ať už je nebo není deklarovaná s modifikátorem přístupu final. Právě toto obvykle požadujete, občas však budete potřebovat učinit nějakou typově zabezpečenou třídu rozšiřitelnou. To může například nastat, pokud použijete typově zabezpečený výčet k reprezentování formátů kódování obrázků a přitom chcete umožnit dalším uživatelům přidávat podporu nových formátů.

Má-li být typově zabezpečený výčet rozšiřitelný, pak mu prostě poskytněte chráněný konstruktor. Ostatní pak mohou danou třídu rozšířovat a přidávat nové konstanty do svých podtříd. Nemusíte se starat o konflikty výčtových konstant jako v případě výčtového vzoru int. Rozšiřitelná varianta vzoru typově zabezpečeného výčtu využívá oboru názvů balíčku k vytvoření "magicky spravovaného" oboru názvů pro rozšiřitelný výčet. Několik organizací může rozšířit daný výčet, aniž by o sobě nadále věděly, a jejich rozšíření nebudou nikdy kolidovat.

Pouhé přidání nového prvku do rozšiřitelného výčtového typu nezajišťuje, že je tento nový prvek plně podporován: Metody přebárající nějaký prvek daného výčtového typu musejí vědět, že jim může být předán prvek neznámý programátorovi. Vícenásobné větve jsou problematické u pevných výčtových typů; u rozšiřitelných výčtových typů jsou smrtelné, protože magicky nerozšíří určitou větev, kdykoli progamátor rozšíří typ.

Jednou z možností řešení tohoto problému je vybavit typově zabezpečenou výčtovou třídu všemi metodami potřebnými k popisu chování nějaké konstanty této třídy. Metody, které nejsou užitečné pro klienty dané třídy, by měly být deklarovaány jako protected aby byly skryty před klienty, ale aby je zárove� mohly podtřídy překrývat. Nemá-li nějaká metoda žádnou rozumnou výchozí implementaci, měla by být deklarována jako abstract a zárove� jako protected.

Je vhodné, aby rozšiřitelné typově zabezpečené výčtové třídy překrývaly metody equals a hashCode nějakými finálními metodami, které volají metody třídy Object. Tím je zajištěno, že žádná podtřáda náhodou nepřekryje tyto metody. Tak je naplněna záruka, že všechny rovné objekty výčtového typu jsou zárove� identické (a.equals(b) právě tehdy, když a == b):

	// Metody zabra�ující překrytí
	public final boolean equals(Object that) {
		return super.equals(that);
	}

	public final int hashCode() {
		return super.hashCode();
	}

Všimněte si, že rozšiřitelná varianta není kompatibilní s porovnatelnou variantou; pokusíte-li se je zkombinovat, pak bude pořadí prvků v podtřídách funkcí pořadí inicializování jednotlivých podtříd. To se může měnit od programu k programu a od spuštění ke spuštění.

Rozšiřitelná varianta vzoru typově zabezpečeného výčtu je kompatibilní se serializovanou variantou, ale kombinování těchto variant vyžaduje pozornost. Každá podtřída musí přiřazovat svá vlastní pořadová čísla a poskytovat svou vlastní metodu readResolve. V zásadě je každá třída zodpovědná za serialzování a deserializování svých vlasttních instancí. Aby bylo vše konkrétnější, dále je uveden příklad verze třídy Operation upravené tak, abz byla jak rozšiřitelná, tak i serializovatelná:

// Serializovatelný, rozšiřitelný, typově zabezpečený výčet
public abstract class Operation implements Serializable {
	private final transient String name;

	protected Operation(String name) {
		this.name = name;
	}

	public static Operation PLUS = new Operation("+" {
		protected double eval(double x, double y) {
			return x + y;
		}
	};

	public static Operation MINUS = new Operation("-" {
		protected double eval(double x, double y) {
			return x - y;
		}
	};

	public static Operation TIMES = new Operation("*" {
		protected double eval(double x, double y) {
			return x * y;
		}
	};

	public static Operation DIVIDE = new Operation("/" {
		protected double eval(double x, double y) {
			return x / y;
		}
	};

	// Vykonat aritmetickou operaci představovanou touto konstantou
	protected abstract double eval(double x, double y);

	public String toString() {
		this.name;
	}

	// Zabránit podtřídám v překrytí Object.equals
	public final boolean equals(Object that) {
		return super.equals(that);
	}

	public final int hashCode() {
		return super.hashCode();
	}

	// Čtyři následující deklarace jsou zapotřebí pro serializaci
	private static int nextOrdinal = 0;
	private final int ordinal = nextOrdinal++;
	private static final int Operation[] VALUES = {PLUS, MINUS, TIMES, DIVIDE};
	
	Object readResolve() throws ObjectStreamException {
		return VALUES[ordinal]; // Podle předpisů
	}
}

Zde máme podtřídu třídy Operation, která přidává operace logaritmu a mocniny. Tato podtřída může existovat mimo balíček obsahující revidovanou třídu Operation. Může být veřejná a může být sama rozšiřitelná. Více nezávisle napsaných podtříd může žít v míru:

// Podtřída rozšiřitelného, serializovatelného,
// typově zabezpečeného výčtu
abstract class ExtendedOperations extends Operation {
	
	ExtendedOperation(String name) {
		super(name);
	}

	public static Operation LOG = new ExtendedOperation("log"); {
		protected double eval(double x, double y) {
			return Math.log(y) / Math.log(x);
		}
	};

	public static Operation EXP = new ExtendedOperation("exp"); {
		protected double eval(double x, double y) {
			return Math.pow(x, y);
		}
	};

	// Čtyři následující deklarace jsou zapotřebí pro serializaci
	private static int nextOrdinal = 0;
	private final int ordinal = nextOrdinal++;
	private static final int Operation[] VALUES = {LOG, EXP};
	
	Object readResolve() throws ObjectStreamException {
		return VALUES[ordinal]; // Podle předpisů
	}
}

Všimněte si, že metody readResolve v právě ukázaných třídách jsou přátelské a nikoli soukromé. To je nezbytné, protože instance Operation a ExtendedOperation jsou ve skutečnosti instancemi anonymních podtříd, takže soukromé metody readResolve by neměly mít žádný vliv (rada 57).

V porovnání se vzorem int má vzor typově zabezpečeného výčtu několik nevýhod. Asi nejzávažnější nevýhodou je skutečnost, že je komplikovanější seskupovat konstanty typově zabezpečených výčtů do sad. V případě výčtů int se to tradičně řeší volbou hodnot výčtových konstant, kdy je každá zvláštním kladným násobkem dvou a sada se reprezentuje jako bitová operace OR příslušných konstant:

// Varianta výčtového vzoru int s bitovými příznaky
public static final int SUIT_CLUBS = 1;
public static final int SUIT_DIAMONDS = 2;
public static final int SUIT_HEARTS = 4;
public static final int SUIT_SPADES = 8;

public static final int SUIT_BLACK = SUIT_CLUBS | SUIT_SPADES;

Reprezentování sad konstant výčtového typu tímto zoůsobem je stručné a extrémně rychlé. V případě sad konstant typově zabezpečených výčtů můžete používat nějakou obecnou implementaci z rámce kolekcí, to však není ani tak stručné, ani tak rychlé:

	Set blackSuits = new HashSet();
	blackSuits.add(Suit.CLUBS);
	blackSuits.add(Suit.SPADES);

Třebaže sady konstant typově zebezpečených výčtů nelze pravděpodobně učinit tak stručnými a rychlými jako sady výčtů int, je možné omezit tento nesoulad poskytnutím implementace Set se speciálním účelem, která přijímá pouze prvky jednoho typu a interně sadu představuje jako bitový vektor. Taková sada se nejlépe implementuje v témže balíčku jako je jeho typ prvku, aby byl možný přístup prostřednictvím atributu nebo přátelské metody k bitové hodnotě interně přiřazené každé konstantě typově zabezpečeného výčtu. Je rozumné poskytnout veřejné konstruktory, které přebírají jako parametry krátké sekvence prvků, takže jsou možné podobné idiomy:

	hand.discard(new SuitSet(Suit.CLUBS, Suit.SPADES));

Menší nevýhodou typově zebezpečneých výčtů v porovnání s výčty int je to, že typově zabezpečené výčty nelze používat v příkazech switch, protože nejsou integrálními konstantami. Místo toho můžete použít následující příkaz if:

	if (suit == Suit.CLUBS) {
		. . .
	} else if (suit == Suit.DIAMONDS) {
		. . .
	} else if (suit == Suit.HEARTS) {
		. . .
	} else if (suit == Suit.SPADES) {
		. . .
	} else {
		throw new NullPointerException("Prázdná barva"); // suit == null
	}

Příkaz if se nemusí vykonávat až tak dobře jako příkaz switch, ale rozdíl nebude pravděpodobně významný. Navíc jsou vícecestné větve ve spojení s konstantami typově zebazpečených výčtů zapotřebí jen výjimečně, protože s nimi lze používat automatické rozdělování metod JVM, jak tomu bylo v našem příkladu Operation.

Další malou výkonnostní nevýhodou typově zabezpečených výčtů je to, že nahrání tříd výčtových typů a zkonstruování konstantních objektů stojí prostor a čas. S výjimkou zařízení s omezenými prostředky, jako jsou mobilní telefony nebo topinkovače, si podobných problémů v praxi asi nepovšimnete.

Celkově lze říci, že výhody typově zebezpečených výčtů oproti výčtům int jsou velké a žádná z nevýhod se nezdá být tak závažnou, pokud se tedy nebude daný výčtový typ používat především jako prvek sady nebo v prostředí se značně omezenými prostředky. Když tedy okolnosti vyžadují zavedení výčtového typu, pak by vám měl na mysli vytanout vyor s typově yabeypečeným výčtem. API používající typově zabezpečené výčty jsou k programátorům mnohem příjemnější než výčty int. Jediným důvodem, proč se typově zebezpečené výčty nepoužívají častěji v rozhraních API platformy Java je skutečnost, že vzor typově zabezpečených výčtů ještě nebyl znám v době, kdy bylo mnoho z těchto API vytvořeno. Konečeně stojí za zopakování, že je potřeba používat výčtové typy jakéhokoli druhu by měla být relativně zřídkavá, protože hlavní využívání těchto typů již bylo překonáno možností vytvářet podtřídy (rada 20).