Jednou z věcí, kterou kritici minulým verzím Javy vyčítali, byla absence výčtových datových typů. Řada programátorů obcházela tento nedostatek tak, že místo požadovaného výčtového typu definovala rozhraní a v něm sadu (většinou celočíselných) konstant zastupujících hodnoty definovaného výčtového typu. Chtěl li někdo tyto konstanty používat, stačilo deklarovat implementaci příslušného rozhraní, a od té chvíle mohla třídy dané hodnoty používat jako by byly její vlastní.

Tento přístup měl několik nevýhod.

  • Třída se hlásila k implementaci rozhraní, aniž něco doopravdy implementovala.
  • Do rozhraní třídy se dostaly informace o typech a hodnotách, které třída a její instance používaly pouze interně
  • Tyto konstanty byly většinou definovány jako celočíselné, čímž byla znemožněna jejich typová kontrola.

Výše uváděné nevýhody popsal Joshua Bloch v knize Effective Java a současně zde popsal, jak by podle něj bylo třeba výčtové typy ve stávající syntaxi definovat. Tato doporučení se stala základem definice výčtových typů ve verzi 5.0.

Nejjednodušší definice

V definici výčtových typů je klíčové slovo class nahrazeno klíčovým slovem enum. Překladač vytvoří třídu, která je potomkem třídy java.lang.enum, a definuje v ní skrytý kód, který později v některých konstrukcích využívá.

Seznam jednotlivých hodnot daného typu je je možno definovat jejich prostým výčtem, např.:

public enum Období { JARO, LÉTO, PODZIM, ZIMA }

nebo výčtem obsahujícím i parametry předávané konstruktoru. Ten použijeme tehdy, je-li konstrukce jednotlivých hodnot složitější a vyžaduje-li explicitní předání parametru použitému konstruktoru, vloží se za název deklarované proměnné seznam parametrů v kulatých závorkách. Pak je ale třeba zabezpečit definici potřebného konstruktoru. Vše si posléze ukážeme.

Zůstaňme pro začátek u této nejjednodušší definice. Použijete-li pouhý seznam hodnot, nemusíte jej dokonce ani ukončovat středníkem. Doporučuji však tuto možnost ignorovat, protože jakmile do těla třídy cokoliv přidáte, budete muset přidat i středník ukončující seznam hodnot.

Definice vypadá velice jednoduše, ale podíváte-li se ne výsledný class-soubor zpětným překladačem (decompilerem), zjistíte, že přeložený soubor zase tak jednoduchý není:

public final class Období extends Enum {
public static final Období JARO;
public static final Období LETO;
public static final Období PODZIM;
public static final Období ZIMA;
private static final Období[] $VALUES;
static {
JARO = new Období("JARO", 0);
LETO = new Období("LETO", 1);
PODZIM = new Období("PODZIM", 2);
ZIMA = new Období("ZIMA", 3);
$VALUES = new Období[] {JARO, LÉTO, PODZIM, ZIMA};
}
public static final Období[] values() {
return (Období[])($VALUES.clone());
}
public static Období valueOf(String name) {
Období[] arr$ = $VALUES;
int len$ = arr$.length;
for(int i$ = 0; i$ < len$; i$++) {
Období období = arr$[i$];
if( období.name().equals(name) )
return období;
}
throw new IllegalArgumentException(name);
}
private Období(String s, int i) {
super(s, i);
}
}

Na tomto zdrojovém kódu si můžete všimnout několika věcí:

  • Překladač definoval třídu jako potomka třída Enum (přesněji java.lang.Enum).
  • Přestože jsme nedefinovali žádný konstruktor, překladač nedefinoval prázdný bezparametrický konstruktor, jak jsme zvyklí z klasických tříd, ale definoval místo něj soukromý dvouparametrický konstruktor, jehož prvním parametrem je název definované hodnoty a druhým její pořadí. Tělo tohoto konstruktoru obsahuje pouze vyvolání rodičovského konstruktoru se stejnými parametry.
  • Překladač definoval statický atribut $VALUES, jenž je vektorem (jednorozměrným polem) obsahujícím odkazy na definované hodnoty výčtového typu.
  • Překladač definoval metodu values(), která vrací kopii vektoru $VALUES.
  • Překladač definoval metodu valueOf(String), která vrací odkaz na instanci, jejíž název převzala jako parametr.

Atribut $VALUES

