Java - výčtové typy

V předešlých verzích byl standardním způsobem jak reprezentovat výčtové typy vzor int Výčtový typ:

	// Vzor výčtový typ int měl mnoho problémů!
	public static final int SEASON_WINTER = 0;
	public static final int SEASON_SPRING = 1;
	public static final int SEASON_SUMMER = 2;
	public static final intSEASON_FALL = 3;

Tento vzor měl mnoho problémů jako:

Tyto problémy je možné obejít pomocí vzoru Typově zabezpečené výčty, ale i tento vzor má svoje mouchy. Je docela upovídaný, náchylný k chybám a jeho výčtové konstanty nelze použít s příkazy switch.

V Tygrovi (Tiger release) dostal programovací jazyk Java klíčové slovo pro výčtové typy. V nejjednodušší formě tyto výčtové typy vypadají jako v C, C++ a C#:

enum Season {WINTER, SPRING, SUMMER, FALL}

Ale vzhled může klamat. Výčtové typy jazyka Java jsou mnohem výkonnější než jejich protějšky v jiných jazycích, které jsou o trochu více nže jen celá čísla. Dekalrace enum znamená že se jedná o plnohodnotnou třídu (nazývanou výčtový typ). Mimo to že řeší všechny problémy které jsme zmi�ovali výše, umož�uje vám do výčtového typu přidat metody a atributy, implementovat rozhraní atd. Výčtové typy poskytují vysoce kvalitní implementace metod třídy . Mohou implementovat rozhraní Comparable a Serializable a serializovaná forma je navržena tak aby ustála změny ve výčtovém typu.

Tady máme příklad třídy hracích karet postavené na jednoduchých výčtech. Třída Card je neměnitelná a j emožné vytovřit pouze jedinou instanci třídy, takže není potřeba překrývat metodu hashCode nebo equals:

import java.util.*;

public class Card {
	public enum Rank {DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING, ACE}
	public enum Suit {CLUBS, DIAMONDS, HEARTS, SPADES}

	private final Rank rank;
	private final Suit suit;
	private Card(Rank rank, Suit suit) {
		this.rank = rank;
		this.suit= suit;
	}

	public Rank rank() {
		return rank;
	}

	public Suit suit() {
		return suit;
	}

	public String toString() {
		return rank +" of "+ suit;
	}

	private static final List<Card> protoDeck = new ArrayList<Card>();

	// Inicializujeme protoDeck
	static {
		for (Suit suit : Suit.values())
			for (Rank rank : Rank.values())
				protoDeck.add(new Card(rank, suit));
	}

	public static ArrayList<Card> newDeck() {
		return new ArrayList<Card>(protoDeck); // Vrátíme kopii protoDeck
	}
}

Metoda toString třídy Card využívá metod toString Rank a Suit. Všimněte si že třída Card je poměrně krátká (má asi jen 25 řádků). Pokud bychom chtěli vytvořit zabezpečené výčty (Rank a Suit) dle svého, museli byste napsat mnohem více řádků než má třída Card.

Soukromý konstruktor třídy Card přebírá dva parametry, Rank a Suit. Pokud náhodou předáte parametry v obráceném pořadí, překladač vás informuje o chybě. V tom se liší od vzoru výčtu typu int při kterém program selže za běhu.

Všimněte si že každý výčet má statickou metodu values která vrací pole obsahující všechny hodnoty výčtového typu v pořadí v jakém jsou deklarovány. Tato metoda je běžně kombinována s cyklem for - each kterým iteruje hodnotami výčtového typu.

Následující příklad je jednoduchý program nazvaný Deal který testuje třídu Card. Načítá dvě čísla z příkazového řádku která představují počet rukou a počet karet v jedné ruce. Pak vytvoří novou hromádku karet, zamíchá je a tiskne typ karet v dané ruce.

import java.util.*;

public class Deal {
	public static void main(String[] args) {
		int numHands = Integer.parseInt(args[0]);
		int cardsPerHand = Integer.parseInt(args[1]);
		List<Card> deck = Card.newDeck();
		Collectons.shuffle(deck);
		for (int i = 0; i < numHands; i++)
			System.out.println(deal(deck, cardsPerHand));
	}

