Item37. ordinal indexing 대신 EnumMap을 사용하라
2022-01-11 22:57:43
다음과 같이 식물 class가 있고 이 class를 LifeCycle enum type을 key로 set에 분류해서 담고 싶다고 가정하자.
public class Plant {
enum LifeCycle{ANNUAL,PERENNIAL , 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;
}
}
한가지 방법은 Set 배열에 ordinal method()로 가져온 LifeCycle Enum type의 index 값으로 배열에 indexing 해 저장하는 것이다.
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]);
}
위 코드는 다음과 같은 문제점을 가지고 있다.
- Generic 배열 : 타입안전하지 않음
- 배열은 각 Index가 무슨 Enum type인지 모름
- 상수의 위치가 변경될 경우 바로 고장남 (Item 35. ordinal() method는 사용하지 말라고 권고 )
EnumMap
EnumMap을 쓰면 위와 같이 ordinal method로 indexing 하는 단점을 제거해주고, 추가로 출력 문자열도 자체로 제공해준다. EnumMap은 runtime에서 generic type 정보 제공을 위해 생성자에서 key 로 사용할 class 객체를 받는다.
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 = " + plantsByLifeCycle);
Stream 방식
아래와 같이 stream을 사용해서 맵을 관리하면 코드를 더 줄일 수 있으나,EnumMap 구현체를 사용한게 아니라 고유한 Map 구현체를 사용했기 때문에 공간과 성능 이점이 사라진다. EnumMap 은 항상 enum 당 하나의 중첩 map을 만들지만, stream은 해당 enum type이 있을때에만 만든다.
Map<Plant.LifeCycle, List<Plant>> result
= garden.stream().collect(Collectors.groupingBy(Plant::getLifeCycle));
groupBy의 2번쨰 parameter인 결과가 삼입될 map을 다음과 같이 EnumMap 객체를 생성해주는 람다식을 넣어주면 EnumMap 객체의 장점을 활용할 수 있다.
garden.stream().collect(groupingBy(Plant::getLifeCycle,()->new EnumMap<>(LifeCycle.class),toSet()));
- 추가로 3개의 parameter 를 받는 groupBy Function의 1번쨰 parameter는 분류 방식, 2번쨰 parameter는 결과가 삼입될 빈 Map 객체를 반환해주는 supplier , 3번째 parameter는 결과를 집계해줄 collector를 받는다.
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T, A, D> downstream)

ordinal method를 사용하는 안좋은 예를 또 들면, 두 가지 상태(Phase) Enum 를 전이(Transition) Enum과 매핑하도록 구현한 프로그램이다.
public enum Phase {
SOLID, LIQUID , GAS;
public enum Transition{
MELT,FREEZE,BOIL,CONDENSE,SUBLIME,DEPOSIT;
// 행은 from의 ordinal 을 , 열은 to의 ordinal을 index로 사용한다.
public 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 method는 위에 설명한 이유들로 사용하면 안된다. 이를 EnumMap으로 다음과 같이 구현할 수 있다. 아래는 중첩 map으로 <이전상태,<이후상태,전이>> 형태로 EnumMap을 초기화하였다.
public enum Phase {
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 Phase from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
public Phase getFrom() {
return from;
}
public static final Map<Phase, Map<Phase,Transition>> m =
Stream.of(values()).collect(
groupingBy(Transition::getFrom,
()->new EnumMap<>(Phase.class),
toMap(
t->t.to,
t->t,
(x,y)->y,
()->new EnumMap<>(Phase.class)
)));
}
groupBy method에서 이전 상태를 기준으로 묶고, 이를 결과로 받을 EnumMap을 생성하고, 다시 Map으로 집계하는데, Map으로 집계할때는 다음과 같이 4개의 parameter가 사용되었다.
toMap(
t->t.to, // 이후 상태를 key로 사용
t->t, // value는 전이값
(x,y)->y, // 동일 key 시 처리 로직인데, 실제로는 사용되지 않음
()->new EnumMap<>(Phase.class) // 결과를 담을 빈 Map 객체 생성
)
toMap method api 설명을 보면 첫번쨰 parameter는 key를 생성해주는 함수, 두번쨰는 value를 생성해주는 함수 , 세번째는 같은 key 를 가지는 value를 어떻게 merge 할지에 대한 처리로직, 네번쨰는 결과가 삼입될 빈 map 객체를 만들어주는 함수를 작성하면 된다고 한다.

이전 oridinal method 방식에 비해 요구사항이 변경되었을때도 map을 생성하는 로직이 변경되지 않는다.
public enum Phase {
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);
// ...코드 변경되지 않음
}
}
댓글
이 게시글에 대한 의견을 공유해주세요!