Jak jsem již řekl, překladač definuje vlastní atribut, který v prvním přiblížení nazve $VALUES. Tento atribut však definuje pouze pro sebe. Je totiž tak soukromý, že jej (na rozdíl od ostatních překladačem dodaných entit) není možno použít ani ve třídě, v níž byl deklarován (debugger vám jej však mezi statickými atributy ukáže).

Použití atributu je sice blokované, avšak jeho název blokován není - deklarujete-li vlastní atribut s tímto názvem, vymyslí překladač pro toto "supersoukromé" pole nějaký jiný.

Budete-li chtít využívat vektor odkazů na jednotlivé instance výčtového typu.

Třída Enum

Když už jsme si prozradili, co vše překladač do definice třídy přidal, měli bychom si také povědět, co třída převezme ze své mateřské třídy.

Než se ale rozhovořím o tom, co námi definovaný typ zdědí, chtěl bych se nejprve zmínit o samotné třídě Enum. Tato třída se totiž zařadila mezi zvláštní třídy, kam patří např. třídy Object nebo String. Její zvláštnost spočívá v tom, že programátor nemůže explicitně definovat potomka této třídy. Pokusíte-li se definovat např. třídu:

public class Výčet extends Enum {}

vyvoláte chybu překladu.

classes cannot directly extend java.lang.Enum.

Potomky třídy Enum mohou být pouze třídy definované explicitně jako výčtový typ, tj. třídy, v jejichž definici je místo class použito enum.

Rodičovský podobjekt třídy Enum uchovává u každé instance její název a pořadí, v němž byla definována. Obě tyto hodnoty lze kdykoliv získat zavoláním metod name(), resp. ordinal(). Vlastní atributy jsou však deklarovány jako soukromé, takže jsou pro potomky nepřístupné.

Kromě toho definuje třída Enum překryté verze metod equals(Object), hashCode() a clone(), přičemž poslední z nich pouze vyvolává výjimku CloneNotSupportedException.

Hlavička třídy Enum má tvar

public abstract class Enum <E extends Enum<E>> implements Comparable<E>, Serializable

Jak vidíte, již ve své hlavčce deklaruje třídu svého potomka, aby pak mohla definovat příslušné metody.

První z nich je metoda compareTo(E) deklarovaná v rozhraní Comparable<E>doprovázená skrytou metodou compareTo(Object), která pouze přetypuje svůj parametr a volá svoji jmenovkyni.

Další je pak metoda getDeclaringClass(), která vrátí class-objekt třídy, v níž je daná výčtová konstanta deklarována (tento objekt je typu Class<E>).

Na první pohled by se mohlo zdát, že tato metoda není potřeba, protože příslušný class-objekt přece vrátí metoda getClass(), ale není tomu tak. Jak uvidíme později, každá z instancí výčtového typu může být instancí nějakého podtypu své rodičovské třídy.

Všechny deklarované metody s výjimkou metody toString() definuje jako konečné, aby potomci nemohli jejich definici ovlivnit.

Pro úplnost bychom ještě měli zmínit také statickou metodu deklarovanou

public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name)

Tato metoda vrátí odkaz na zadanou konstantu zadaného výčtového typu.

Použití výčtových typů v programu

Konstanty výčtových typů je možné používat nejenom k uchovávání hodnot a k jejich případnému porovnávání v příkazech if nebo while, ale je je možné použít také v příkazu switch a cyklu for.

Přepínač

Nová verze Javy umožňuje používat konstanty výčtových typů i v přepínači. Nesmíme však zapomenout na to, že překladač nekontroluje, zda jsme vyčerpali všechny možnosti, a může se chybně domnívat, že existuje ještě nějaká cesta, kterou jsme nepokryli.

Pokud bychom např. v následující definici nadefinovali závěrečný příkaz return, upozornil by nás na to, že daná metoda musí vracet hodnotu.

public static String činnost( Období období ) {
switch( období ) {
case JARO: return "kvete";
case LÉTO: return "zraje";
case PODZIM: return "plodí";
case ZIMA: return "spí";
}
//Překladač neví, že jsem všechna období vyčerpal
return null;
}

Druhou možností, jak program upravit, aby byl překladač spokojen, je nahradit návěští poslední větve přepínače návěštím default. Takto definovaný program je pak sice kratší, ale na druhou stranu pro mnohé méně srozumitelný. Optimální by bylo upravit definici do následující podoby:

public static String činnost( Období období ) {
switch( období ) {
case JARO: return "kvete";
case LÉTO: return "zraje";
case PODZIM: return "plodí";
case ZIMA: return "spí";
default: throw new IllegalArgumentException(
"Neočekávaná hodnota parametru období=" + období );
}
}

