일상/레벨업 독서

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

Gamii 2022. 4. 12. 20:26
728x90

핵심 정리

배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니, 대신 EnumMap을 사용하라. 다차원 관계는 EnumMap<..., EnumMap<...>>으로 표현하라. "애플리케이션 프로그래머는 Enum.ordinal을 (웬만해서는) 사용하지 말아야 한다(아이템 35)"는 일반 원칙의 특수한 사례다. 

 

 

 

ordinal 기반 인덱싱

 

배열이나 리스트에서 원소를 꺼낼 때 ordinal 메서드(아이템 35)로 인덱스를 얻는 코드가 있다. 식물의 생애주기를 열거 타입으로 표현한 LifeCycle 열거 타입을 예로 보자.

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

이제 정원에 심은 식물들을 배열 하나로 관리하고 이들을 생애주기(한해살이, 여러해살이, 두해살이)별로 묶어보자.

public static void ordinalArray(List<Plant> garden){
	Set<Plant>[] plantsByLifeCycleArr =
      (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];

    //1. Set을 3개의 배열(생애주기별)이 만들어진다. 각 배열을 순회하여 HashSet으로 초기화 해준다. 
    for (int i = 0; i < plantsByLifeCycleArr.length; i++){
      plantsByLifeCycleArr[i] = new HashSet<>();
    }

    //2.plant를 배열의 Set에 추가한다. 
    //배열의 인덱스는 plant가 가지고 있는 LifeCycle 열거타입의 ordinal로 정한다.
    for (Plant plant : garden){
        plantsByLifeCycleArr[plant.lifeCycle.ordinal()].add(p);
    }

    //3.결과 출력
    for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
      System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycleArr[i]);
    }
}

 

 

 

 

동작은 하지만 문제가 많다.

 

 

1. 배열은 제네릭과 호환되지 않으니(아이템 28) 비검사 형변환을 수행해야 하고 깔끔히 컴파일되지 않을 것이다.

 

2. 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다.

 

3. 정확한 정숫값을 사용한다는 것을 직접 보증해야 한다.

 

 

 

해결책

 

1. EnumMap을 사용해서 해결해보자.

public static void ordinalArray(List<Plant> garden){
	//위 코드와 다르게 안전하지 않은 형변환은 사용하지 않는다.
    Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);
    
    for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
        plantsByLifeCycle.put(lc, new HashSet<>());
    }
    
    //ordinal을 이용한 배열 인덱스를 사용하지 않아, 
    //인덱스를 계산하는 과정에서 오류가 날 가능성이 없어졌다.
    for (Plant p : garden) {
        plantsByLifeCycle.get(p.lifeCycle).add(p);
    }
    
    // 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공하니,
    // 출력 결과에 직접 레이블을 달 일도 없다.
    System.out.println(plantsByLifeCycle);
}

EnumMap의 성능이 ordinal을 쓴 배열에 비견되는 이유는 그 내부에서 배열을 사용하기 때문이다. 내부 구현 방식을 안으로 숨겨서 Map의 타입 안전성과 배열의 성능을 모두 얻어낸 것이다.

 

 

2. 스트림을 사용해 맵을 관리하면 코드를 더 줄일 수 있다.

//HashMap을 이용한 데이터와 열거타입 매핑
public static void streamCode1(List<Plant> garden){
	Map plant_map = garden.stream()
                          .collect(Collectors.groupingBy(plant -> plant.lifeCycle));
}

//EnumMap을 이용해 데이터와 열거타입 매핑
public static void streamCode2(List<Plant> garden){
    garden.stream()
          .collect(Collectors.groupingBy(plant -> plant.lifeCycle
                                         , () -> new EnumMap<>(LifeCycle.class)
                                         , toSet()));
}

streamCode1와 streamCode2의 차이는 groupingBy 메소드에 원하는 맵 구현체를 명시하였는가 차이다. 

streamCode1은 EnumMap이 아닌 고유한 맵 구현체를 사용했기 때문에 EnumMap을 써서 얻은 공간과 성능 이점이 사라진다는 문제가 있다. 

 

 

 

EnumMap 코드와 Stream 코드의 차이가 있다.

EnumMap 코드는 열거 타입 상수 별로 하나씩 모든 Key를 만든다. 반면, Stream 코드는 존재하는 열거 타입 상수만 Key를 만든다.

 

//EnumMap 코드 결과
{ANNUAL=[A, B], PERENNIAL=[C], BIENNIAL=[]}

//Stream 코드 결과
{ANNUAL=[A, B], PERENNIAL=[C]}