[Effective Java 3/E] ITEM 37. ordinal 인덱싱 대신 EnumMap을 사용하라

    Effective Java 표지. Source : https://blog.insightbook.co.kr/

    배열이나 리스트에서 원소를 꺼낼 때, ordinal메서드로 인덱스를 얻는 코드가 가끔 있는데, 바람직하지 못하다.

     

    예제 1-1 : Plant 클래스

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

    Plant 클래스의 inner class로 enum 타입을 선언해주었다. Enum타입은 생애주기를 나타낸다.

     

    예제 1-2 : ordinal()을 배열의 인덱스로 사용(따라하지 말 것)

    Set<Plant>[] plantsByLifeCycleArr =
            (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
    for (int i = 0; i < plantsByLifeCycleArr.length; i++)
        plantsByLifeCycleArr[i] = new HashSet<>();
    for (Plant p : garden)
        plantsByLifeCycleArr[p.lifeCycle.ordinal()].add(p);
    // 결과 출력
    for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
        System.out.printf("%s: %s%n",
                Plant.LifeCycle.values()[i], plantsByLifeCycleArr[i]);
    }

    위 코드는 각 식물의 생애주기별로 집합을 통해 관리하려는 코드다. 배열은 Enum 타입의 상수를 매핑하려는 목적으로 사용한 것이다.

    그러나 굉장히 문제가 많은 코드이다 배열과 제네릭의 호환성 때문에 type safe 보장 불가(ITEM 28)하다.

     

    대신에 EnumMap을 사용하자.

    Enum 타입의 상수를 Key로 사용하는 빠른 EnumMap을 사용하는 것이 좋다.

    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);
    1. 짧게 작성 가능하다.
    2. type safe가 보장된다.
    3. EnumMap의 Key는 한정적 타입 토큰을 이용한 Class 객체이다.(ITEM 33)
    4. 성능도 뒤떨어지지 않는다.
    5. EnumMap이 배열에 뒤쳐지지 않는 이유는, 내부 구현에 배열을 사용했다. 별 차이가 없이 느껴질 수도 있겠지만 배열을 직접 다루는 것과 잘 만들어진 API를 이용하는건 생산성 측면에서나 안전성 측면에서 큰 차이를 보인다.

     

    참고 : Stream을 이용해서 맵 관리하기

    // 코드 37-3 스트림을 사용한 코드 1 - EnumMap을 사용하지 않는다! (228쪽)
    System.out.println(Arrays.stream(garden)
            .collect(groupingBy(p -> p.lifeCycle)));
    
    // 코드 37-4 스트림을 사용한 코드 2 - EnumMap을 이용해 데이터와 열거 타입을 매핑했다. (228쪽)
    System.out.println(Arrays.stream(garden)
            .collect(groupingBy(p -> p.lifeCycle,
                    () -> new EnumMap<>(LifeCycle.class), toSet())));

     

    위의 코드는 EnumMap 대신 고유의 맵 구현체를 사용했기에 EnumMap의 공간과 성능 이점을 활용할 수 없다.

    매개변수 3개짜리 Collectors.groupingBy 메서드를 이용해 원하는 맵 구현체를 명시할 수 있으며, 37-4 코드를 참조하면 된다.

    EnumMap만 사용했을 때와 Stream을 사용했을 때의 차이는, 전자는 각 생애주기에 해당하는 식물이 존재하든 그렇지 않든 각 생애주기별 중첩 맵을 만들지만, Stream을 이용할 땐 해당하는 식물 주기가 존재할 때만 중첩 맵을 만든다.

    댓글