아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라. - ksw6169/effective-java GitHub Wiki
다음과 같은 식물(Plant) 클래스가 있을 때 이를 생애주기별로 분리해보자.
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;
}
}
이 때 다음과 같이 배열의 인덱스를 ordinal을 이용해 설정하는 코드가 있을 수 있는데, 배열은 제네릭과 호환되지 않으니 비검사 형변환으로 인해 깔끔히 컴파일되지 않을 것이다. 또한 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다.
/**
* 정원에 심은 식물들을 배열 하나로 관리하고, 이들을 생애주기 별로 묶는다.
* 생애주기별로 총 3개의 집합을 만들고 정원을 한 바퀴 돌며 각 실물들을 해당 집합에 넣는다.
*/
Set<Plant> plantsByLifeCycle[] = (Set<Plant>[]) new Set[LifeCycle.values().length];
for (int i=0; i<plantsByLifeCycle.length; i++) {
plantsByLifeCycle[i] = new HashSet<>();
}
for (Plant p : garden) {
// ordinal() 을 배열의 인덱스로 활용한다.
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]);
}
EnumMap은 열거 타입을 키로 사용하도록 설계한 Map 구현체로 아주 빠르다. EnumMap의 장점은 다음과 같다.
- 더 짧고 명료하면서 안전하고 성능도 원래 버전과 동일하다.
- 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공하니 출력 결과에 직접 레이블을 달 일도 없다.
- 나아가 배열 인덱스를 계산하는 과정에서 오류가 날 가능성도 원천봉쇄된다.
- EnumMap의 성능이 ordinal을 쓴 배열에 비견되는 이유는 그 내부에서 배열을 사용하기 때문이다.
- 내부 구현 방식을 안으로 숨겨서 Map의 타입 안전성과 배열의 성능을 모두 얻어낸 것이다.
- 여기서 EnumMap의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰으로 런타임 제네릭 타입 정보를 제공한다.
Map<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);
스트림을 사용하면 코드를 더 줄일 수 있다.
Map<LifeCycle, Set<Plant>> map = Arrays.stream(garden)
.collect(Collectors.groupingBy(p -> p.lifeCycle, Collectors.toSet()));
위 코드는 EnumMap이 아닌 고유한 맵 구현체를 사용했기 때문에 EnumMap을 써서 얻은 공간과 성능 이점이 사라진다는 문제가 있다. 매개변수 3개짜리 Collectors.groupingBy 메소드는 mapFactory 매개변수에 원하는 맵 구현체를 명시해 호출할 수 있다.
Map<LifeCycle, Set<Plant>> map = Arrays.stream(garden)
.collect(Collectors.groupingBy(p -> p.lifeCycle, () -> new EnumMap<>(LifeCycle.class), Collectors.toSet()));
이번에는 상태를 전이와 매핑되도록 구현해보자. 액체(LIQUID)에서 고체(SOLID) 로의 전이는 응고(FREEZE)가 되고, 액체에서 기체(GAS)로의 전이는 기화(BOIL)가 된다.
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()];
}
}
}
이전 예제와 마찬가지로 Phase나 Phase.Transtion 열거 타입을 수정하면서 TRANSITIONS를 함께 수정하지 않거나 실수로 잘못 수정하면 런타임 오류가 발생한다. 그리고 상태 전이표의 크기는 상태의 가짓수가 늘어나면 제곱해서 커지며 null로 채워지는 칸도 늘어날 것이다.
따라서 다음과 같이 EnumMap을 사용해 상태와 전이를 매핑해서 관리해보자. 단순히 상태와 전이값만 추가해주면 모든 코드가 문제 없이 동작한다.
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);
private final Phase from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
// 상태 전이 맵을 초기화한다.
private static final Map<Phase, Map<Phase, Transition>> m = Arrays.stream(Transition.values())
.collect(
Collectors.groupingBy(
t -> t.from,
() -> new EnumMap<>(Phase.class),
Collectors.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);
}
}
}
Collectors.toMap() 의 merge function
Stream을 Map으로 만들 때 key가 중복되는 경우 value를 어떻게 하나로 병합할지 방법을 제공하는 함수다. 위의 예제에서는 (x, y) → y 라고 작성했는데, 중복될 경우 y 값을 value로 사용하도록 하였다.
Stream<String[]> stream = Stream.of(
new String[][] {
{ "A", "1" },
{ "A", "2" },
{ "B", "3" },
{ "A", "333" }
});
Map<String, String> result = stream.collect(Collectors.toMap(p -> p[0], p -> p[1], (x, y) -> y));
result.entrySet().stream().forEach(e -> System.out.println(e.getKey() + ", " + e.getValue()));
A, 333
B, 3
- Effective Java 3/E
- Collectors toMap() method in Java with ExamplesㅣGeeksForGeeks