[Effective Java] 아이템 37 - ordinal 인덱싱 대신 EnumMap을 사용하라
배열이나 리스트에서 원소를 꺼낼 때 ordinal 메소드로 인덱스를 얻는 코드가 있다.....
아이템 35를 참조해보면 ordinal() 메소드가 EnumSet과 EnumMap 같이 열거 타임 기반의 자료구조에 쓸 목적으로 설계되었다고는 하나, 절대 사용하지 말자는 멘션이 있다는 점을 회고해보자.
정원에 심은 식물들을 배열 하나로 관리하고, 이들을 생애주기 별로 묶어보는 등과 같은 기능을 위해 다음과 같이 코드를 짰다고 하자.
class Plant{
enum LifeCycle { ANUUAL, 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 값을 그 배열의 인덱스로 사용하려 할 것이다.
다음은 따라하면 안되는 코드이다.
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 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]);
}
따라하면 안되는 이유는 다음과 같다.
- 배열은 제너릭과 호환되지 않으니 비검사 형변환을 수행해야 하고 깔끔히 컴파일되지 않을 것이다(아이템 28)
- 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다.
- 정수는 열거타입과 달리 타입 안전하지 않기에, 정확한 정수값을 사용한다는 것을 프로그래머가 직접 보증해야한다.
- 잘못된 값을 사용하면 잘못된 동작을 묵묵히 수행하거나 ArrayIndexOutOfBoundsException을 던질 것이다.
이에 대한 해결책이 존재한다.
위 코드에서 배열은 실질적으로 열거 타입 상수를 값으로 매핑하는 일을 하기에 Map을 사용할 수 있다.
이러한 특징들을 이용하여 열거 타입을 키로 사용하도록 설계한 아주 빠른 Map 구현체가 바로 EnumMap이다.
EnumMap을 사용하여 위 코드를 다시 리팩토링하면 다음과 같다.
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(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);
- 제너릭과 배열을 같이 사용한 부분이 사라짐
- EnumMap을 사용함
- 출력 결과에 직접 레이블을 달 필요가 없어짐(sout(plantsByLifeCycle 부분)
더 짧고 명료하고 안전하고 성능도 원래 버전과 비등하다.
안전하지 않은 형변환(Set<Plant>[])은 쓰지 않고, 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공하니 출력 결과에 직접 레이블을 달 일 또한 없다.
더불어 배열 인덱스를 계산하는 과정에서 오류가 날 가능성도 존재하지 않는다.(ArrayIndexOutOfBoundsException 가능성 사라짐)
EnumMap의 성능이 ordinal()에 비견되는 이유는 내부에서 배열을 사용하기 때문이다. 이 때문에 내부 구현 방식을 안으로 숨겨서 Map의 타입 안전성과 배열의 성능을 모두 얻어낸 것이다.
EnumMap의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰으로 런타임 제너릭 타입 정보를 제공한다 - 아이템 33
스트림을 사용하면 위의 코드를 더욱 간결하게 줄일 수 있다. - 아이템 45
System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle)));
이 코드는 EnumMap이 아닌 고유한 맵 구현체를 사용했기 때문에 EnumMap을 써서 얻은 공간과 성능 이점이 사라진다는 문제가 있다.
매개변수 3개짜리 Collectors.groupingBy 메소드는 mapFactory 매개변수에 원하는 맵 구현체를 명시해 호출할 수 있다.
//EnumMap을 이용해 데이터와 열거 타입을 매핑했다.
System.out.println(Arrays.stream(garden).collect(groupingBy(p->p.lifeCycle, () -> new EnumMap<>(LifeCycle.class), toSet())));
위 코드처럼 단순한 프로그램에서는 최적화가 굳이 필요 없지만, 맵을 빈번히 사용하는 프로그램에서는 꼭 필요할 것이다.
스트림을 사용하면 EnumMap만 사용했을 때와는 살짝 다르게 동작한다.
EnumMap 버전은 언제나 식물의 생애주기당 하나씩의 중첩 맵을 만들지만, 스트림에서는 해당 생애주기에 속하는 식물이 있을 때만 만든다.
ordinal()이 사용되기에 매력적이게 생긴 코드가 있을 수도 있지만 주의하자.
컴파일러는 ordina()l과 배열 인덱스의 관계를 알 도리가 없기에 수정시에 런타임 오류가 날 수도 있거나 이상하게 동작하거나 예외를 던질 수 있다.
정리하자면 ordinal()을 사용하는 것보다는 EnumMap을 사용하는 편이 훨씬 낫다.
핵심 정리
배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않다. 그러니 대신에 EnumMap을 사용하라.
다차원 관계는 EnumMap<..., EnumMap<...>>으로 표현하라.