	public static ArrayList<Card> deal(List<Card> deck, int n) {
		int deckSize = deck.size();
		List<Card> handView = deck.subList(deckSize - n, deckSize);
		ArrayList<Card> hand = new ArrayList<Card>(handView);
		handView.clear();
		return hand;
	}
}

java Deal 4 5
[FOUR of HEARTS, NINE of DIAMONDS, QUEEN of SPADES, ACE of SPADES, NINE of SPADES]
[DEUCE  of HEARTS, EIGHT of SPADES, JACK of DIAMONDS, TEN of CLUBS, SEVEN of SPADES]
[FIVE of HEARTS, FOUR of DIAMONDS, SIX of DIAMONDS, NINE of CLUBS, JACK of CLUBS]
[SEVEN of HEARTS, SIX of CLUBS, DEUCE of DIAMONDS, THREE of SPADES, EIGHT of CLUBS]

Předpokládejme že chcete přidat data a vlatnosti do výčtového typu. Například zvažme planety obíhající kolem slunce. Každá planeta má svoji hmotnost a poloměr, a umí spočítat gravitaci na povrchu a váhu objektu na povrchu. Mohlo by to vypadat takhle:

public enum Planet {
	MERCURY (3.303e+23, 2.4397e6),
	VENUS (4.869e+24, 6.0518e6),
	EARTH (5.976e+24, 6.37814e6),
	MARS (6.421+23, 3.3972e6),
	JUPITER (1.9e+27, 7.1492e7),
	SATURN (5.688e+26, 6.0268e7),
	URANUS (8.686e+25, 2.5559e7),
	NEPTUNE (1.024e+26, 2.4746e7),
	PLUTO (1.27e+22, 1.137e6);

	private final double mass; 
	private final double radius;
	
	Planet(double mass, ,font id=kwrd>double radius) {
		this.mass = mass;
		this.radius = radius = radius;
	}

	private double mass() {
		return mass;
	}

	private double radius() {
		return radius;
	}

	public static final double G = 6.67300E-11;

	double surfaceGravity() {
		return G * mass / (radius * radius);
	}

	double surfaceWeight(double otherMass) {
		return otherMass * surfaceGravity();
	}
}

Výčtový typ Planet obsahuje konstruktor, a každá konstanta výčtového typu je deklarována s parametry které se mají předat konstruktoru.

Následující program přebírá jako parametr vaši váhu kterou máte zde na Zemi (v jakýchkoli jednotkách) a vypočítá a vytiskne vaši váhu na všch ostatních planetách (ve stejných jednotkách):

	public static void main(String[] args) {
		double earthWeight = Double.parseDouble(args[0]);
		double mass = earthWeight / EARTH.surfaceGravity();
		for (Planet p : Planet.values())
			System.out.printf("Your weight on %s is %f%n", p, p.surfaceWeight(mass));
	}

java Planet 175
Your weight on MERCURY is 66.107583
Your weight on VENUS is 158.374842
Your weight on EARTH is 175.000000
Your weight on MARS is 66.279007
Your weight on JUPITER is 442.847567
Your weight on SATURN is 186.552719
Your weight on URANUS is 158.397260
Your weight on NEPTUNE is 199.207413
Your weight on PLUTO is 11.703031

Myšlenka přidání vlastností výčtovým konstantám může zajít trochu dále. Můžete nastavit pro každou konstantu odlišnou vlastnost každé metody. Jedním způsobemjak toho dosáhnout je pomocí příkazu switch. Následující příklad obsahuje výčtové typy které představují čtyři základní aritmetické operace a jejichž metody eval provádí tyto operace:

public enum Operation {
	PLUS, MINUS, TIMES, DIVIDE;

	double eval(double x, double y) {
		switch(this) {
			case PLUS: return x + y;
			case MINUS: return x - y;
			case TIMES: return x * y;
			case DIVIDE: return x / y;
		}
		throw new AssertionError("Unknown op: "+ this);
	}
}

