Item 37. EnumMap

ordinal 인덱싱 대신 EnumMap을 사용하라.

배열이나 리스트에서 원소를 꺼낼 때 ordinal 메서드로 인덱스를 얻을 수 있지만, 보통 이런 용도로 ordinal을 쓰는 것은 좋지 않다.

ordinal

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;
    }
}

class Main {
    public static void main(String[] args) {
        Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length]; // 비검사 형변환 경고
        List<Plant> garden = List.of(
                new Plant("바질", Plant.LifeCycle.ANNUAL),
                new Plant("캐러웨이", Plant.LifeCycle.BIENNIAL),
                new Plant("딜", Plant.LifeCycle.ANNUAL),
                new Plant("라벤더", Plant.LifeCycle.PERENNIAL),
                new Plant("파슬리", Plant.LifeCycle.BIENNIAL),
                new Plant("로즈마리", Plant.LifeCycle.PERENNIAL)
        );

        for (int i = 0; i < plantsByLifeCycle.length; i++) {
            plantsByLifeCycle[i] = new HashSet<>();
        }

        for (Plant p : garden) {
            plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
        }

        for (int i = 0; i < plantsByLifeCycle.length; i++) {
            // 배열은 인덱스의 의미를 모르기 때문에 출력할 때 마다 LifeCycle.values()를 호출해야 한다.
            System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]); // ArrayIndexOutOfBoundsException 발생 가능
        }
    }
}

EnumMap

위 방식은 동작은 하지만 주석에 적힌 대로 문제가 많다. 위 방식이 아닌 EnumMap을 사용하면 이런 문제를 해결할 수 있다.

class Main {
    public static void main(String[] args) {
        Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class); // 형변환 없이 안전하게 선언

        List<Plant> garden = List.of(
                new Plant("바질", Plant.LifeCycle.ANNUAL),
                new Plant("캐러웨이", Plant.LifeCycle.BIENNIAL),
                new Plant("딜", Plant.LifeCycle.ANNUAL),
                new Plant("라벤더", Plant.LifeCycle.PERENNIAL),
                new Plant("파슬리", Plant.LifeCycle.BIENNIAL),
                new Plant("로즈마리", Plant.LifeCycle.PERENNIAL)
        );

        for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
            plantsByLifeCycle.put(lc, new HashSet<>());
        }

        for (Plant p : garden) {
            plantsByLifeCycle.get(p.lifeCycle).add(p);
        }

        for (Plant.LifeCycle lifeCycle : plantsByLifeCycle.keySet()) {
            // 키를 직접 사용해 순회하면 ordinal 메서드를 사용하지 않아도 된다.
            System.out.printf("%s: %s%n", lifeCycle, plantsByLifeCycle.get(lifeCycle)); // 인덱스를 사용하지 않고도 출력 가능
        }
    }
}

이와 같이 EnumMap을 사용하면 성능 저하도 없고, 타입 안전성도 확보할 수 있다. (내부에서 배열을 사용하기 때문이며, 내부 구현 방식을 안으로 숨겨서 타입 안전성을 확보한다.)

열거 타입 값들 매핑 예시

이번엔 두 열거 타입 값들을 매핑하여 로직을 구현해야하는 경우를 살펴보면 아래와 같이 구현할 수 있다.

enum Phase {
    SOLID, LIQUID, GAS;

    public enum Transition {
        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;

        // 행: from, 열: to, 상태가 늘어나는 만큼 이차원 배열이 커진다.
        private static final Transition[][] TRANSITIONS = {
                {null, MELT, SUBLIME},
                {FREEZE, null, BOIL},
                {DEPOSIT, CONDENSE, null}
        };

        // 다른 상태로 전이하는 메서드
        public static Transition from(Phase from, Phase to) {
            // ordinal 메서드를 사용해 인덱스를 얻는다, 이는 위에서 살펴본 것처럼 좋지 않은 방식이다.
            return TRANSITIONS[from.ordinal()][to.ordinal()];
        }
    }
}

위와 같이 구현하면 상태가 늘어날 때마다 이차원 배열의 크기가 커지기 때문에 메모리 낭비가 심해지는 것 뿐만 아니라 오류가 발생할 가능성도 높아진다.

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);

        // 상전이 맵 초기화
        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))));

        private final Phase from;
        private final Phase to;

        Transition(Phase from, Phase to) {
            this.from = from;
            this.to = to;
        }

        public static Transition from(Phase from, Phase to) {
            return m.get(from).get(to);
        }
    }
}

하지만 EnumMap을 사용하면 초기화 과정이 다소 복잡해지지만, 이차원 배열을 사용하는 것보다 훨씬 안정적이고 유연하게 사용할 수 있다. 만약 새로운 상태인 PLASMA가 추가되더라도 주석과 같이 상태와 전이 목록에만 추가하고, 나머지 코드는 수정할 필요가 없다.

Last updated

Was this helpful?