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ějijava.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.