Takováto definice je připravena i na případ, kdy bychom se náhodou rozhodli v budoucnu počet hodnot daného výčtového typu změnit.

Překladač řeší tyto konstrukce tak, že definuje pomocnou vnořenou třídu, jejímž statickým atributem je vektor mapující ordinální čísla použitých hodnot daného výčtového typu na návěští case. Část kódu obsahující předchozí metodu by se pak přeložila následovně:

static class _cls1 {
//Název je pouze pomocný - třída je ve skutečnosti anonymní
static final int $SwitchMap$Obdobi[];
static {
$SwitchMap$Obdobi = new int[Obdobi.values().length];
try {
$SwitchMap$Obdobi[Obdobi.JARO.ordinal()] = 1;
}catch (NoSuchFieldError ex) { }
try {
$SwitchMap$Obdobi[Obdobi.LETO.ordinal()] = 2;
}catch (NoSuchFieldError ex) { }
try {
$SwitchMap$Obdobi[Obdobi.PODZIM.ordinal()] = 3;
}catch (NoSuchFieldError ex) { }
try {
$SwitchMap$Obdobi[Obdobi.ZIMA.ordinal()] = 4;
}catch (NoSuchFieldError ex) { }
}
}
public static String cinnost(Obdobi obdobi) {
switch (_cls1.$SwitchMap$Obdobi[obdobi.ordinal()]) {
case 1: return "kvete";
case 2: return "zraje";
case 3: return "plodí";
case 4: return "spí";
}
throw new IllegalArgumentException(
"Neočekávaná hodnota parametru období=" + obdobi );
}

Cyklus

Použití v klasických cyklech je zřejmé. Ne každého ovšem napadne, že výčtové typy je možno použít i v nově zavedené verzi cyklu for. Pouze nesmíme zapomenout, že o vektor, přes který iterujeme, musíme nejprve příslušný výčtový typ požádat - např.:

public static String vyjmenuj() {
String s = "";
for( Období obdobi : Období.values() )
s += obdobi + " ";
return s;
}

Složitější definice výčtových typů

Na počátku článku jsem uváděl nejjednodušší možnou definici výčtového typu. Výčtové typy však mohou obsahovat i vlastní metody a definované konstanty mohou využívat konstruktory s dalšími parametry. Ukažme si tyto možnosti např. na příkladu třídy Směr8.

Všimněte si, jak jsou předávány parametry konstruktoru - za definovanou konstantu se pouze vloží závorky a za ně se vypíší hodnoty předávaných parametrů.

Při definici konstruktoru je třeba ignorovat skutečnost, že rodičovská třída má pouze dvouparametrický konstruktor a rodičovský konstruktor nevolat. Volání rodičovského konstruktoru v konstruktorech výčtových typů totiž patří mezi zakázané operace a je výhradním právem a povinností překladače .

Další věcí, která stojí za zmínku, je statický blok inicializující statické atributy. Těmto atributům totiž není možné přiřazovat hodnoty v konstruktoru, protože v době, kdy jsou instance konstruovány, dané atributy ještě vůbec neexistují. Protože deklarace výčtových hodnot musí být první deklarací v těle výčtového typu, nezbude vám, než umístit deklaraci statických atributů až za deklaraci hodnot výčtového typu a tím pádem budete moci statické atributy používat až poté, co budou výčtové konstanty definovány.

U výčtových typů proto neplatí, že statické atributy a metody je možné používat ještě před tím, než bude vytvořena první instance. Vzhledem k syntaktickým pravidlům se instance vždy vytvoří před tím, než je možné použít jakýkoliv jiný statický atribut či metodu.

Potřebujete-li proto naplnit nějaké statické kontejnery hodnotami odpovídajícími jednotlivým výčtovým konstantám, musíte zvolit nějaké náhradní řešení. V uváděném příkladu je definována pomocná vnitřní třída Přepravka, do jejichž instancí se v konstruktory uloží potřebné hodnoty a po definici výčtových konstant se tyto hodnoty vloží do příslušných kontejnerů a přepravky se předají správci paměti (garbage collector).

Ošetříte-li všechny tyto nebezpečné situace, je následující definice dalších metod již rutinní záležitostí a v programu jsou proto uvedeny jen částečně.