To funguje dobře, ale bez příkazu throw se program nepřeloží, což není zrovna nejlepší. Horší je, že pokaždé když přidáte novou konstantu třídy Operation, nesmíte zapomenout přidat nový nový případ do příkazu switch.

Existuje i jiný způsob jak zaručit pro každou konstantu jiné chování. Můžete deklarovat abstraktní metodu výčtového typu a překrýt ji konkrétní metodou pro každou konstantu. Takové metody se nazývají konkrétní metody konstanty. Tady je uppravený předchozí příklad:

public enum Operation {
	PLUS {
		double eval(double x, double y) {
			return x + y;
		}
	},
	MINUS {
		 double eval(double x, double y) {
			return x - y;
		}
	},
	TIMES {
		double eval(double x, double y) {
			return x * y;
		}
	},
	DIVIDE {
		double eval(double x, double y) {
			return x / y;
		}
	};

	abstract double eval(double x, double y);
}

Níže je program testující třídu Operation. Program přebírá dva argumenty příkazového řádku, prochází všechny operace a pro provádí každou operaci a tiskne výsledky:

	public static void main(String args[]) {
		double x = Double.parseDouble(args[0]);
		double y = Double.parseDouble(args[1]);
		for (Operation op : Operation.values())
			System.out.printf("%f %s %f = %f%n", x, op, y, op.eval(x, y));
	}

java Operation 4 2
4.000000 PLUS 2.000000 = 6.000000
4.000000 MINUS 2.000000 = 2.000000
4.000000 TIMES 2.000000 = 8.000000
4.000000 DIVIDE 2.000000 = 2.000000

Konkrétní metody konstant jsou samozřejmě příliš sofistikované a mnoho programátorů je nikdy nepoužije, ale je dobré o nich vědět.

Do balíčku java.util byly přidány dvě třídy na podporu výčtových typů: speciální implementace Set a Map nazvané EnumSet a EnumMap. EnumSet je vysoce výkonnou implementací Set výčtových typů. Všechny členy množiny výčtů musí být stejného typu. Vnitřně je tato množina reprezentována vektorem bitů, typicky long. Množiny výčtů podporují iterování výčtovými typy. Jako příklad poslouží následující deklarace výčtového typu:

enum Day {
	SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY}

Můžeme iterovat jednotlivými dny týdne. Třída EnumSet poskytuje statickou továrnu která nám to ulehčí:

	for (Day d : EnumSet.range(Day.MONDAY, Day.FRIDAY))
		System.out.println(d);

Množiny výčtů jsou také hodnotnou, typově zabezpečenou náhradou tradičníchpřízanků (bitových masek):

	enumSet.of(Style.BOLD, Style.ITALIC)

Stejně tak EnumMap je vysoce výkonnou implemntací Map výčtových klíčů, interně implemntovanou jako pole. Výčtové mapy jsou kombinací hodnoty a bezpečnosti rozhraní Map s rychlostí pole. Pokud chcete namapovat výčtový typ na nějakou hodnotu, měli byste vždy použít EnumMap místo pole.

Třída Card uvedená výše obsahuje statickou továrnu která vrací balíček karet, ale neobsahuje možnost získat specifickou kartu. Vystavení konstruktoru by zničilo vlastnost jedináčka. Tady je návod jak napsat statickou továrnu která chrání vlastnost jedináčka použitím vnitřní třídy EnumMap:

	private staic Map<Suit, Map<Rank, Card>> table =
		new EnumMap<Suit, Map<Rank, Card>>(Suit.class);

	static {
		for (Suit suit : Suit.values()) {
			Map<Rank, Card> suitTable = new EnumMap<Rank, Card>(Rank.class);
			for (Rank rank : Rank.values())
				suitTable.put(rank, new Card(rank, suit));
			table.put(suit, suiTable);
		}
	}

	public static Card valueOf(Rank rank, Suit suit) {
		return table.get(suit).get(rank);
	}

EnumMap (table) mapuje každou suit do EnumMap která mapuje každý rank ke card. Vyhledávání v tabulce které provádí metoda valueOf je interně implementováno přístupem do dvou polí, ale kód je mnohem čistější a bezpečnější.