[Effective Java] 아이템 37 : ordinal 인덱싱 대신 EnumMap을 사용하라

Loopy·2022년 8월 19일
0

이펙티브 자바

목록 보기
36/76
post-thumbnail

1️⃣ Ordinal()


배열이나 리스트에서 원소를 꺼낼 때와 같이 인덱싱이 필요할때는, ordinal() 메서드(아이템 35)로 인덱스를 얻을 수 있다.

🔗 Ordinal 메서드 사용

다음의 식물을 나타낸 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;
    }
 }

생애주기별로 총 3개의 집합을 만들고, 각 식물을 해당 집합에 넣자. 이때 주로 생애주기의 ordinal 값을 그 배열의 인덱스로 사용하려 할 것이다.

▶️ 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); // ordinal()사용
       
  // 결과 출력
 for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
      System.out.printf("%s: %s%n",
                    Plant.LifeCycle.values()[i], plantsByLifeCycleArr[i]);
 }

해당 코드는 동작은 하지만, 다음과 같은 문제가 존재한다.

1) 배열은 제네릭과 호환되지 않으니(아이템 28) 비검사 형변환을 수행해야 한다.
2) 정확한 정수값을 사용한다는 것을 직접 보증해야 한다. (정수는 타입 안전하지 않음)

2️⃣ EnumMap


🔗 EnumMap 개념 및 사용

👉 위 문제점의 해결책은, EnumMap을 사용하는 것이다.

EnumMap은 열거 타입을 키로 가지며, 배열과 마찬가지로 실질적으로 열거 타입 상수를 값으로 매핑하는 일을 한다.

참고로 키 타입의 Class 객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공하고 있다.(아이템 33)

▶️ EnumMap을 사용해 데이터와 열거 타입을 매핑

Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =
                new EnumMap<>(Plant.LifeCycle.class);  // EnumMap
                  
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) 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공하니 출력 결과에 직접 레이블을 달 필요 없다.
3) EnumMap 내부 구현은 배열 방식이므로 성능이 동일하다.

🔗 EnumMap + Stream 사용

스트림(아이템 45)를 사용해 맵을 관리하면 코드를 더 줄일 수 있다.

▶️ 스트림을 사용한 코드

Collectors.groupingBy() 메서드는 mapFactory 매개변수에 원하는 맵 구현체를 명시해 호출 할 수 있기 때문에, 다음과 같이 EnumMap을 이용해 데이터와 열거 타입을 매핑할 수 있다.

System.out.println(Arrays.stream(garden)
                .collect(groupingBy(p -> p.lifeCycle,
                        () -> new EnumMap<>(LifeCycle.class), toSet())));
}

스트림 버전에서는 해당 생애주기에 속하는 식물이 있을 때만 만들고, EnumMap 버전은 언제나 생애주기당 하나씩 중첩 맵을 만든다는 점에서 차이가 존재한다.

🔗 중첩 EnumMap : 다차원 관계 표현

두 열거 타입 값들을 매핑하느라, ordinal을 두 번씩이나 쓴 배열들의 배열의 예시를 보자. 다음은 이 방식을 이용해 두 가지 상태를 전이와 매핑하도록 구현한 코드이다.

▶️ 2중 배열의 인덱스에 ordinal() 사용

public enum Phase {
    SOLID, LIQUID, GAS;
    public enum Transition {
        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
        
        private static final Transition[][] TRANSITIONS = {
            {null, MELT, SUBLIME },
            {FREEZE, null, BOIL},
            {DEPOSIT, CONDENSE, null}
       };

       // 한 상태에서 다른 상태로의 전이를 반환
       public static Transition from(Phase from, Phase to){
        return TRANSITIONS[from.ordinal()][to.ordinal()];
       }
   }
}

하지만, 마찬가지로 컴파일러는 ordinal과 배열 인덱스의 관계를 알 수 없다.
즉, Phase 나 Phase.Transition 열거 타입을 수정한다면 ArrayIndexOutOfBoundsException 이나 NullPointerException 예외를 던질 가능성이 높아지는 것이다.

전이 하나를 얻으려면 상태 2개가 필요하니, 맵 2개를 중첩하는 EnumMap을 사용하면 해당 문제를 쉽게 해결할 수 있다.(안쪽 맵은 이전 상태와 전이를 연결, 바깥 맵은 이후 상태와 안쪽 맵을 연결)

▶️ 중첩 EnumMap으로 데이터와 열거 타입 쌍을 연결

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

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

Map<Phase, Map<Phase, Transition>> 는 "이전 상태에서 '이후 상태에서 전이로의 맵'에 대응시키는 맵" 이라는 뜻이다.

만약에 새로운 상태를 추가한다면, 배열로 만든 코드는 새로운 상수 추가 + 배열 크기 교체가 필요하지만 EnumMap 버전에서는 상태 목록(ex)PLASMA) 과 전이 목록(ex) IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS))에 원소만 추가해주면 끝이기 때문에 간단하다는 장점이 있다.

📚 핵심 정리
배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니, 대신 EnumMap을 사용하라. 다차원 관계는 EnumMap<..., EnumMap<...>> 으로 표현하라.

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글