/*******************************************************************************
* Třída sloužící jako výčtový typ pro 8 hlavních světových stran
* a zvláštní hodnotu reprezentující nezadaný směr.
* Třída je zjednodušenou verzí stejnojmenné třídy z balíčku rup.spolecne.
*
* @author Rudolf Pecinovský
* @version 2.01, duben 2004
*/
public enum Směr8
{
//== HODNOTY VÝČTOVÉHO TYPU ====================================================
//Následující definice přidávají další čtyři parametry konstruktoru:
// - zmněny vodorovné a svislé souřadnice při pohybu v daném směru
// - zkratka používaná pro daný směr
// - plný název směru bez diakritiky
VÝCHOD ( 1, 0, "S", "VYCHOD"),
SEVEROVÝCHOD( 1, -1, "SV", "SEVEROVYCHOD"),
SEVER ( 0, -1, "S", "SEVER"),
SEVEROZÁPAD ( -1, -1, "SZ", "SEVEROZAPAD"),
ZÁPAD ( -1, 0, "Z", "ZAPAD"),
JIHOZÁPAD ( -1, 1, "JZ", "JIHOZAPAD"),
JIH ( 0, 1, "J", "JIH"),
JIHOVÝCHOD ( 1, 1, "JV", "JIHOVYCHOD"),
ŽÁDNÝ ( 0, 0, "@", "ZADNY"),
;
//== KONSTANTNÍ ATRIBUTY TŘÍDY =================================================
public static final int SMĚRŮ = 9;
private static final int MASKA = 7;
private static final Map<String,Směr8> názvy =
new HashMap<String,Směr8>( SMĚRŮ*3 );
private static final int[][] posun = new int[SMĚRŮ][2];
private static final Směr8[] SMĚRY = values();
static
{
for( Směr8 s : SMĚRY )
{
posun[s.ordinal()][0] = s.přepravka.dx;
posun[s.ordinal()][1] = s.přepravka.dy;
názvy.put( s.přepravka.zkratka, s );
názvy.put( s.přepravka.názevBHC,s );
názvy.put( s.name(), s );
s.přepravka = null;
}
}
//== PROMĚNNÉ ATRIBUTY INSTANCÍ ================================================
private static class Přepravka
{
int dx, dy;
String zkratka, názevBHC;
}
Přepravka přepravka;
//##############################################################################
//== KONSTRUKTORY A TOVÁRNÍ METODY =============================================
/**************************************************************************
* Vytvoří nový směr a zapamatuje si různé verze jeho názvu.
*/
private Směr8( int dx, int dy,
String zkratka, String název, String názevBHC )
{
přepravka = new Přepravka();
přepravka.dx = dx;
přepravka.dy = dy;
přepravka.zkratka = zkratka;
přepravka.název = název;
přepravka.názevBHC = názevBHC;
}
//== VLASTNI METODY INSTANCÍ ===================================================
/**************************************************************************
* Vráti směr otočený o 90° vlevo.
* Oproti metodě vlevoVbok nepotřebuje přetypovávat výsledek na Směr8.
*
* @return Směr objektu po vyplněni příkazu vlevo v bok
*/
public Směr8 vlevoVbok()
{
ověřPlatný();
return SMĚRY[MASKA & (2+ordinal())];
}
//Obdobně lze definovat i vpravoVbok, čelemVzad, nalevoVpříč a napravoVpříč
/**************************************************************************
* Vrátí znaménko změny x-ové souřadnice při pohybu v daném směru.
*
* @return znaménko změny x-ové souřadnice při pohybu v daném směru
*/
public int dx()
{
ověřPlatný();
return posun[ordinal()][0];
}
/**************************************************************************
* Vrátí x-ovou souřadnici políčka v daném směru
* vzdáleného vodorovně o zadanou vzdálenost.
*
* @param x x-ová souřadnice stávajícího políčka
* @param vzdálenost Vzdálenost políčka ve vodorovném směru
*
* @return x-ová souřadnice požadovaného políčka
*/
public int dalšíX( int x, int vzdálenost )
{
ověřPlatný();
return x + posun[ordinal()][0]*vzdálenost;
}
//Obdobné metody lze definovat i pro svislý směr
//== SOUKROMÉ A POMOCNÉ METODY INSTANCÍ ========================================
/***************************************************************************
* Ověří použitelnost daného směru, tj. že instance opravdu definuje
* smyslupllný směr.
*/
private void ověřPlatný()
{
if( this == ŽÁDNÝ )
throw new IllegalStateException(
"Operaci není možno porvádět nad směrem ŽÁDNÝ" );
}
}

Konstanty anonymních typů

Při vyjmenovávání metod třídy Enum jsem se zmínil o tom, že každá instance výčtového typu může být jiného typu. Nyní bych vám ukázal, jak se takový typ definuje a k čemu může být dobrý.

