언어/Effective Java

이펙티브 자바 Item 37 - ordinal 인덱싱 대신 EnumMap을 사용하라

now0204 2024. 6. 7. 17:43

 

열거 타입의 ordinal을 배열의 인덱스로 사용하는 경우가 있다. 식물의 생애주기를 열거 타입으로 표현한 

LifeCycle타입을 예로 들어보자

 

public class Plant {
    final String name;
    final LifeCycle lifeCycle;

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

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

public enum LifeCycle {
    ANNUAL, PERNNIAL, BIENNIAL
}

 

- Plant 클래스는 LifeCycle 열거 타입을 멤버 변수로 가지고 있다. 이를 이용해서 식물들을 생애주기로 묶어보자.

public static void usingOrdinalArray(List<Plant> garden) {
        Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[LifeCycle.values().length];
        for (int i = 0 ; i < plantsByLifeCycle.length ; i++) {
            plantsByLifeCycle[i] = new HashSet<>();
        }
		//총 3개의 Set에 저장됨 (LifeCycle가 ANNUAL, PERNNIAL, BIENNIAL 총 3개임)
        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",
                    LifeCycle.values()[i], plantsByLifeCycle[i]);
        }
    }

 

위 코드는 맨 처음 언급한 열거 타입의 ordinal을 배열의 인덱스로 사용하는 코드이다.

 

1. Set 배열을 생성해 생애주기별 관리한다. 총 3개의 배열이 만들어질 것이다. 각 배열을 순회하여 빈 HashSet으로 초기화 해준다.

 

2. Plant 들을 배열의 Set에 추가한다. 이때 plant가 가지고 있는 LifeCycle 열거타입의 oridnal 값으로 배열의 인덱스를 결정한다. 그 결과 식물의 생애주기 별로 Set에 추가된다. 

 

3. 결과를 출력한다. 열거 타입의 values로 반환되는 열거 타입 상수 배열의 순서는 ordinal 값으로 결정되기 때문에 Set 배열의 Set이 의미하는 생애주기는 values 순서와 같을 것이다. 

 

잘 동작하지만, 한가지 문제가 있다.

 

1. 배열은 제네릭과 호환되지 않는다. 따라서 비검사 형변환을 수행해야한다. 

 

2. 사실상 배열은 각 인덱스가 의미하는 바를 알지 못하기 때문에 출력 결과에 직접 레이블을 달아야 한다.

 

3. 정수는 열거 타입과 달리 타입 안전하지 않기 때문에 정확한 정숫값을 사용한다는 것을 직접 보증해야한다.

 

이러한 단점들을 java.util 패키지의 EnumMap을 사용하여 해결해보자. EnumMap은 열거 타입을 키로 사용하는 Map 구현체이다. 

 

public static void usingEnumMap(List<Plant> garden){

	//Key로 사용할 Enum 클래스를 지정한다.
	Map<LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);
	
    //Enum을 values로 뽑아서, Key가 Enum의 구현체, Values를 줘서 Map을 채운다) 
    for(LifeCycle lifeCycle : LifeCycle.values()){
    	plantsByLifeCycle.put(lifeCycel, new HashSet<>());
    }
    
    for(Plant plant : garden){
    	plantsByLifeCycle.get(plant.lifeCycel).add(plant);
    }
    
    System.out.println(plantsByLifeCycle);

}

 

 

1. 이전의 ordinal을 사용한 코드와 다르게 안전하지 않은 형변환을 사용하지 않는다.

 

2. 결과를 출력하기 위해 번거롭던 과정도 EunmMap 자체가 toString을 제공하기 때문에 번거롭지 않게 되었다.

 

3. ordinal을 이용한 배열 인덱스를 사용하지 않으니 인덱스를 계산하는 과정에서 오류가 날 가능성이 존재하지 않는다.

 

4. EnumMap은 내부에서 배열을 사용하기 때문에 내부 구현 방식을 숨겨서 Map의 타입 안정성과 배열의 성능을 모두 얻어냈다.

 

여기서 EnumMap의 생성자는 한정적 타입 토큰의 키 타입의 Class 객체를 받는데 이는 제네릭의 타입 정보가 런타임시에 소거되기 때문에 런타임 제네릭 타입 정보를 제공하기 위해 키 타입의 Class 객체를 받도록 했다.

 


 

2. 중첩 EnumMap - 데이터와 열거 타입 쌍 매핑 

 

두 열거 타입 값들을 매핑할 때 ordinal 두 번씩 쓴 배열들의 배열을 본 적이 있을 것이다. 

두 가지 상태(Phase)를 전이(Transition)과 매핑하는 예제를 보자 

 

public enum Phase{
	
    SOLID, LIQUID, GAS;
    
    public enum Transition{
    
    	MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
        
        // 행은 from의 ordinal을, 열은 to의 ordinal을 인덱스로 쓴다. 
        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()];
        }
    }
}

 

문제점 

 

1. Phase나 Phase.Transition 열거 타입을 수정할 때, 상전이 표인 TRANSITIONS도 정확하게 수정해야 한다.

    컴파일러는 ordinal과 배열 인덱스의 관계를 알지 못하기 때문에, 실수로 수정하지 않거나 잘못 수정하면 런타임 오류가 

    ArrayIndexOutOfBoundException이나 NullPointerException을 던질 수도 있고, 운이 나쁘면 예외도 던지지 않고 이상하게 동작할 수도 있다.

 

2. 공간이 비효율적이다.

    상전이 표의 크기는 상태의 가짓수가 늘어남에 따라 제곱씩 커진다.

    또한 가짓수가 늘어날수록 null로 채워지는 칸도 늘어난다.

 


중첩 EnumMap을 사용하는 경우