[Effective Java] 아이템37 - ordinal 인덱싱 대신 EnumMap을 사용하라
LifeCycle 이라는 enum Type을 가지는 Plant 클래스가 있다. Plant의 배열이 있는 상황에서, LiefeCycle 기준으로 Map을 만들어보자.
public 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;
}
}
ordinal 사용
아이템 35에서 알아봤듯이 enum type은 상수의 위치를 반환하는 ordinal 메서드를 가지고 있다. 이 값을 이용해서 배열의 인덱스로 사용해서 Map을 만들 수 있다.
Plant[] garden = {
new Plant("바질", LifeCycle.ANNUAL),
new Plant("캐러웨이", LifeCycle.BIENNIAL),
new Plant("딜", LifeCycle.ANNUAL),
new Plant("라벤더", LifeCycle.PERENNIAL),
new Plant("파슬리", LifeCycle.BIENNIAL),
new Plant("로즈마리", LifeCycle.PERENNIAL)
};
Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
// Plant의 LifeCycle enum 만큼 Set<> 배열을 만든다.
for(int i=0; i<plantsByLifeCycle.length; i++) {
plantsByLifeCycle[i] = new HashSet<>();
}
for(Plant p : garden) {
// Plant의 LifeCycle을 가져와서 해당 LifeCycle의 위치를 인덱스로 활용한다.
plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
}
이 방법의 문제점은, 제네릭 배열을 사용했기 때문에 비검사 형변환을 수행해야 한다. 안그러면 아래와 같은 warning이 뜬다.
Type safety: Unchecked cast from Set[] to Set
[]
더 심각한 문제는 ordinal()메서드에서 반환되는 정수가 정확한 정수값을 사용한다는 것을 보증해야 한다는 점이다.
EnumMap 사용
EnumMap은 짧고 명료하고 안전하게 enum 타입의 맵을 만들 수 있다.
// 배열을 사용하기 때문에 배열의 성능을 가지고 있다.
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class); // 한정적 타입 토큰
// 각 맵의 value (HashSet)을 먼저 초기화 해준다.
for (Plant.LifeCycle lc : Plant.LifeCycle.values() ) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
for(Plant p : garden) {
plantsByLifeCycle.get(p.lifeCycle).add(p);
}
EnumMap의 생성자가 받는 파라미터는 맵의 Key의 Type이고 이 Class 객체는 한정적 타입 토큰으로 런타임 제네렉 타입 정보를 제공한다. 따라서 Type 안정성도 보장할 수 있다.
스트림 활용
System.out.println(Arrays.stream(garden)
.collect(Collectors.groupingBy(p -> p.lifeCycle)));
Map<Object,List<Plant>> test = Arrays.stream(garden)
.collect(Collectors.groupingBy(p -> p.lifeCycle));
위와 같이 스트림을 사용해서, LifeCycle 을 키로 가지고 있는 맵을 만들 수도 있다. 하지만 반환 타입을 확인해보면 Map<Object, List<Plant>>
로 Key가 Object이고, Value가 List
System.out.println(Arrays.stream(garden)
.collect(Collectors.groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class),
Collectors.toSet())));
EnumMap<Plant.LifeCycle, Set<Plant>> test2 = Arrays.stream(garden)
.collect(Collectors.groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class),
Collectors.toSet()));
Collectors.groupingBy의 mapFactory
매개변수에 원하는 맵 구현체를 명시해 호출할 수 있다.
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier, Supplier
mapFactory, Collector<? super T, A, D> downstream)
그러면 return Type은 EnumMap<Plant.LifeCycle, Set
두 열거 타입 값들을 매핑하는 경우
한 개 이상의 값을 상수에 매핑하고자 하는 경우에는 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; // 필드1
private final Phase to; // 필드 2
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
// 상전이 맵을 초기화한다.
private static final Map<Phase, Map<Phase, Transition>>
m = Stream.of(values()).collect(groupingBy(t -> t.from,
() -> new EnumMap<>(Phase.class),
toMap(t -> t.to, t -> t,
(x, y) -> y, () -> new EnumMap<>(Phase.class))));
public static Transition from(Phase from, Phase to) {
return m.get(from).get(to);
}
}
}
여기서 상전이 맵을 어떻게 선언했는지 집중하면 된다.
- 첫번째 Collector에서는 이전(from) 상태를 기준으로 묶는다.
- SOLID, LIQUID, GAS
- 두번째 Collector에서는 이후(to) 상태를 기준을 묶고 값(MELT, SUMLIME)을 저장한다.
- (SOLID)-LIQUID=MELT, (SOLID)-GAS=SUBLIME
상전이 맵 m의 결과
{SOLID={LIQUID=MELT, GAS=SUBLIME}, LIQUID={SOLID=FREEZE, GAS=BOIL}, GAS={SOLID=DEPOSIT, LIQUID=CONDENSE}}
위 내용은 Collector.toMap 메서드를 잘 알아야 이해가 갈 것 같다.
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator mergeFunction, Supplier
mapFactory)
댓글남기기