[교재 EffectiveJava] 아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라

반응형
728x90
반응형

ordinal 인덱싱의 문제점

배열 또는 리스트에서 원소를 꺼낼때 ordinal 메서드로 인덱스를 얻는 코드가 있다.

package com.java.effective.item37;

public class Plant {

    enum LifeCycle { ANNUAL, PERNNIAL, BIENNIAL}

    final String name;
    final LifeCycle lifeCycle;

    public Plant(String name, LifeCycle lifeCycle) {
        this.name = name;
        this.lifeCycle = lifeCycle;
    }

    @Override
    public String toString() {
        return name;
    }
}

 

이 식물들을 배열 하나로 관리하고, 생애주기(한해살이, 여러해살이, 두해살이)별로 묶어보자. 생애주기별로 총 3개의 집합을 만들고 정원을 한바퀴 돌며 각 식물을 해당 집합에 넣는다. 이때 어떤 프로그래머는 집합들을 배열 하나에 넣고 생애주기의 ordinal 값을 그 배열의 인덱스로 사용하려 할 것이다.

 

ordinal()을 배열 인덱스로 사용하는 좋지 않은 코드
package com.java.effective.item37;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class Main {
    public static void usingOrdinalArray(List<Plant> garden) {
        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 plant : garden) {
            // ordinal() 인덱싱
            plantsByLifeCycle[plant.lifeCycle.ordinal()].add(plant);
        }

        for (int i = 0 ; i < plantsByLifeCycle.length ; i++) {
            System.out.printf("%s : %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
        }
    }
}

배열은 제네릭과 호환되지 않으니, 비검사 형변환을 수행해야 하고 컴파일 경고가 발생한다. 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야한다. 정확한 정숫값을 사용한다는 것을 직접 보장해야한다. 정수는 열거 타입과 달리 타입 안전하지 않으므로, 잘못된 값을 사용하면 잘못된 동작을 그대로 수행하거나 arrayIndexOutOfBoundsException 을 던질 것이다.

 

 

해결방안 EnumMap

EnumMap을 사용하여 데이터와 열거 타입을 매핑한다.

package com.java.effective.item37;

import java.util.*;

public class Main {
    public static void usingEnumMap(List<Plant> garden) {
        Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);

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

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

이전 코드의 문제점을 해결해준다. 안전하지않은 형변환을 쓰지 않고, 맵의 키인 열거 타입이 그 자세로 출력용 문자열을 제공하니 출력 결과에 직접 레이블을 달 필요가 없다. 

 

 

스트림 사용

스트림을 사용한 코드로 맵을 관리하면 코드를 더 줄일 수 있다. 위 코드의 동작을 스트림을 사용해보자.

 

EnumMap 사용하지 않은 코드
System.out.println(Arrays.stream(garden)
                .collect(groupingBy(p -> p.lifeCycle)));

EnumMap이 아닌 고유한 맵 구현체를 사용하여 EnumMap을 써서 얻었던 공간과 성능 이점이 사라졌다. 

 

EnumMap 사용한 코드
System.out.println(Arrays.stream(garden)
                .collect(Collectors.groupingBy(p -> p.lifeCycle,
                        () -> new EnumMap<>(LifeCycle.class), toSet()));

 

매개변수 3개의 Collectors.groupintBy 메서드는 mapFactory 매개변수에 원하는 맵 구현체를 명시하여 호출할 수 있다. 스트림을 사용할 경우 EnumMap만 사용했을 경우와 조금 다르게 동작한다. 만약 생애주기에 한해살이, 여러해살이 식물만 있고 두해살이는 없다면 EnumMap 버전에서는 맵을 3개 만드는 반면, 스트림은 2개만 만든다. 스트림은 해당 생애주기에 속하는 식물이 있을 때만 만든다.

 

 

ordinal() 사용 예제

두 열거 타입 값들을 매핑하기 위해 ordinal을 두번 이상 쓴 배열들의 배열이 존재하는 예제를 보자.  두가지 상태(Phase)를 전이(Transition)와 매핑하도록 구현한 프로그램으로, 액체 -> 고체 (응고), 액체 -> 기체 (기화) 된다.

package com.java.effective.item37;

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 과 배열 인덱스의 관계를 알지 못한다.  예상치못한 상황이 발생할 수 있다.  해당 코드를 EnumMap으로 바꿔보자.

 

EmumMap 사용 버전
package com.java.effective.item37;

import java.util.EnumMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toMap;

public enum PhaseEnumMap {
    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 PhaseEnumMap from;
        private final PhaseEnumMap to;

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

        /**
         * 이전 상태에서 '이후 상태에서 전이로의 맵'을 대응시키는 맵
         */
        private static final Map<PhaseEnumMap, Map<PhaseEnumMap, Transition>>
                // groupingBy : 전이를 이전 상태를 기준으로 묶는다.
                m = Stream.of(values()).collect(groupingBy(t -> t.from,
                    () -> new EnumMap<>(PhaseEnumMap.class),
                    // toMap : 이후 상태를 전이에 대응시키는 EnumMap 생성한다.
                    toMap(t -> t.to, t -> t,
                            // (x,y) -> y 는 선언만 하고 실제로는 쓰이지 않는다.
                            // -> 단지 EnumMap을 얻으려면 맵 팩터리가 필요하고,
                            //    수집기들은 점층적 팩터리를 제공하기 때문이다.
                            (x, y) -> y, () -> new EnumMap<>(PhaseEnumMap.class))));

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

▶ m 디버깅

위 코드에서 새로운 상태인 플라스마(PLASMA)를 추가하려고 한다. 우리는 EnumMap 으로 구현된 코드에서 새로운 상태의 추가를 간단하게 해결할 수 있다.

 

PLASMA 를 추가한 코드
package com.java.effective.item37;

import java.util.EnumMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toMap;

public enum PhaseEnumMap {
    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);
		
        // 나머지 코드는 그대로 유지
        ...
    }
}

결론적으로, 배열의 인덱스를 얻기 위해 ordinal을 쓰는 것보다 EnumMap을 사용하여 구현하는게 더 좋다. 

 

 

 

반응형

Designed by JB FACTORY