Takovýto výčtový typ využijete tehdy, budete-li potřebovat, aby jeho instance nepředstavovaly datový, ale funkční objekt, tj. aby se jednotlivé instance lišily chováním svých metod.

Výčtový typ, jehož instance se liší chováním svých metod, definujete tak, že jeho konstanty definujete jako instance anonymních tříd.

import java.util.Iterator;
public enum Operátor
{
//== HODNOTY VÝČTOVÉHO TYPU ====================================================
SOUČET( '+' )
{ double proveď(double x, double y) { return x + y; }
void nic() {} },
ROZDÍL( '-' )
{ double proveď(double x, double y) { return x - y; } },
SOUČIN( '×' )
{ double proveď(double x, double y) { return x * y; } },
PODÍL( ':' )
{ double proveď(double x, double y) { return x / y; } },
MOCNĚNÍ( '^' )
{ double proveď(double x, double y) { return Math.pow(x,y); } },
ODMOCNĚNÍ( 'V' ) //Přesněji '\u221A'
{ double proveď(double x, double y) { return Math.pow(y,1/x); } };
//== KONSTANTNÍ ATRIBUTY INSTANCÍ ==============================================
private final char znak;
//##############################################################################
//== KONSTRUKTORY A TOVÁRNÍ METODY =============================================
private Operátor( char znak )
{
this.znak = znak;
}
//== ABSTRAKTNÍ METODY =========================================================
abstract double proveď(double x, double y);
//== TESTY A METODA MAIN =======================================================
public static void test()
{
double x = 3;
double y = 8;
for (Operátor o : values() )
{
System.out.println( o + ": " +
x + " " + o.znak + " " + y + " = " +
o.proveď(x, y));
}
System.out.println( "Mocnění 2: " +
y + " " + MOCNĚNÍ.znak + " " + x + " = " +
MOCNĚNÍ.proveď(y, x));
System.out.println( "Odmocnění 2: " +
y + " " + ODMOCNĚNÍ.znak + " " + x + " = " +
ODMOCNĚNÍ.proveď(y, x));
System.out.println();
for (Operátor o : values() )
{
System.out.println( o +
" je konstantou třídy " + o.getDeclaringClass().getName() +
", ale instancí třídy " + o.getClass().getName() );
}
}
}

Při spuštění metody test se na standardní výstup vypíše následující text:

SOUČET: 3.0 + 8.0 = 11.0
ROZDÍL: 3.0 - 8.0 = -5.0
SOUČIN: 3.0 × 8.0 = 24.0
PODÍL: 3.0 : 8.0 = 0.375
MOCNĚNÍ: 3.0 ^ 8.0 = 6561.0
ODMOCNĚNÍ: 3.0 V 8.0 = 2.0
Mocnění 2: 8.0 ^ 3.0 = 512.0
Odmocnění 2: 8.0 ^ 3.0 = 1.147202690439877
SOUČET je konstantou třídy Operátor, ale instancí třídy Operátor$1
ROZDÍL je konstantou třídy Operátor, ale instancí třídy Operátor$2
SOUČIN je konstantou třídy Operátor, ale instancí třídy Operátor$3
PODÍL je konstantou třídy Operátor, ale instancí třídy Operátor$4
MOCNĚNÍ je konstantou třídy Operátor, ale instancí třídy Operátor$5
ODMOCNĚNÍ je konstantou třídy Operátor, ale instancí třídy Operátor$6

Uvedená třída využívá jednoparametrický konstruktor. Kdyby vystačila s bezparametrickým konstruktorem, nebylo by třeba za názvy výčtových konstant uvádět ani prázdné závorky a mohli bychom rovnou začít psát otevírací složenou závorku s tělem příslušné anonymní třídy.

Konstanty mohou ve svých třídách definovat libovolné metody, ale zvenku budou dostupné pouze ty, které definuje současně jejich mateřská třída, tj. třída, jejímiž jsou konstantami.

Metody, které budou definovány ve všech třídách mohou být v mateřské třídě deklarovány jako abstraktní (ostatně jak jinak, když budou vždy překryty).

Přestože se tak mateřská třída stane abstraktní třídou, nesmít tuto skutečnost uvést v její hlavičce. To, že je třída abstraktní překladač z přítomnosti abstraktních metod pochopí. Přítomnost klíčového slova abstract v hlavičce výčtového typu však považuje za syntaktickou chybu.

V závěru testovací metody jsem vám také ukázal cyklus dokazující nepoužitelnost metody getClass() pro zjištění mateřského výčtového typu konstant.