As an example of using the ordinal method (Item35), consider the following Plant class.
class Plant {
enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override public String toString() {
return name;
}
}
Now suppose you want to create an array of Plants for each life cycle. For that, you might end up using ordinal as follows:
// Using ordinal() to index into an array - DON'T DO THIS!
Set<Plant>[] plantsByLifeCycle =
(Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycle.length; i++)
plantsByLifeCycle[i] = new HashSet<>();
for (Plant p : garden)
plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
// Print the results
for (int i = 0; i < plantsByLifeCycle.length; i++) {
System.out.printf("%s: %s%n",
Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}
The biggest problem here is that when you try to access an array, you need to choose the correct integer value for the array ordinal located in the enum. If you choose the wrong number, you will get an ArrayIndexOutOfBoundsException if you are lucky, but if you are not, the process will proceed with the wrong choice.
There is a better way to achieve the same. Since the array here acts like a Map that obtains value using enum as a key, it is better to use Map for implementation. Furthermore, an efficient implementation as a Map with enum as a key is done in EnumMap
, so we will see an example using this below.
// Using an EnumMap to associate data with an enum
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =
new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values())
plantsByLifeCycle.put(lc, new HashSet<>());
for (Plant p : garden)
plantsByLifeCycle.get(p.lifeCycle).add(p);
System.out.println(plantsByLifeCycle);
This implementation is shorter, easier to see, and safer. Specifically, it has the following merits.
Also, the EnumMap constructor takes a Class object as an argument, which is a bounded type and provides run-time generic type information (Item33). The previous example is even shorter with a stream (Item45):
// Naive stream-based approach - unlikely to produce an EnumMap!
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle)));
The problem with this implementation is that EnumMap is not used and its performance is inferior to what it would be if it were used. To fix this, show the Map that is explicitly used as follows.
// Using a stream and an EnumMap to associate data with an enum
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class), toSet())));
This optimization becomes important when using Map a lot.
Two enums may be represented using an array of arrays with ordinal values. The following source code is an example that deals with changes between two states.
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
// Rows indexed by from-ordinal, cols by to-ordinal
private static final Transition[][] TRANSITIONS = { { null, MELT, SUBLIME }, { FREEZE, null, BOIL },
{ DEPOSIT, CONDENSE, null } };
// Returns the phase transition from one phase to another
public static Transition from(Phase from, Phase to) {
return TRANSITIONS[from.ordinal()][to.ordinal()];
}
}
}
There is a problem with this program. The compiler has no way of knowing the relationship between the ordinal value and the index of the array, and if it fails to create the array or forgets to update the array with the information update, it either fails at runtime or ArrayIndexOutOfBoundsException
Or, NullPointerException
occurs, or the process proceeds with incorrect behavior.
The above enum can be written better by using EnumMap
.
// Using a nested EnumMap to associate data with enum pairs
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID), BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID), SUBLIME(SOLID,
GAS), DEPOSIT(GAS, SOLID);
private final Phase from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
// Initialize the phase transition map
private static final Map<Phase, Map<Phase, Transition>> m = Stream.of(values())
.collect(groupingBy(t -> t.from, () -> new EnumMap<>(Phase.class),
toMap(t -> t.to, t -> t, (x, y) -> y, () -> new EnumMap<>(Phase.class))));
public static Transition from(Phase from, Phase to) {
return m.get(from).get(to);
}
}
}
The initialization part of this code is a little complicated. The third argument in toMap, `(x, y)-> y```, is not used and is only needed to get the EnumMap. Now suppose we want to define a new state, plasma. The definitions of state transitions added here are ionization and deionization. In an array-based enum, if I try to incorporate this change, I add one new constant to
Phase```, two constants to ``
Phase.Transition, and nine elements. It is necessary to rewrite the two-dimensional array that was there so that it has 16 elements. On the other hand, if it is an enum based on
EnumMap, add one new constant to `` `Phase
and add two constants to `` `Phase.Transition``` as shown below. Just add it to.
// Adding a new phase using the nested EnumMap implementation
public enum Phase {
SOLID, LIQUID, GAS, PLASMA;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID),
IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);
... // Remainder unchanged
}
}
This code can't cause human error, and since it uses an array of arrays inside EnumMap, there is no inferior performance.
Recommended Posts