Item 37: Use EnumMap instead of ordinal indexing

37. Use EnumMap instead of ordinal

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

Item 37: Use EnumMap instead of ordinal indexing
Item 36: Use EnumSet instead of bit fields
Use enum instead of int constant
Item 71: Avoid unnecessary use of checked exceptions
Item 72: Favor the use of standard exceptions
[Java] Why use StringUtils.isEmpty () instead of String.isEmpty ()
Item 87: Consider using a custom serialized form
[Read Effective Java] Chapter 2 Item 1 "Consider static factory methods instead of constructors"
Item 36: Use EnumSet instead of bit fields
Item 37: Use EnumMap instead of ordinal indexing
Item 44: Favor the use of standard functional interfaces
Why use setters/getters instead of public/private in Java
[Swift] Why FlowLayout should use Delegate instead of instance
Use of Date class
Rails Tutorial/Significance of Indexing
Item 53: Use varargs judiciously
Item 45: Use streams judiciously