Skip to content

Latest commit

 

History

History
147 lines (107 loc) · 5.5 KB

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

File metadata and controls

147 lines (107 loc) · 5.5 KB

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

배열이나 리스트에서 원소를 꺼낼 때, ordinal 메서드로 인덱스를 얻을 수 있지만, 사용하지 말자!

class Plant {
    enum LifeCycle { ANNUAL, PERPENNIAL, BIENNIAL }
    
    final String name;
    final LifeCycle;
    
    Plant(String name, LifeCycle lifeCycle) {
        this.name = name;
        this.lifeCycle = lifeCycle;
    }
    
    @Override public String toString() {
        return name;
    }
}
  • ordinal()을 배열 인덱스로 사용한 경우

    Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
    
    for(int i = 0; i < plantsByLife.length; i++) 
        plantsByLifeCycle[i] = new HashSet<>();
    
    for(Plant p: garden)
        plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
    
    //결과 출력
    for(int i = 0; i < plantsByLifeCycle.length; i++) {
        System.out.printf("%s: %s%n",
                         Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
    }
    • 문제점
      • 배열은 제네릭과 호환되지 않기 때문에 비검사 형변환을 수행해야 하고, 깔끔하게 컴파일되지 않는다.
      • 배열은 각 인덱스의 의미를 모르기 때문에 출력 결과에 직접 레이블을 달아야 한다.
      • 정확한 정숫값을 사용하는 것을 직접 보증해야 한다.
    • EnumMap을 사용하여 열거 타입 상수를 값으로 매핑할 수 있다.
  • EnumMap을 사용하여 데이터와 열거 타입을 매핑하는 경우

    Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMp<>(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); 
    • 맵의 키인 열거 타입이 그 자체로 출력용 문제열을 제공하기 때문에 출력 결과에 직접 레이블을 달지 않아도 된다.
    • 배열 인덱스를 계산하지 않기 때문에 오류 가능성이 줄어든다.
  • 스트림을 사용한 코드 - Enum을 사용하지 않는다.

    System.out.println(Arrays.stream(garden))
        .collect(groupingBy(p -> p.lifeCycle, () -> new EnumMap<>(LifeCycle.class), toSet()));
    • Enum이 아닌 고유한 맵 구현체를 사용했기 때문에 Enum을 사용해서 얻은 공간과 성능 이점이 사라진다는 문제가 있다.
  • 스트림을 사용한 코드 - EnumMap을 사용하여 데이터와 열거 타입을 매핑했다.

    System.out.println(Arrays.stream(garden)
                      .colect(groupingBy(p -> p.lifeCycle,
                                        () -> new EnumMap<>(LifeCycle.class), toSet())));
    • EnumMap 버전은 식물의 생애주기당 하나씩 중첩 맵을 만들지만, 스트림 버전에서는 해당 생애주기에 속하는 식물이 있을 때만 만든다.
    • e.g) 정원에 한해살이와 여러해살이 식물만 살고, 두해살이는 없다면, EnumMap 버전에서는 맵을 3개 만들고, 스트림 버전에서는 2개만 만든다.
  • 두 열거 타입을 매핑하기 위해 ordinal을 두 번 호출하는 방식을 사용하지 말자!

    public enum Phase {
        SOLID, LIQUID, GAS;
        
        public enum Transition {
            MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
            
            // 행은 from의 ordinal을, 열은 to의 ordinal을 인덱스로 사용한다.
            private static final Transision[][] 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과 배열 인덱스의 관계를 알 수 없다. 따라서 PhasePhase.Transition 열거 타입을 수정하면서 상전이 표 TRANSITION을 함께 수정하지 않거나 실수로 잘못 수정하면 런타임 오류가 날 수 있다.
  • 중첩 EnumMap으로 데이터와 열거 타입 쌍을 연결할 수 있다.

    public enum Phase {
        SOLID, LIQUID, GAS;
        
        public enum Transition {
            MELT(SOLID, LIQUID), FREEZE(LIAUID, 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);
        }
    }
    • 중첩 EnumMap을 사용한다면 코드가 복잡해질 수 있지만, 가독성이 높아지며, 새로운 상태를 간단하게 추가할 수 있다.

정리

  • 배열의 인덱스를 얻기 위해 ordinal을 사용하는 것은 일반적으로 좋지 않으니, EnumMap을 사용하라!
  • 다차원 관계는 Enum<..., EnumMap<...>>으로 표현할 수 있다.